From 8521f695a91831a76511e80b2d82b5abfcde07a3 Mon Sep 17 00:00:00 2001 From: Ryan Macham Date: Mon, 6 Apr 2026 14:50:33 +0100 Subject: [PATCH] Removed JavaService, and moved Java handling to CmlLib. --- .idea/.idea.AlayaCore/.idea/vcs.xml | 6 + AlayaCore.sln.DotSettings.user | 21 ++- .../Interfaces/Clients/IHttpClient.cs | 2 + .../Services/IGameInstallService.cs | 1 + .../Interfaces/Services/IManifestService.cs | 2 +- AlayaCore/LaunchDirector.cs | 61 ++++----- AlayaCore/Models/Manifests/DTO/ManifestDto.cs | 3 + .../Models/Manifests/LauncherManifestModel.cs | 2 +- AlayaCore/Models/Manifests/ManifestModel.cs | 6 - AlayaCore/Services/AuthService.cs | 32 ++++- AlayaCore/Services/GameInstallService.cs | 67 +++++---- AlayaCore/Services/GameLaunchService.cs | 27 ++-- AlayaCore/Services/HttpDownloadService.cs | 65 +++++---- AlayaCore/Services/InstallStateService.cs | 43 ++++-- AlayaCore/Services/LauncherUpdateService.cs | 53 ++++++-- AlayaCore/Services/ManifestService.cs | 128 ++++++++++-------- AlayaCore/States/LaunchPlan.cs | 9 -- .../Utilities/Extensions/MappingExtensions.cs | 4 - AlayaCore/Utilities/Stores/LocalFileStore.cs | 22 ++- 19 files changed, 348 insertions(+), 206 deletions(-) create mode 100644 .idea/.idea.AlayaCore/.idea/vcs.xml diff --git a/.idea/.idea.AlayaCore/.idea/vcs.xml b/.idea/.idea.AlayaCore/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.AlayaCore/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AlayaCore.sln.DotSettings.user b/AlayaCore.sln.DotSettings.user index d83be9d..c9dd86c 100644 --- a/AlayaCore.sln.DotSettings.user +++ b/AlayaCore.sln.DotSettings.user @@ -1,3 +1,22 @@  + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded - ForceIncluded \ No newline at end of file + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded \ No newline at end of file diff --git a/AlayaCore/Abstractions/Interfaces/Clients/IHttpClient.cs b/AlayaCore/Abstractions/Interfaces/Clients/IHttpClient.cs index b34bb34..42d7bd0 100644 --- a/AlayaCore/Abstractions/Interfaces/Clients/IHttpClient.cs +++ b/AlayaCore/Abstractions/Interfaces/Clients/IHttpClient.cs @@ -11,5 +11,7 @@ namespace AlayaCore.Abstractions.Interfaces.Clients Uri uri, HttpCompletionOption completionOption, CancellationToken cancellationToken); + + HttpClient HttpClient { get; } } } \ No newline at end of file diff --git a/AlayaCore/Abstractions/Interfaces/Services/IGameInstallService.cs b/AlayaCore/Abstractions/Interfaces/Services/IGameInstallService.cs index 8c78eea..5fffa91 100644 --- a/AlayaCore/Abstractions/Interfaces/Services/IGameInstallService.cs +++ b/AlayaCore/Abstractions/Interfaces/Services/IGameInstallService.cs @@ -10,6 +10,7 @@ namespace AlayaCore.Abstractions.Interfaces.Services { public interface IGameInstallService { + Task EnsureMinecraftInstalledAsync( ManifestModel manifest, InstallEnvironment environment, diff --git a/AlayaCore/Abstractions/Interfaces/Services/IManifestService.cs b/AlayaCore/Abstractions/Interfaces/Services/IManifestService.cs index f6d8f31..b60a74e 100644 --- a/AlayaCore/Abstractions/Interfaces/Services/IManifestService.cs +++ b/AlayaCore/Abstractions/Interfaces/Services/IManifestService.cs @@ -16,7 +16,7 @@ namespace AlayaCore.Abstractions.Interfaces.Services Task GetLocalLauncherManifestAsync(CancellationToken cancellationToken = default); - Task GetRemoteLauncherManifestHashAsync(CancellationToken cancellationToken = default); + Task GetRemoteLauncherManifestAsync(CancellationToken cancellationToken = default); Task GetRemoteCoreManifestVersionAsync(CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/AlayaCore/LaunchDirector.cs b/AlayaCore/LaunchDirector.cs index d8ff114..8e001e3 100644 --- a/AlayaCore/LaunchDirector.cs +++ b/AlayaCore/LaunchDirector.cs @@ -20,7 +20,6 @@ namespace AlayaCore private readonly IManifestService _manifestService; private readonly IUpdateService _updateService; private readonly IInstallStateService _installStateService; - private readonly IJavaService _javaService; private readonly IModService _modService; private readonly IGameLaunchService _gameLaunchService; private readonly IGameInstallService _gameInstallService; @@ -36,7 +35,6 @@ namespace AlayaCore IManifestService manifestService, IUpdateService updateService, IInstallStateService installStateService, - IJavaService javaService, IModService modService, IGameLaunchService gameLaunchService, IGameInstallService gameInstallService, @@ -47,7 +45,6 @@ namespace AlayaCore _manifestService = manifestService ?? throw new ArgumentNullException(nameof(manifestService)); _updateService = updateService ?? throw new ArgumentNullException(nameof(updateService)); _installStateService = installStateService ?? throw new ArgumentNullException(nameof(installStateService)); - _javaService = javaService ?? throw new ArgumentNullException(nameof(javaService)); _modService = modService ?? throw new ArgumentNullException(nameof(modService)); _gameLaunchService = gameLaunchService ?? throw new ArgumentNullException(nameof(gameLaunchService)); _gameInstallService = gameInstallService ?? throw new ArgumentNullException(nameof(gameInstallService)); @@ -68,7 +65,6 @@ namespace AlayaCore { LaunchPlan launcherUpdatePlan = new LaunchPlan( launcherNeedsUpdate: true, - javaNeedsInstallOrUpdate: false, minecraftNeedsInstallOrUpdate: false, neoforgeNeedsInstallOrUpdate: false, modsNeedSync: false, @@ -84,17 +80,13 @@ namespace AlayaCore .GetCurrentEnvironmentAsync(cancellationToken) .ConfigureAwait(false); - bool javaNeedsInstallOrUpdate = - _options.ForceReinstall || - !environment.JavaInstalled || - !string.Equals(environment.JavaVersion, manifest.RequiredJavaVersion, StringComparison.OrdinalIgnoreCase); - bool needAuthenticating = !await _authService .IsAuthenticatedAsync(cancellationToken) .ConfigureAwait(false); - + bool minecraftNeedsInstallOrUpdate = _options.ForceReinstall || + !environment.JavaInstalled || !environment.MinecraftInstalled || !string.Equals(environment.MinecraftVersion, manifest.MinecraftVersion, StringComparison.OrdinalIgnoreCase); @@ -109,7 +101,6 @@ namespace AlayaCore LaunchPlan plan = new LaunchPlan( launcherNeedsUpdate: false, - javaNeedsInstallOrUpdate: javaNeedsInstallOrUpdate, minecraftNeedsInstallOrUpdate: minecraftNeedsInstallOrUpdate, neoforgeNeedsInstallOrUpdate: neoforgeNeedsInstallOrUpdate, modsNeedSync: modsNeedSync, @@ -119,8 +110,9 @@ namespace AlayaCore return plan; } - public async Task InstallOrUpdateAsync(CancellationToken cancellationToken = default, - EventHandler? minecraftProgess = null, + public async Task InstallOrUpdateAsync( + CancellationToken cancellationToken = default, + EventHandler? minecraftProgress = null, EventHandler? byteProgress = null, IProgress? neoForgeProgress = null, IProgress? neoForgeByteProgress = null) @@ -133,8 +125,8 @@ namespace AlayaCore { cancellationToken.ThrowIfCancellationRequested(); - ManifestModel? manifest; - InstallEnvironment? environment; + ManifestModel manifest; + InstallEnvironment environment; switch (plan.State) { @@ -152,23 +144,12 @@ namespace AlayaCore return; } - case LaunchState.InstallJava: - { - manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false); - environment = await _installStateService - .GetCurrentEnvironmentAsync(cancellationToken) - .ConfigureAwait(false); - - await _javaService - .EnsureValidJavaInstalledAsync(manifest, environment, cancellationToken) - .ConfigureAwait(false); - - break; - } - case LaunchState.NeedAuthenticating: { - await _authService.AuthenticateAsync(cancellationToken); + await _authService + .AuthenticateAsync(cancellationToken) + .ConfigureAwait(false); + break; } @@ -180,7 +161,12 @@ namespace AlayaCore .ConfigureAwait(false); await _gameInstallService - .EnsureMinecraftInstalledAsync(manifest, environment, cancellationToken, minecraftProgess, byteProgress) + .EnsureMinecraftInstalledAsync( + manifest, + environment, + cancellationToken, + minecraftProgress, + byteProgress) .ConfigureAwait(false); break; @@ -194,8 +180,13 @@ namespace AlayaCore .ConfigureAwait(false); await _gameInstallService - .EnsureNeoForgeInstalledAsync(manifest, environment, cancellationToken, neoForgeProgress, neoForgeByteProgress) - .ConfigureAwait(false); + .EnsureNeoForgeInstalledAsync( + manifest, + environment, + cancellationToken, + neoForgeProgress, + neoForgeByteProgress) + .ConfigureAwait(false); break; } @@ -230,7 +221,9 @@ namespace AlayaCore if (_options.ForceReinstall) { - await _settingsService.SetForceReinstallAsync(false, cancellationToken).ConfigureAwait(false); + await _settingsService + .SetForceReinstallAsync(false, cancellationToken) + .ConfigureAwait(false); } plan = await EvaluateAsync(cancellationToken).ConfigureAwait(false); diff --git a/AlayaCore/Models/Manifests/DTO/ManifestDto.cs b/AlayaCore/Models/Manifests/DTO/ManifestDto.cs index 14f99f2..f9a4231 100644 --- a/AlayaCore/Models/Manifests/DTO/ManifestDto.cs +++ b/AlayaCore/Models/Manifests/DTO/ManifestDto.cs @@ -19,6 +19,9 @@ namespace AlayaCore.Models.Manifests.DTO [JsonProperty("javaUrl", Required = Required.Always)] [JsonConverter(typeof(UriConverter))] public Uri RequiredJavaUrl { get; set; } = null!; + + [JsonProperty("javaArchiveHash", Required = Required.Always)] + public string JavaArchiveHash { get; set; } = string.Empty; [JsonProperty("minecraftVersion", Required = Required.Always)] public string MinecraftVersion { get; set; } = string.Empty; diff --git a/AlayaCore/Models/Manifests/LauncherManifestModel.cs b/AlayaCore/Models/Manifests/LauncherManifestModel.cs index fe8daae..d6b4034 100644 --- a/AlayaCore/Models/Manifests/LauncherManifestModel.cs +++ b/AlayaCore/Models/Manifests/LauncherManifestModel.cs @@ -4,7 +4,7 @@ namespace AlayaCore.Models.Manifests { public sealed class LauncherManifestModel { - public Version Version { get; } + public Version? Version { get; } public string Sha512Hash { get; } public Uri DownloadUri { get; } diff --git a/AlayaCore/Models/Manifests/ManifestModel.cs b/AlayaCore/Models/Manifests/ManifestModel.cs index 5e02235..40df915 100644 --- a/AlayaCore/Models/Manifests/ManifestModel.cs +++ b/AlayaCore/Models/Manifests/ManifestModel.cs @@ -9,8 +9,6 @@ namespace AlayaCore.Models.Manifests public sealed class ManifestModel { public Version AlayaVersion { get; } - public string RequiredJavaVersion { get; } - public Uri RequiredJavaBaseUrl { get; } public string MinecraftVersion { get; } public string NeoforgedVersion { get; } @@ -20,8 +18,6 @@ namespace AlayaCore.Models.Manifests public ManifestModel( Version alayaVersion, - string requiredJavaVersion, - Uri requiredJavaUrl, string minecraftVersion, string neoforgedVersion, Uri serverUrl, @@ -29,8 +25,6 @@ namespace AlayaCore.Models.Manifests IEnumerable files) { AlayaVersion = alayaVersion ?? throw new ArgumentNullException(nameof(alayaVersion)); - RequiredJavaVersion = RequireNonEmpty(requiredJavaVersion, nameof(requiredJavaVersion)); - RequiredJavaBaseUrl = requiredJavaUrl ?? throw new ArgumentNullException(nameof(requiredJavaUrl)); MinecraftVersion = RequireNonEmpty(minecraftVersion, nameof(minecraftVersion)); diff --git a/AlayaCore/Services/AuthService.cs b/AlayaCore/Services/AuthService.cs index 81cf3fb..3700e99 100644 --- a/AlayaCore/Services/AuthService.cs +++ b/AlayaCore/Services/AuthService.cs @@ -26,12 +26,34 @@ namespace AlayaCore.Services _fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore)); } - public Task IsAuthenticatedAsync(CancellationToken cancellationToken = default) + public async Task IsAuthenticatedAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - bool authenticated = _session != null && _session.CheckIsValid(); - return Task.FromResult(authenticated); + if (_session != null && _session.CheckIsValid()) + { + return true; + } + + try + { + JELoginHandler loginHandler = await BuildHandlerAsync().ConfigureAwait(false); + + _session = await loginHandler + .AuthenticateSilently(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return _session != null && _session.CheckIsValid(); + } + catch (OperationCanceledException) + { + throw; + } + catch + { + _session = null; + return false; + } } public async Task AuthenticateAsync(CancellationToken cancellationToken = default) @@ -46,6 +68,10 @@ namespace AlayaCore.Services .AuthenticateSilently(cancellationToken: cancellationToken) .ConfigureAwait(false); } + catch (OperationCanceledException) + { + throw; + } catch { _session = await loginHandler diff --git a/AlayaCore/Services/GameInstallService.cs b/AlayaCore/Services/GameInstallService.cs index fa4b9ff..db1aaef 100644 --- a/AlayaCore/Services/GameInstallService.cs +++ b/AlayaCore/Services/GameInstallService.cs @@ -12,8 +12,6 @@ using CmlLib.Core; using CmlLib.Core.Installer.NeoForge; using CmlLib.Core.Installer.NeoForge.Installers; using CmlLib.Core.Installers; -using CmlLib.Core.Java; -using CmlLib.Core.VersionMetadata; namespace AlayaCore.Services { @@ -39,7 +37,7 @@ namespace AlayaCore.Services ManifestModel manifest, InstallEnvironment environment, CancellationToken cancellationToken = default, - EventHandler? minecraftProgess = null, + EventHandler? minecraftProgress = null, EventHandler? byteProgress = null) { if (manifest == null) @@ -77,7 +75,7 @@ namespace AlayaCore.Services await CleanOldInstallAsync(cancellationToken).ConfigureAwait(false); } - MinecraftLauncher launcher = GetOrCreateLauncher(minecraftProgess, byteProgress); + MinecraftLauncher launcher = GetOrCreateLauncher(minecraftProgress, byteProgress); await launcher .InstallAsync(manifest.MinecraftVersion, cancellationToken) @@ -122,6 +120,16 @@ namespace AlayaCore.Services return; } + bool neoForgeMismatch = + environment.NeoforgedInstalled && + !string.Equals(environment.NeoforgedVersion, manifest.NeoforgedVersion, StringComparison.OrdinalIgnoreCase); + + if (neoForgeMismatch) + { + await CleanOldInstallAsync(cancellationToken).ConfigureAwait(false); + return; + } + if (!environment.MinecraftInstalled || !string.Equals(environment.MinecraftVersion, manifest.MinecraftVersion, StringComparison.OrdinalIgnoreCase)) { @@ -135,12 +143,13 @@ namespace AlayaCore.Services MinecraftLauncher launcher = GetOrCreateLauncher(); - await DownloadAndInstallNeoForgeAsync( + await InstallNeoForgeAsync( launcher, manifest, environment, cancellationToken, - progress, byteProgress).ConfigureAwait(false); + progress, + byteProgress).ConfigureAwait(false); await launcher .InstallAsync(manifest.MinecraftVersion, cancellationToken) @@ -193,10 +202,13 @@ namespace AlayaCore.Services _gamePath = null; _minecraftLauncher = null; - await _settingsService.UpdateLaunchVersionAsync(string.Empty, cancellationToken).ConfigureAwait(false); + await _settingsService + .UpdateLaunchVersionAsync(string.Empty, cancellationToken) + .ConfigureAwait(false); } - private MinecraftLauncher GetOrCreateLauncher(EventHandler? minecraftProgess = null, + private MinecraftLauncher GetOrCreateLauncher( + EventHandler? minecraftProgress = null, EventHandler? byteProgress = null) { if (_minecraftLauncher != null) @@ -206,18 +218,21 @@ namespace AlayaCore.Services _gamePath = new AlayaPath(_fileStore); _minecraftLauncher = new MinecraftLauncher(_gamePath); - - if(byteProgress != null) + + if (byteProgress != null) + { _minecraftLauncher.ByteProgressChanged += byteProgress; - - if(minecraftProgess != null) - _minecraftLauncher.FileProgressChanged += minecraftProgess; - + } + + if (minecraftProgress != null) + { + _minecraftLauncher.FileProgressChanged += minecraftProgress; + } return _minecraftLauncher; } - private async Task DownloadAndInstallNeoForgeAsync( + private async Task InstallNeoForgeAsync( MinecraftLauncher launcher, ManifestModel manifest, InstallEnvironment environment, @@ -230,16 +245,6 @@ namespace AlayaCore.Services throw new ArgumentNullException(nameof(launcher)); } - bool neoForgeMismatch = - environment.NeoforgedInstalled && - !string.Equals(environment.NeoforgedVersion, manifest.NeoforgedVersion, StringComparison.OrdinalIgnoreCase); - - if (neoForgeMismatch) - { - await CleanOldInstallAsync(cancellationToken).ConfigureAwait(false); - return; - } - NeoForgeInstaller installer = new NeoForgeInstaller(launcher); NeoForgeInstallOptions options = new NeoForgeInstallOptions @@ -248,12 +253,16 @@ namespace AlayaCore.Services JavaPath = environment.JavaPath, SkipIfAlreadyInstalled = true }; - - if(progress != null) + + if (progress != null) + { options.FileProgress = progress; - - if(byteProgress != null) + } + + if (byteProgress != null) + { options.ByteProgress = byteProgress; + } string version = await installer .Install(manifest.MinecraftVersion, manifest.NeoforgedVersion, options) diff --git a/AlayaCore/Services/GameLaunchService.cs b/AlayaCore/Services/GameLaunchService.cs index d4998f7..8b71bd4 100644 --- a/AlayaCore/Services/GameLaunchService.cs +++ b/AlayaCore/Services/GameLaunchService.cs @@ -8,9 +8,7 @@ using AlayaCore.Installation; using AlayaCore.Models; using AlayaCore.Models.Configuration; using AlayaCore.Models.Manifests; -using AlayaCore.Utilities.Enums; using CmlLib.Core; -using CmlLib.Core.Auth; using CmlLib.Core.ProcessBuilder; namespace AlayaCore.Services @@ -22,7 +20,6 @@ namespace AlayaCore.Services private readonly ILaunchDirector _director; private readonly GameOptions _gameOptions; - private AlayaPath? _gamePath; private MinecraftLauncher? _minecraftLauncher; public GameLaunchService( @@ -69,7 +66,11 @@ namespace AlayaCore.Services cancellationToken.ThrowIfCancellationRequested(); - MLaunchOption option = await BuildLaunchOptions(manifest, environment); + MLaunchOption option = await BuildLaunchOptionsAsync( + manifest, + environment, + cancellationToken).ConfigureAwait(false); + MinecraftLauncher launcher = GetOrCreateLauncher(); var process = await launcher @@ -89,27 +90,23 @@ namespace AlayaCore.Services return _minecraftLauncher; } - _gamePath = new AlayaPath(_fileStore); - _minecraftLauncher = new MinecraftLauncher(_gamePath); - + _minecraftLauncher = new MinecraftLauncher(new AlayaPath(_fileStore)); return _minecraftLauncher; } - private string GetMinecraftPath() - { - return _fileStore.GetOrCreate(FolderLocation.Game); - } - - private async Task BuildLaunchOptions(ManifestModel manifest, InstallEnvironment environment) + private async Task BuildLaunchOptionsAsync( + ManifestModel manifest, + InstallEnvironment environment, + CancellationToken cancellationToken) { if (manifest.ServerUrl == null) { - throw new InvalidDataException("Manifest Server Url is not configured."); + throw new InvalidDataException("Manifest ServerUrl is not configured."); } return new MLaunchOption { - Session = await _authService.GetSessionAsync(), + Session = await _authService.GetSessionAsync(cancellationToken).ConfigureAwait(false), JavaPath = environment.JavaPath, MinimumRamMb = _gameOptions.MinimumRamMB, MaximumRamMb = _gameOptions.MaximumRamMB, diff --git a/AlayaCore/Services/HttpDownloadService.cs b/AlayaCore/Services/HttpDownloadService.cs index 2f68d16..7a1a65b 100644 --- a/AlayaCore/Services/HttpDownloadService.cs +++ b/AlayaCore/Services/HttpDownloadService.cs @@ -14,7 +14,7 @@ namespace AlayaCore.Services { public sealed class HttpDownloadService : IDownloadService { - private const int BUFFER_SIZE = 81920; + private const int BufferSize = 81920; private readonly IHttpClient _httpClient; @@ -45,16 +45,10 @@ namespace AlayaCore.Services throw new ArgumentException("Destination path cannot be null, empty, or whitespace.", nameof(destinationPath)); } - if (string.IsNullOrWhiteSpace(sha512Hash)) - { - throw new ArgumentException("SHA-512 hash cannot be null, empty, or whitespace.", nameof(sha512Hash)); - } - + string normalizedExpectedHash = NormalizeHash(sha512Hash); cancellationToken.ThrowIfCancellationRequested(); - string normalizedExpectedHash = NormalizeHash(sha512Hash); string fileName = Path.GetFileName(destinationPath); - if (string.IsNullOrWhiteSpace(fileName)) { throw new ArgumentException("Destination path must include a file name.", nameof(destinationPath)); @@ -69,7 +63,7 @@ namespace AlayaCore.Services progress?.Report(new DownloadProgress( fileName: fileName, destinationPath: destinationPath, - bytesDownloaded: existingLength, + bytesDownloaded: 0, totalBytes: existingLength, bytesPerSecond: null, statusMessage: "File already present and valid.")); @@ -91,7 +85,7 @@ namespace AlayaCore.Services using HttpResponseMessage response = await _httpClient.GetAsync( sourceUri, HttpCompletionOption.ResponseHeadersRead, - cancellationToken); + cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); @@ -106,17 +100,21 @@ namespace AlayaCore.Services bytesPerSecond: null, statusMessage: "Starting download...")); - await using Stream responseStream = await response.Content.ReadAsStreamAsync(); + await using Stream responseStream = await response.Content + .ReadAsStreamAsync() + .ConfigureAwait(false); + await using FileStream fileStream = new FileStream( tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None, - BUFFER_SIZE, + BufferSize, useAsync: true); + using SHA512 sha512 = SHA512.Create(); - byte[] buffer = new byte[BUFFER_SIZE]; + byte[] buffer = new byte[BufferSize]; Stopwatch stopwatch = Stopwatch.StartNew(); while (true) @@ -125,7 +123,7 @@ namespace AlayaCore.Services buffer, 0, buffer.Length, - cancellationToken); + cancellationToken).ConfigureAwait(false); if (bytesRead == 0) { @@ -136,7 +134,7 @@ namespace AlayaCore.Services buffer, 0, bytesRead, - cancellationToken); + cancellationToken).ConfigureAwait(false); sha512.TransformBlock(buffer, 0, bytesRead, null, 0); @@ -158,10 +156,9 @@ namespace AlayaCore.Services } sha512.TransformFinalBlock(Array.Empty(), 0, 0); - await fileStream.FlushAsync(cancellationToken); + await fileStream.FlushAsync(cancellationToken).ConfigureAwait(false); string actualHash = ConvertToLowerHex(sha512.Hash); - if (!string.Equals(actualHash, normalizedExpectedHash, StringComparison.OrdinalIgnoreCase)) { throw new InvalidDataException( @@ -204,18 +201,13 @@ namespace AlayaCore.Services throw new ArgumentException("File path cannot be null, empty, or whitespace.", nameof(filePath)); } - if (string.IsNullOrWhiteSpace(sha512Hash)) - { - throw new ArgumentException("SHA-512 hash cannot be null, empty, or whitespace.", nameof(sha512Hash)); - } + string normalizedExpectedHash = NormalizeHash(sha512Hash); if (!File.Exists(filePath)) { return false; } - string normalizedExpectedHash = NormalizeHash(sha512Hash); - using FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); using SHA512 sha512 = SHA512.Create(); @@ -228,6 +220,7 @@ namespace AlayaCore.Services private static void EnsureDestinationDirectoryExists(string destinationPath) { string? directoryPath = Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrWhiteSpace(directoryPath)) { Directory.CreateDirectory(directoryPath); @@ -250,7 +243,31 @@ namespace AlayaCore.Services private static string NormalizeHash(string hash) { - return hash.Trim().Replace("-", string.Empty).ToLowerInvariant(); + if (string.IsNullOrWhiteSpace(hash)) + { + throw new ArgumentException("SHA-512 hash cannot be null, empty, or whitespace.", nameof(hash)); + } + + string normalized = hash.Trim().Replace("-", string.Empty).ToLowerInvariant(); + + if (normalized.Length != 128) + { + throw new ArgumentException("SHA-512 hash must be 128 hexadecimal characters long.", nameof(hash)); + } + + foreach (char c in normalized) + { + bool isHex = + (c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f'); + + if (!isHex) + { + throw new ArgumentException("SHA-512 hash contains invalid characters.", nameof(hash)); + } + } + + return normalized; } private static string ConvertToLowerHex(byte[]? hashBytes) diff --git a/AlayaCore/Services/InstallStateService.cs b/AlayaCore/Services/InstallStateService.cs index 253dc87..621c714 100644 --- a/AlayaCore/Services/InstallStateService.cs +++ b/AlayaCore/Services/InstallStateService.cs @@ -137,9 +137,9 @@ namespace AlayaCore.Services { string executableName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "javaw.exe" - : "javaw"; + : "java"; - string runtimePath = _fileStore.GetOrCreate(FolderLocation.JavaRuntime); + string runtimePath = _fileStore.Get(FolderLocation.JavaRuntime); string fullPath = Path.Combine(runtimePath, "bin", executableName); if (!File.Exists(fullPath)) @@ -161,7 +161,10 @@ namespace AlayaCore.Services return InstalledVersionState.Empty(); } - string[] versionDirectories = Directory.GetDirectories(versionsPath); + string[] versionDirectories = Directory + .GetDirectories(versionsPath) + .OrderBy(path => path, StringComparer.OrdinalIgnoreCase) + .ToArray(); if (versionDirectories.Length == 0) { @@ -187,7 +190,10 @@ namespace AlayaCore.Services continue; } - JObject versionJson = LoadJson(versionJsonPath); + if (!TryLoadJson(versionJsonPath, out JObject? versionJson)) + { + continue; + } string? id = versionJson.Value("id"); string? inheritsFrom = versionJson.Value("inheritsFrom"); @@ -199,7 +205,7 @@ namespace AlayaCore.Services if (IsNeoForgeVersion(id, inheritsFrom)) { - neoForgeVersion = id; + neoForgeVersion ??= id; if (string.IsNullOrWhiteSpace(minecraftVersion) && !string.IsNullOrWhiteSpace(inheritsFrom)) { @@ -209,10 +215,7 @@ namespace AlayaCore.Services continue; } - if (string.IsNullOrWhiteSpace(minecraftVersion)) - { - minecraftVersion = id; - } + minecraftVersion ??= id; } return new InstalledVersionState(minecraftVersion, neoForgeVersion); @@ -220,7 +223,7 @@ namespace AlayaCore.Services private string GetVersionsPath() { - return Path.Combine(_fileStore.GetOrCreate(FolderLocation.Game), VersionsFolderName); + return Path.Combine(_fileStore.Get(FolderLocation.Game), VersionsFolderName); } private static bool IsNeoForgeVersion(string? id, string? inheritsFrom) @@ -239,21 +242,31 @@ namespace AlayaCore.Services value.Contains("neoforged", StringComparison.OrdinalIgnoreCase); } - private static JObject LoadJson(string path) + private static bool TryLoadJson(string path, out JObject? jsonObject) { - if (string.IsNullOrWhiteSpace(path)) + jsonObject = null; + + if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) { - throw new ArgumentException("Path cannot be null, empty, or whitespace.", nameof(path)); + return false; } string json = File.ReadAllText(path); if (string.IsNullOrWhiteSpace(json)) { - throw new InvalidDataException($"File '{path}' was empty."); + return false; } - return JObject.Parse(json); + try + { + jsonObject = JObject.Parse(json); + return true; + } + catch + { + return false; + } } private sealed class InstalledVersionState diff --git a/AlayaCore/Services/LauncherUpdateService.cs b/AlayaCore/Services/LauncherUpdateService.cs index 06a6a31..e736241 100644 --- a/AlayaCore/Services/LauncherUpdateService.cs +++ b/AlayaCore/Services/LauncherUpdateService.cs @@ -40,30 +40,65 @@ namespace AlayaCore.Services return true; } + if (localManifest.Version == null) + { + return true; + } + if (string.IsNullOrWhiteSpace(localManifest.Sha512Hash)) { return true; } - string remoteHash = await _manifestService - .GetRemoteLauncherManifestHashAsync(cancellationToken) + LauncherManifestModel remoteManifest = await _manifestService + .GetRemoteLauncherManifestAsync(cancellationToken) .ConfigureAwait(false); - if (string.IsNullOrWhiteSpace(remoteHash)) + if (remoteManifest == null) + { + throw new InvalidOperationException("Remote launcher manifest could not be loaded."); + } + + if (remoteManifest.Version == null) + { + throw new InvalidOperationException("Remote launcher manifest returned an invalid version."); + } + + if (string.IsNullOrWhiteSpace(remoteManifest.Sha512Hash)) { throw new InvalidOperationException("Remote launcher manifest returned an invalid SHA-512 hash."); } - return !string.Equals( + bool versionMismatch = localManifest.Version != remoteManifest.Version; + bool hashMismatch = !string.Equals( localManifest.Sha512Hash.Trim(), - remoteHash.Trim(), + remoteManifest.Sha512Hash.Trim(), StringComparison.OrdinalIgnoreCase); + + return versionMismatch || hashMismatch; } - public Task LaunchUpdaterAsync(LauncherManifestModel newManifest, CancellationToken cancellationToken = default) + public Task LaunchUpdaterAsync( + LauncherManifestModel newManifest, + CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); + if (newManifest == null) + { + throw new ArgumentNullException(nameof(newManifest)); + } + + if (newManifest.DownloadUri == null) + { + throw new InvalidOperationException("Launcher manifest does not contain a download URI."); + } + + if (!newManifest.DownloadUri.IsAbsoluteUri) + { + throw new InvalidOperationException("Launcher download URI must be absolute."); + } + string updaterPath = _options.AlayaUpdaterPath; if (string.IsNullOrWhiteSpace(updaterPath)) @@ -84,11 +119,13 @@ namespace AlayaCore.Services string workingDirectory = Path.GetDirectoryName(updaterPath) ?? throw new InvalidOperationException("Updater working directory could not be resolved."); + string arguments = $"-url \"{newManifest.DownloadUri.AbsoluteUri}\""; + var startInfo = new ProcessStartInfo { FileName = updaterPath, WorkingDirectory = workingDirectory, - Arguments = $@"-url {newManifest.DownloadUri}", + Arguments = arguments, UseShellExecute = false, CreateNoWindow = true }; @@ -102,7 +139,7 @@ namespace AlayaCore.Services throw new InvalidOperationException("Failed to start updater process."); } } - catch (Exception ex) when (!(ex is OperationCanceledException)) + catch (Exception ex) when (ex is not OperationCanceledException) { throw new InvalidOperationException("Failed to launch updater process.", ex); } diff --git a/AlayaCore/Services/ManifestService.cs b/AlayaCore/Services/ManifestService.cs index 95287ab..3f249f3 100644 --- a/AlayaCore/Services/ManifestService.cs +++ b/AlayaCore/Services/ManifestService.cs @@ -17,9 +17,9 @@ namespace AlayaCore.Services { public sealed class ManifestService : IManifestService { - private const string CORE_MANIFEST_FILE_NAME = "CoreManifest.json"; - private const string LAUNCHER_MANIFEST_FILE_NAME = "LauncherManifest.json"; - private const string INSTALLED_MODS_MANIFEST_FILE_NAME = "InstalledModsManifest.json"; + private const string CoreManifestFileName = "CoreManifest.json"; + private const string LauncherManifestFileName = "LauncherManifest.json"; + private const string InstalledModsManifestFileName = "InstalledModsManifest.json"; private readonly IDownloadService _downloadService; private readonly IHttpClient _httpClient; @@ -50,7 +50,8 @@ namespace AlayaCore.Services cancellationToken); } - public async Task GetInstalledModsManifestAsync(CancellationToken cancellationToken = default) + public async Task GetInstalledModsManifestAsync( + CancellationToken cancellationToken = default) { string path = GetInstalledModsManifestPath(); @@ -66,11 +67,12 @@ namespace AlayaCore.Services return InstalledModsManifestModel.Empty(); } - InstalledModsManifestModel? manifest = DeserializeAndMapManifest( - json, - path, - static dto => dto.ToModel(), - swallowDeserializationErrors: true); + InstalledModsManifestModel? manifest = + DeserializeAndMapManifest( + json, + path, + static dto => dto.ToModel(), + swallowDeserializationErrors: true); return manifest ?? InstalledModsManifestModel.Empty(); } @@ -105,28 +107,10 @@ namespace AlayaCore.Services public async Task GetRemoteCoreManifestVersionAsync(CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); - - using HttpResponseMessage response = await _httpClient.GetAsync( + ManifestModel remoteManifest = await GetRemoteManifestAsync( _options.CoreManifestUri, - HttpCompletionOption.ResponseContentRead, - cancellationToken).ConfigureAwait(false); - - response.EnsureSuccessStatusCode(); - - string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - if (string.IsNullOrWhiteSpace(json)) - { - throw new InvalidDataException( - $"Remote core manifest response from '{_options.CoreManifestUri}' was empty."); - } - - ManifestModel remoteManifest = DeserializeAndMapManifest( - json, - _options.CoreManifestUri.ToString(), static dto => dto.ToModel(), - swallowDeserializationErrors: false)!; + cancellationToken).ConfigureAwait(false); if (remoteManifest.AlayaVersion == null) { @@ -137,30 +121,13 @@ namespace AlayaCore.Services return remoteManifest.AlayaVersion; } - public async Task GetRemoteLauncherManifestHashAsync(CancellationToken cancellationToken = default) + public async Task GetRemoteLauncherManifestAsync( + CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); - - using HttpResponseMessage response = await _httpClient.GetAsync( + LauncherManifestModel remoteManifest = await GetRemoteManifestAsync( _options.LauncherManifestUri, - HttpCompletionOption.ResponseContentRead, - cancellationToken).ConfigureAwait(false); - - response.EnsureSuccessStatusCode(); - - string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - if (string.IsNullOrWhiteSpace(json)) - { - throw new InvalidDataException( - $"Remote launcher manifest response from '{_options.LauncherManifestUri}' was empty."); - } - - LauncherManifestModel remoteManifest = DeserializeAndMapManifest( - json, - _options.LauncherManifestUri.ToString(), static dto => dto.ToModel(), - swallowDeserializationErrors: false)!; + cancellationToken).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(remoteManifest.Sha512Hash)) { @@ -168,22 +135,28 @@ namespace AlayaCore.Services $"Remote launcher manifest from '{_options.LauncherManifestUri}' does not contain a valid SHA-512 hash."); } - return remoteManifest.Sha512Hash.Trim(); + if (remoteManifest.DownloadUri == null || !remoteManifest.DownloadUri.IsAbsoluteUri) + { + throw new InvalidDataException( + $"Remote launcher manifest from '{_options.LauncherManifestUri}' does not contain a valid download URI."); + } + + return remoteManifest; } public string GetLauncherManifestPath() { - return Path.Combine(_fileStore.GetOrCreate(FolderLocation.Manifests), LAUNCHER_MANIFEST_FILE_NAME); + return Path.Combine(_fileStore.Get(FolderLocation.Manifests), LauncherManifestFileName); } public string GetCoreManifestPath() { - return Path.Combine(_fileStore.GetOrCreate(FolderLocation.Manifests), CORE_MANIFEST_FILE_NAME); + return Path.Combine(_fileStore.Get(FolderLocation.Manifests), CoreManifestFileName); } public string GetInstalledModsManifestPath() { - return Path.Combine(_fileStore.GetOrCreate(FolderLocation.Manifests), INSTALLED_MODS_MANIFEST_FILE_NAME); + return Path.Combine(_fileStore.Get(FolderLocation.Manifests), InstalledModsManifestFileName); } private async Task LoadLocalManifestAsync( @@ -283,6 +256,51 @@ namespace AlayaCore.Services swallowDeserializationErrors: false)!; } + private async Task GetRemoteManifestAsync( + Uri manifestUri, + Func map, + CancellationToken cancellationToken) + where TDto : class + { + if (manifestUri == null) + { + throw new ArgumentNullException(nameof(manifestUri)); + } + + if (!manifestUri.IsAbsoluteUri) + { + throw new ArgumentException("Manifest URI must be absolute.", nameof(manifestUri)); + } + + if (map == null) + { + throw new ArgumentNullException(nameof(map)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using HttpResponseMessage response = await _httpClient.GetAsync( + manifestUri, + HttpCompletionOption.ResponseContentRead, + cancellationToken).ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(json)) + { + throw new InvalidDataException( + $"Remote manifest response from '{manifestUri}' was empty."); + } + + return DeserializeAndMapManifest( + json, + manifestUri.ToString(), + map, + swallowDeserializationErrors: false)!; + } + private static TModel? DeserializeAndMapManifest( string json, string sourceName, @@ -342,7 +360,7 @@ namespace AlayaCore.Services { return map(dto); } - catch (Exception ex) when (!(ex is OperationCanceledException)) + catch (Exception ex) when (ex is not OperationCanceledException) { if (swallowDeserializationErrors) { diff --git a/AlayaCore/States/LaunchPlan.cs b/AlayaCore/States/LaunchPlan.cs index 2062bef..cbbe5d7 100644 --- a/AlayaCore/States/LaunchPlan.cs +++ b/AlayaCore/States/LaunchPlan.cs @@ -5,7 +5,6 @@ namespace AlayaCore.States Ready, LauncherNeedsUpdate, NeedAuthenticating, - InstallJava, InstallMinecraft, InstallNeoforge, SyncMods @@ -14,7 +13,6 @@ namespace AlayaCore.States public sealed class LaunchPlan { public bool LauncherNeedsUpdate { get; } - public bool JavaNeedsInstallOrUpdate { get; } public bool MinecraftNeedsInstallOrUpdate { get; } public bool NeoforgeNeedsInstallOrUpdate { get; } public bool ModsNeedSync { get; } @@ -26,7 +24,6 @@ namespace AlayaCore.States public bool NeedsUpdating => LauncherNeedsUpdate || - JavaNeedsInstallOrUpdate || MinecraftNeedsInstallOrUpdate || NeoforgeNeedsInstallOrUpdate || ModsNeedSync; @@ -36,14 +33,12 @@ namespace AlayaCore.States public LaunchPlan( bool launcherNeedsUpdate, - bool javaNeedsInstallOrUpdate, bool minecraftNeedsInstallOrUpdate, bool neoforgeNeedsInstallOrUpdate, bool modsNeedSync, bool needAuthenticating) { LauncherNeedsUpdate = launcherNeedsUpdate; - JavaNeedsInstallOrUpdate = javaNeedsInstallOrUpdate; MinecraftNeedsInstallOrUpdate = minecraftNeedsInstallOrUpdate; NeoforgeNeedsInstallOrUpdate = neoforgeNeedsInstallOrUpdate; ModsNeedSync = modsNeedSync; @@ -58,9 +53,6 @@ namespace AlayaCore.States if (NeedAuthenticating) return LaunchState.NeedAuthenticating; - if (JavaNeedsInstallOrUpdate) - return LaunchState.InstallJava; - if (MinecraftNeedsInstallOrUpdate) return LaunchState.InstallMinecraft; @@ -77,7 +69,6 @@ namespace AlayaCore.States { return new LaunchPlan( launcherNeedsUpdate: false, - javaNeedsInstallOrUpdate: false, minecraftNeedsInstallOrUpdate: false, neoforgeNeedsInstallOrUpdate: false, modsNeedSync: false, diff --git a/AlayaCore/Utilities/Extensions/MappingExtensions.cs b/AlayaCore/Utilities/Extensions/MappingExtensions.cs index c9dc425..6d86789 100644 --- a/AlayaCore/Utilities/Extensions/MappingExtensions.cs +++ b/AlayaCore/Utilities/Extensions/MappingExtensions.cs @@ -47,8 +47,6 @@ namespace AlayaCore.Utilities.Extensions return new ManifestModel( dto.AlayaVersion, - dto.RequiredJavaVersion, - dto.RequiredJavaUrl, dto.MinecraftVersion, dto.NeoforgedVersion, dto.ServerUrl, @@ -80,8 +78,6 @@ namespace AlayaCore.Utilities.Extensions return new ManifestDto { AlayaVersion = model.AlayaVersion, - RequiredJavaVersion = model.RequiredJavaVersion, - RequiredJavaUrl = model.RequiredJavaBaseUrl, MinecraftVersion = model.MinecraftVersion, NeoforgedVersion = model.NeoforgedVersion, Files = model.Files.Select(file => file.ToDto()).ToList() diff --git a/AlayaCore/Utilities/Stores/LocalFileStore.cs b/AlayaCore/Utilities/Stores/LocalFileStore.cs index ef2daab..daa245a 100644 --- a/AlayaCore/Utilities/Stores/LocalFileStore.cs +++ b/AlayaCore/Utilities/Stores/LocalFileStore.cs @@ -10,7 +10,7 @@ namespace AlayaCore.Utilities.Stores { private static readonly string BaseDirectoryPath = AppContext.BaseDirectory; private static readonly string JavaDirectoryPath = Path.Combine(BaseDirectoryPath, "Java"); - private static readonly string JavaRuntimeDirectoryPath = Path.Combine(JavaDirectoryPath, "java-runtime-epsilon"); + private static readonly string JavaRuntimeDirectoryPath = Path.Combine(JavaDirectoryPath, "runtime"); private static readonly string GameDirectoryPath = Path.Combine(BaseDirectoryPath, "Game"); private static readonly string ModsDirectoryPath = Path.Combine(GameDirectoryPath, "mods"); private static readonly string ResourcePacksDirectoryPath = Path.Combine(GameDirectoryPath, "resourcepacks"); @@ -36,6 +36,26 @@ namespace AlayaCore.Utilities.Stores { FolderLocation.Data, DataDirectoryPath} }; + public LocalFileStore() + { + CreateDirectories(); + } + + public void CreateDirectories() + { + Directory.CreateDirectory(BaseDirectoryPath); + Directory.CreateDirectory(JavaDirectoryPath); + Directory.CreateDirectory(JavaRuntimeDirectoryPath); + Directory.CreateDirectory(GameDirectoryPath); + Directory.CreateDirectory(ModsDirectoryPath); + Directory.CreateDirectory(ResourcePacksDirectoryPath); + Directory.CreateDirectory(ConfigDirectoryPath); + Directory.CreateDirectory(DownloadsDirectoryPath); + Directory.CreateDirectory(ManifestsDirectoryPath); + Directory.CreateDirectory(PluginsDirectoryPath); + Directory.CreateDirectory(DataDirectoryPath); + } + public string Get(FolderLocation location) { if (!Folders.TryGetValue(location, out string? path))