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..2dc79e9 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,6 @@
- 4.1.1
+ 5.0.0
net8.0
@@ -10,6 +10,6 @@
linux-x64
enable
- 11.2.1
+ 11.2.3
diff --git a/MelonLoader.Installer/.editorconfig b/MelonLoader.Installer/.editorconfig
index fc7a9b1..7a2086d 100644
--- a/MelonLoader.Installer/.editorconfig
+++ b/MelonLoader.Installer/.editorconfig
@@ -12,7 +12,7 @@ indent_style = space
tab_width = 4
# New line preferences
-end_of_line = crlf
+end_of_line = lf
insert_final_newline = false
#### .NET Code Actions ####
@@ -88,71 +88,71 @@ dotnet_style_allow_statement_immediately_after_block_experimental = false
#### C# Coding Conventions ####
# var preferences
-csharp_style_var_elsewhere = true
-csharp_style_var_for_built_in_types = true
-csharp_style_var_when_type_is_apparent = true
+csharp_style_var_elsewhere = true:warning
+csharp_style_var_for_built_in_types = true:warning
+csharp_style_var_when_type_is_apparent = true:warning
# Expression-bodied members
-csharp_style_expression_bodied_accessors = true
-csharp_style_expression_bodied_constructors = false
-csharp_style_expression_bodied_indexers = true
-csharp_style_expression_bodied_lambdas = true
-csharp_style_expression_bodied_local_functions = false
-csharp_style_expression_bodied_methods = false
-csharp_style_expression_bodied_operators = false
-csharp_style_expression_bodied_properties = true
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
# Pattern matching preferences
-csharp_style_pattern_matching_over_as_with_null_check = true
-csharp_style_pattern_matching_over_is_with_cast_check = true
-csharp_style_prefer_extended_property_pattern = true
-csharp_style_prefer_not_pattern = true
-csharp_style_prefer_pattern_matching = true
-csharp_style_prefer_switch_expression = true
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_prefer_extended_property_pattern = true:suggestion
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_prefer_pattern_matching = true:silent
+csharp_style_prefer_switch_expression = true:suggestion
# Null-checking preferences
-csharp_style_conditional_delegate_call = true
+csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
-csharp_prefer_static_anonymous_function = true
-csharp_prefer_static_local_function = true
+csharp_prefer_static_anonymous_function = true:suggestion
+csharp_prefer_static_local_function = true:suggestion
csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
-csharp_style_prefer_readonly_struct = true
-csharp_style_prefer_readonly_struct_member = true
+csharp_style_prefer_readonly_struct = true:suggestion
+csharp_style_prefer_readonly_struct_member = true:suggestion
# Code-block preferences
-csharp_prefer_braces = when_multiline
-csharp_prefer_simple_using_statement = true
-csharp_prefer_system_threading_lock = true
-csharp_style_namespace_declarations = file_scoped
-csharp_style_prefer_method_group_conversion = true
-csharp_style_prefer_primary_constructors = true
-csharp_style_prefer_top_level_statements = false
+csharp_prefer_braces = when_multiline:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_prefer_system_threading_lock = true:suggestion
+csharp_style_namespace_declarations = file_scoped:warning
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_style_prefer_top_level_statements = false:silent
# Expression-level preferences
-csharp_prefer_simple_default_expression = true
-csharp_style_deconstructed_variable_declaration = true
-csharp_style_implicit_object_creation_when_type_is_apparent = true
-csharp_style_inlined_variable_declaration = true
-csharp_style_prefer_index_operator = true
-csharp_style_prefer_local_over_anonymous_function = true
-csharp_style_prefer_null_check_over_type_check = true
-csharp_style_prefer_range_operator = true
-csharp_style_prefer_tuple_swap = true
-csharp_style_prefer_utf8_string_literals = true
-csharp_style_throw_expression = true
-csharp_style_unused_value_assignment_preference = discard_variable
-csharp_style_unused_value_expression_statement_preference = discard_variable
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_null_check_over_type_check = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_prefer_tuple_swap = true:suggestion
+csharp_style_prefer_utf8_string_literals = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
# 'using' directive preferences
-csharp_using_directive_placement = outside_namespace
+csharp_using_directive_placement = outside_namespace:warning
# New line preferences
-csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
-csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false
-csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
-csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
-csharp_style_allow_embedded_statements_on_same_line_experimental = false
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false:silent
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:warning
+csharp_style_allow_embedded_statements_on_same_line_experimental = false:warning
#### C# Formatting Rules ####
@@ -242,3 +242,41 @@ dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
+csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
+
+[*.{cs,vb}]
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = false:suggestion
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+tab_width = 4
+indent_size = 4
+end_of_line = lf
+dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
+dotnet_style_namespace_match_folder = true:warning
+dotnet_style_readonly_field = true:suggestion
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+dotnet_style_allow_multiple_blank_lines_experimental = false:warning
+dotnet_style_allow_statement_immediately_after_block_experimental = false:warning
+dotnet_code_quality_unused_parameters = non_public:suggestion
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
\ No newline at end of file
diff --git a/MelonLoader.Installer/App.axaml b/MelonLoader.Installer/App.axaml
index 4b98e36..3e80c63 100644
--- a/MelonLoader.Installer/App.axaml
+++ b/MelonLoader.Installer/App.axaml
@@ -6,9 +6,8 @@
-
+
-
\ No newline at end of file
diff --git a/MelonLoader.Installer/Assets/bg.png b/MelonLoader.Installer/Assets/bg.png
index 728c9a5..08cad83 100644
Binary files a/MelonLoader.Installer/Assets/bg.png and b/MelonLoader.Installer/Assets/bg.png differ
diff --git a/MelonLoader.Installer/Assets/loading-anim.gif b/MelonLoader.Installer/Assets/loading-anim.gif
new file mode 100644
index 0000000..9a92310
Binary files /dev/null and b/MelonLoader.Installer/Assets/loading-anim.gif differ
diff --git a/MelonLoader.Installer/Config.cs b/MelonLoader.Installer/Config.cs
index e8b059a..dea2552 100644
--- a/MelonLoader.Installer/Config.cs
+++ b/MelonLoader.Installer/Config.cs
@@ -13,12 +13,14 @@ 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");
- public static string[] LoadGameList()
+ static Config()
{
- if (!File.Exists(GameListPath))
- return [];
+ Directory.CreateDirectory(CacheDir);
+ }
- return File.ReadAllLines(GameListPath);
+ public static string[] LoadGameList()
+ {
+ return !File.Exists(GameListPath) ? [] : File.ReadAllLines(GameListPath);
}
public static void SaveGameList(IEnumerable gamePaths)
diff --git a/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs b/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs
index f9bfb9a..e84a857 100644
--- a/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs
+++ b/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs
@@ -4,8 +4,6 @@
namespace MelonLoader.Installer.GameLaunchers;
-#pragma warning disable CA1416
-
public class EgsLauncher : GameLauncher
{
private static readonly string? manifestDir;
@@ -19,36 +17,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"))
+ {
+ 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
{
- 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 _);
+ 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..07c63ee 100644
--- a/MelonLoader.Installer/GameLaunchers/GogLauncher.cs
+++ b/MelonLoader.Installer/GameLaunchers/GogLauncher.cs
@@ -3,8 +3,6 @@
namespace MelonLoader.Installer.GameLaunchers;
-#pragma warning disable CA1416
-
public class GogLauncher : GameLauncher
{
private static readonly RegistryKey? gamesKey;
@@ -16,7 +14,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 +32,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..9d572ac 100644
--- a/MelonLoader.Installer/GameLaunchers/SteamLauncher.cs
+++ b/MelonLoader.Installer/GameLaunchers/SteamLauncher.cs
@@ -4,8 +4,6 @@
namespace MelonLoader.Installer.GameLaunchers;
-#pragma warning disable CA1416
-
public class SteamLauncher : GameLauncher
{
private static readonly string? steamPath;
@@ -28,7 +26,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 +47,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 28d5065..f1af819 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);
+ }
+
+ 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++ / (double)tasks.Count, null));
}
- LoadSavedGames();
+ 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()
@@ -60,6 +70,7 @@ private static void AddGameSorted(GameModel game)
Games.Insert(i, game);
return;
}
+
if (!gameHasMl && iHasMl)
continue;
@@ -106,7 +117,7 @@ public static void RemoveGame(GameModel game)
var dataDirs = rawDataDirs.Where(x => File.Exists(x[..^5] + ".exe")).ToArray();
if (dataDirs.Length == 0)
{
- dataDirs = rawDataDirs.Where(x => File.Exists(x[..^5] + ".x86_64")).ToArray();
+ dataDirs = [.. rawDataDirs.Where(x => File.Exists(x[..^5] + ".x86_64"))];
if (dataDirs.Length != 0)
{
linux = true;
@@ -117,7 +128,7 @@ public static void RemoveGame(GameModel game)
return null;
}
}
-
+
if (dataDirs.Length > 1)
{
errorMessage = "The selected directory contains multiple Unity games?";
@@ -126,10 +137,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 +186,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/IconExtractor.cs b/MelonLoader.Installer/IconExtractor.cs
index 1542f3b..a464452 100644
--- a/MelonLoader.Installer/IconExtractor.cs
+++ b/MelonLoader.Installer/IconExtractor.cs
@@ -3,8 +3,6 @@
namespace MelonLoader.Installer;
-#pragma warning disable CA1416
-
internal class IconExtractor
{
public static Avalonia.Media.Imaging.Bitmap? GetExeIcon(string filePath)
diff --git a/MelonLoader.Installer/InstallerUtils.cs b/MelonLoader.Installer/InstallerUtils.cs
index e69c267..b98877b 100644
--- a/MelonLoader.Installer/InstallerUtils.cs
+++ b/MelonLoader.Installer/InstallerUtils.cs
@@ -51,8 +51,8 @@ static InstallerUtils()
var buffer = new byte[1024 * 16];
while (position < destination.Length - 1)
{
- var read = await content.ReadAsync(buffer, 0, buffer.Length);
- await destination.WriteAsync(buffer, 0, read);
+ var read = await content.ReadAsync(buffer);
+ await destination.WriteAsync(buffer.AsMemory(0, read));
position += read;
diff --git a/MelonLoader.Installer/MLManager.cs b/MelonLoader.Installer/MLManager.cs
index e7c9500..4b243a0 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
@@ -10,8 +9,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",
@@ -26,6 +24,8 @@ internal static class MLManager
public static List Versions { get; } = [];
+ public static bool RefreshedOnce { get; private set; }
+
static MLManager()
{
Program.Exiting += HandleExit;
@@ -43,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}'";
- var relStr = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
+ onProgress?.Invoke(0.4, "Adding MelonLoader workflow builds");
+
+ 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"
@@ -90,7 +80,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,
@@ -102,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)
@@ -143,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)
@@ -387,6 +382,7 @@ void SetProgress(double progress, string? newStatus = null)
onFinished?.Invoke("Failed to download MelonLoader: " + result);
return;
}
+
bufferStr.Seek(0, SeekOrigin.Begin);
currentTask++;
diff --git a/MelonLoader.Installer/MLVersion.cs b/MelonLoader.Installer/MLVersion.cs
index f6e9900..1f1dd66 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;
@@ -36,6 +36,7 @@ public class MLVersion
}
}
}
+
if (mlAssemblyPath == null)
return null;
@@ -45,7 +46,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
{
diff --git a/MelonLoader.Installer/MelonLoader.Installer.csproj b/MelonLoader.Installer/MelonLoader.Installer.csproj
index 901bdbb..d15095c 100644
--- a/MelonLoader.Installer/MelonLoader.Installer.csproj
+++ b/MelonLoader.Installer/MelonLoader.Installer.csproj
@@ -15,10 +15,10 @@
en
true
true
+ MelonLoader.Installer.Program
Lava Gang
discord.gg/2Wn3N2P
- MelonLoader.Installer.Program
@@ -42,13 +42,14 @@
-
+
+
-
+
diff --git a/MelonLoader.Installer/Program.cs b/MelonLoader.Installer/Program.cs
index 728cc61..50cb8a1 100644
--- a/MelonLoader.Installer/Program.cs
+++ b/MelonLoader.Installer/Program.cs
@@ -1,6 +1,14 @@
using Avalonia;
using System.Diagnostics;
-using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+[assembly: SupportedOSPlatform(
+#if WINDOWS
+ "windows"
+#elif LINUX
+ "linux"
+#endif
+)]
namespace MelonLoader.Installer;
@@ -22,9 +30,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))
@@ -42,11 +47,6 @@ private static void Main(string[] args)
}
}
-#if WINDOWS
- if (Updater.CheckLegacyUpdate())
- return;
-#endif
-
if (!CheckProcessLock())
return;
@@ -60,7 +60,7 @@ private static void Main(string[] args)
}
Exiting?.Invoke();
-
+
processLock.Dispose();
File.Delete(processLockPath);
}
@@ -99,9 +99,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..447b01a 100644
--- a/MelonLoader.Installer/Updater.cs
+++ b/MelonLoader.Installer/Updater.cs
@@ -1,36 +1,51 @@
using System.Diagnostics;
-using System.Runtime.InteropServices;
using System.Text.Json.Nodes;
+#if LINUX
+using System.Runtime.InteropServices;
+#endif
+
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)
@@ -76,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;
@@ -161,10 +130,10 @@ public static bool CheckLegacyUpdate()
".Linux"
#endif
));
-
+
return asset?["browser_download_url"]?.ToString();
}
-
+
#if LINUX
// user permissions
const int S_IRUSR = 0x100;
@@ -182,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/DialogBoxModel.cs b/MelonLoader.Installer/ViewModels/DialogBoxModel.cs
index 897c853..fcbdbb8 100644
--- a/MelonLoader.Installer/ViewModels/DialogBoxModel.cs
+++ b/MelonLoader.Installer/ViewModels/DialogBoxModel.cs
@@ -3,8 +3,8 @@
public class DialogBoxModel : ViewModelBase
{
public required string Message { get; init; }
- public string ConfirmText { get; init; } = "YES";
- public string CancelText { get; init; } = "NO";
+ public string ConfirmText { get; init; } = "Yes";
+ public string CancelText { get; init; } = "No";
public bool IsError { get; init; }
public bool IsConfirmation { get; init; }
}
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/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 b/MelonLoader.Installer/Views/DetailsView.axaml
index 66213ee..0b2d662 100644
--- a/MelonLoader.Installer/Views/DetailsView.axaml
+++ b/MelonLoader.Installer/Views/DetailsView.axaml
@@ -3,81 +3,83 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:MelonLoader.Installer.ViewModels"
- mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="650"
+ mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="650"
x:Class="MelonLoader.Installer.Views.DetailsView"
x:DataType="vm:DetailsViewModel">
-
+
-
-
-
-
-
-
-
-
-
-
- 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/DetailsView.axaml.cs b/MelonLoader.Installer/Views/DetailsView.axaml.cs
index e3e9320..67be99f 100644
--- a/MelonLoader.Installer/Views/DetailsView.axaml.cs
+++ b/MelonLoader.Installer/Views/DetailsView.axaml.cs
@@ -5,13 +5,12 @@
using Avalonia.Threading;
using MelonLoader.Installer.ViewModels;
using System.ComponentModel;
-using System.Runtime.InteropServices;
namespace MelonLoader.Installer.Views;
public partial class DetailsView : UserControl
{
- public DetailsViewModel? Model => (DetailsViewModel?)DataContext;
+ public DetailsViewModel? Model => DataContext as DetailsViewModel;
public DetailsView()
{
@@ -35,14 +34,14 @@ private void PropertyChangedHandler(object? sender, PropertyChangedEventArgs cha
UpdateVersionInfo();
}
}
-
- protected override async void OnDataContextChanged(EventArgs e)
+
+ protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (Model == null)
return;
-
+
#if LINUX
if (Model.Game.IsLinux)
{
@@ -56,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)
@@ -78,6 +71,8 @@ public void UpdateVersionList()
if (NightlyCheck.IsChecked != true)
en = en.Where(x => !x.Version.IsPrerelease || x.IsLocalPath);
+ Model.Offline = !en.Any();
+
VersionCombobox.ItemsSource = en;
VersionCombobox.SelectedIndex = 0;
}
@@ -86,13 +81,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 +171,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))
{
@@ -192,7 +187,7 @@ private void OnOperationFinished(string? errorMessage, bool addedLocalBuild = fa
};
}
- DialogBox.ShowNotice("SUCCESS!", $"Successfully {operationType}{((!Model.Game.MLInstalled || isInstall) ? string.Empty : " to")}\nMelonLoader v{(Model.Game.MLInstalled ? Model.Game.MLVersion : currentMLVersion)}");
+ DialogBox.ShowNotice("Success!", $"Successfully {operationType}{((!Model.Game.MLInstalled || isInstall) ? string.Empty : " to")}\nMelonLoader v{(Model.Game.MLInstalled ? Model.Game.MLVersion : currentMLVersion)}");
}
private void OpenDirHandler(object sender, RoutedEventArgs args)
@@ -268,7 +263,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 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 @@
-
+
diff --git a/MelonLoader.Installer/Views/DialogBox.axaml.cs b/MelonLoader.Installer/Views/DialogBox.axaml.cs
index a843710..a5b3678 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;
@@ -15,12 +16,12 @@ public DialogBox()
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
-
+
OnCancel?.Invoke();
}
public static void ShowError(string message, Action? onClose = null)
- => ShowError("ERROR!", message, onClose);
+ => ShowError("MelonLoader Installer: Error", message, onClose);
public static void ShowError(string title, string message, Action? onClose = null)
{
@@ -37,7 +38,7 @@ public static void ShowError(string title, string message, Action? onClose = nul
}
public static void ShowNotice(string message)
- => ShowNotice("NOTICE", message);
+ => ShowNotice("MelonLoader Installer: Notice", message);
public static void ShowNotice(string title, string message)
{
@@ -55,9 +56,9 @@ public static void ShowConfirmation(
string message,
Action? onConfirm = null,
Action? onCancel = null,
- string confirmText = "YES",
- string cancelText = "NO")
- => ShowConfirmation("CONFIRMATION",
+ string confirmText = "Yes",
+ string cancelText = "No")
+ => ShowConfirmation("MelonLoader Installer",
message,
onConfirm,
onCancel,
@@ -69,8 +70,8 @@ public static void ShowConfirmation(
string message,
Action? onConfirm = null,
Action? onCancel = null,
- string confirmText = "YES",
- string cancelText = "NO")
+ string confirmText = "Yes",
+ string cancelText = "No")
{
new DialogBox
{
@@ -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/GameControl.axaml b/MelonLoader.Installer/Views/GameControl.axaml
index 05f1f8b..0cf85b2 100644
--- a/MelonLoader.Installer/Views/GameControl.axaml
+++ b/MelonLoader.Installer/Views/GameControl.axaml
@@ -3,19 +3,19 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:MelonLoader.Installer.ViewModels"
- mc:Ignorable="d" d:DesignWidth="400"
+ mc:Ignorable="d"
x:Class="MelonLoader.Installer.Views.GameControl"
x:DataType="vm:GameModel"
- Margin="0 0 0 5">
-
\ No newline at end of file
diff --git a/MelonLoader.Installer/Views/MainView.axaml.cs b/MelonLoader.Installer/Views/MainView.axaml.cs
index 570326b..9712fed 100644
--- a/MelonLoader.Installer/Views/MainView.axaml.cs
+++ b/MelonLoader.Installer/Views/MainView.axaml.cs
@@ -8,7 +8,9 @@ namespace MelonLoader.Installer.Views;
public partial class MainView : UserControl
{
private static bool showedNotice;
-
+
+ private static DateTime _lastTimeCheckedVersions;
+
public MainViewModel? Model => (MainViewModel?)DataContext;
public MainView()
@@ -20,20 +22,25 @@ protected override async void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
- if (Model == null)
+ if (Model == null || MainWindow.Instance == null)
return;
- // if the updater has already ran, we already did all initialization
- if (Updater.State == Updater.UpdateState.None)
+ if (!GameManager.Initialized)
{
- Model.Ready = false;
- if (!await DoInit())
- {
- return;
- }
- Model.Ready = true;
+ await GameManager.InitAsync(MainWindow.Instance.SetLoadStatus);
}
+ if (DateTime.UtcNow - _lastTimeCheckedVersions > TimeSpan.FromMinutes(3))
+ {
+ _lastTimeCheckedVersions = DateTime.UtcNow;
+
+ var error = await MLManager.RefreshVersionsAsync(MainWindow.Instance.SetLoadStatus);
+ if (error != null)
+ DialogBox.ShowError(error);
+ }
+
+ MainWindow.Instance.FinishLoad();
+
OnGameListUpdate(null, null);
GameManager.Games.CollectionChanged += OnGameListUpdate;
@@ -42,42 +49,11 @@ protected override async void OnDataContextChanged(EventArgs e)
showedNotice = true;
DialogBox.ShowNotice("""
You're currently using a bleeding-edge CI build.
- Please note that this build will not auto-update, so it's recommended to use a stable release instead.
+ Please note that this build will not auto-update, so it's recommended to use a stable one instead.
""");
}
}
- private static async Task DoInit()
- {
- try
- {
- var checkUpdate = Task.Run(Updater.UpdateIfPossible);
- var otherInit = Task.WhenAll(Task.Run(MLManager.Init), Task.Factory.StartNew(GameManager.Init, TaskCreationOptions.LongRunning));
- if (await checkUpdate is { } updateTask)
- {
- _ = MainWindow.Instance.HandleUpdate(updateTask);
- return false;
- }
- await otherInit;
- }
- catch (Exception ex)
- {
- CrashException(ex);
- }
- return true;
- }
-
- 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 f40c41a..9db940e 100644
--- a/MelonLoader.Installer/Views/MainWindow.axaml
+++ b/MelonLoader.Installer/Views/MainWindow.axaml
@@ -3,7 +3,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
- Width="500" Height="700"
+ MinWidth="700" MinHeight="700"
+ Width="1200" Height="800"
x:Class="MelonLoader.Installer.Views.MainWindow"
Icon="/Assets/icon.ico"
Title="MelonLoader Installer"
diff --git a/MelonLoader.Installer/Views/MainWindow.axaml.cs b/MelonLoader.Installer/Views/MainWindow.axaml.cs
index 63eddcb..fd5a097 100644
--- a/MelonLoader.Installer/Views/MainWindow.axaml.cs
+++ b/MelonLoader.Installer/Views/MainWindow.axaml.cs
@@ -9,28 +9,43 @@ public partial class MainWindow : Window
{
public static MainWindow Instance { get; private set; } = null!;
+ private static ViewState? _lastViewState;
+
+ public static bool InLoadView => _currentLoadView != null;
+
+ private static LoadView? _currentLoadView;
+
+ private static readonly List _closeLocks = [];
+
public MainWindow()
{
Instance = this;
InitializeComponent();
- ShowMainView();
+ InitAsync();
}
- public async Task HandleUpdate(Task updaterTask)
+ public async void InitAsync()
{
- try
- {
- SetViewport(new UpdaterView());
- await updaterTask;
- Close();
- }
- catch (Exception ex)
+ using (RequestCloseLock())
{
- DialogBox.ShowError(ex.Message);
- ShowMainView();
+ if (await Updater.UpdateIfPossibleAsync(SetLoadStatus))
+ {
+ Close();
+ return;
+ }
}
+
+ ShowMainView();
+ FinishLoad();
+ }
+
+ public static CloseLock RequestCloseLock()
+ {
+ var loc = new CloseLock();
+ _closeLocks.Add(loc);
+ return loc;
}
protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e)
@@ -39,7 +54,7 @@ protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e)
if (!IsVisible)
return;
-
+
Topmost = true;
Topmost = false;
#if WINDOWS
@@ -50,27 +65,107 @@ protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e)
protected override void OnClosing(WindowClosingEventArgs e)
{
- if (Updater.State == Updater.UpdateState.Updating || Content is DetailsView { Model.Installing: true })
- e.Cancel = true;
+ if (_closeLocks.Count != 0)
+ return;
base.OnClosing(e);
}
- public void SetViewport(UserControl view)
+ public void SetViewport(ViewModelBase? model = null) where T : UserControl, new()
{
- Viewport.Child = view;
+ if (!Dispatcher.UIThread.CheckAccess())
+ {
+ Dispatcher.UIThread.Invoke(() => SetViewport(model));
+ return;
+ }
+
+ _lastViewState = new ViewState(model);
+
+ if (!InLoadView)
+ {
+ Viewport.Child = _lastViewState.Get();
+ }
}
public void ShowMainView()
{
- SetViewport(new MainView());
+ SetViewport();
+ }
+
+ public void SetLoadStatus(double progress, string? newStatus)
+ {
+ if (!Dispatcher.UIThread.CheckAccess())
+ {
+ Dispatcher.UIThread.Invoke(() => SetLoadStatus(progress, newStatus));
+ return;
+ }
+
+ if (_currentLoadView == null)
+ {
+ if (_lastViewState != null)
+ {
+ _lastViewState.Model = Viewport.Child?.DataContext as ViewModelBase;
+ }
+
+ _currentLoadView = new LoadView();
+ Viewport.Child = _currentLoadView;
+ }
+
+ _currentLoadView.SetProgress(progress, newStatus);
+ }
+
+ public void FinishLoad()
+ {
+ if (!Dispatcher.UIThread.CheckAccess())
+ {
+ Dispatcher.UIThread.Invoke(FinishLoad);
+ return;
+ }
+
+ if (!InLoadView)
+ return;
+
+ _currentLoadView = null;
+
+ if (_lastViewState == null)
+ {
+ Viewport.Child = null;
+ return;
+ }
+
+ Viewport.Child = _lastViewState.Get();
}
public void ShowDetailsView(GameModel game)
{
- SetViewport(new DetailsView()
+ SetViewport(new DetailsViewModel(game));
+ }
+
+ public class CloseLock : IDisposable
+ {
+ public void Dispose()
{
- DataContext = new DetailsViewModel(game)
- });
+ _closeLocks.Remove(this);
+ GC.SuppressFinalize(this);
+ }
+ }
+
+ private abstract class ViewState(ViewModelBase? model)
+ {
+ public ViewModelBase? Model { get; set; } = model;
+
+ public abstract UserControl Get();
+ }
+
+ private class ViewState(ViewModelBase? model) : ViewState(model) where T : UserControl, new()
+ {
+ public override UserControl Get()
+ {
+ var obj = new T();
+ if (Model != null)
+ obj.DataContext = Model;
+
+ return obj;
+ }
}
}
diff --git a/MelonLoader.Installer/Views/UpdaterView.axaml b/MelonLoader.Installer/Views/UpdaterView.axaml
deleted file mode 100644
index fbac528..0000000
--- a/MelonLoader.Installer/Views/UpdaterView.axaml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
- Updating...
-
-
-
\ No newline at end of file
diff --git a/MelonLoader.Installer/Views/UpdaterView.axaml.cs b/MelonLoader.Installer/Views/UpdaterView.axaml.cs
deleted file mode 100644
index 691604c..0000000
--- a/MelonLoader.Installer/Views/UpdaterView.axaml.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Threading;
-
-namespace MelonLoader.Installer.Views;
-
-public partial class UpdaterView : UserControl
-{
- public UpdaterView()
- {
- InitializeComponent();
-
- Updater.Progress += (progress, _) => Dispatcher.UIThread.Post(() => OnProgress(progress));
- }
-
- private void OnProgress(double progress)
- {
- Progress.Value = progress * 100;
- }
-}
\ No newline at end of file