ILogger support added
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AByteProgress_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003F99_003Fc83c3b28_003FByteProgress_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADefaultFileExtractors_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003Fe8_003F52aaf39a_003FDefaultFileExtractors_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADefaultFileExtractors_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003Fe8_003F52aaf39a_003FDefaultFileExtractors_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003F5c_003Ff0b24cad_003FExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003F5c_003Ff0b24cad_003FExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIGameInstaller_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003Fdf_003F3b38ca47_003FIGameInstaller_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIGameInstaller_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003Fdf_003F3b38ca47_003FIGameInstaller_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AInstallerEventType_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003F4e_003F4bf1d82f_003FInstallerEventType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AInstallerProgressChangedEventArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003Ffb_003F6d837772_003FInstallerProgressChangedEventArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIVersionMetadata_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003Ff2_003Fc2330846_003FIVersionMetadata_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIVersionMetadata_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003Ff2_003Fc2330846_003FIVersionMetadata_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIXboxGameAccountManager_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F101e700861b6410da498d4e79271a86112600_003Fc4_003F2054a44d_003FIXboxGameAccountManager_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIXboxGameAccountManager_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F101e700861b6410da498d4e79271a86112600_003Fc4_003F2054a44d_003FIXboxGameAccountManager_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJELoginHandlerBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2dce88ad0d604d86b0d410923cb59f4bb200_003Fb4_003F534fcf72_003FJELoginHandlerBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJELoginHandlerBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2dce88ad0d604d86b0d410923cb59f4bb200_003Fb4_003F534fcf72_003FJELoginHandlerBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AlayaCore.Models;
|
using AlayaCore.Models.Progress;
|
||||||
|
using AlayaCore.Models.Results;
|
||||||
using AlayaCore.States;
|
using AlayaCore.States;
|
||||||
using CmlLib.Core;
|
|
||||||
using CmlLib.Core.Installers;
|
|
||||||
|
|
||||||
namespace AlayaCore.Abstractions.Interfaces
|
namespace AlayaCore.Abstractions.Interfaces
|
||||||
{
|
{
|
||||||
public interface ILaunchDirector
|
public interface ILaunchDirector
|
||||||
{
|
{
|
||||||
Task<LaunchPlan> EvaluateAsync(CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
Task InstallOrUpdateAsync(CancellationToken cancellationToken = default,
|
|
||||||
EventHandler<InstallerProgressChangedEventArgs>? minecraftProgess = null,
|
|
||||||
EventHandler<ByteProgress>? byteProgress = null,
|
|
||||||
IProgress<InstallerProgressChangedEventArgs>? neoForgeProgress = null,
|
|
||||||
IProgress<ByteProgress>? neoForgeByteProgress = null);
|
|
||||||
|
|
||||||
Task LaunchAsync(CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
bool CanRun { get; }
|
bool CanRun { get; }
|
||||||
bool NeedsUpdating { get; }
|
bool NeedsUpdating { get; }
|
||||||
|
|
||||||
|
bool IsUpdatingLauncher { get; }
|
||||||
|
|
||||||
LaunchPlan? CurrentPlan { get; }
|
LaunchPlan? CurrentPlan { get; }
|
||||||
|
|
||||||
|
Task<LaunchPlan> EvaluateAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<InstallOrUpdateResult> InstallOrUpdateAsync(
|
||||||
|
CancellationToken cancellationToken = default,
|
||||||
|
IProgress<LauncherProgress>? progress = null);
|
||||||
|
|
||||||
|
Task LaunchAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,8 @@ namespace AlayaCore.Abstractions.Interfaces.Services
|
|||||||
{
|
{
|
||||||
public interface IManifestService
|
public interface IManifestService
|
||||||
{
|
{
|
||||||
Task<ManifestModel> GetCoreManifestAsync(CancellationToken cancellationToken = default);
|
Task<ManifestModel> GetAlayaManifestAsync(CancellationToken cancellationToken = default);
|
||||||
Task<ManifestModel?> GetLocalCoreManifestAsync(CancellationToken cancellationToken = default);
|
Task<ManifestModel?> GetLocalAlayaManifestAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task<InstalledModsManifestModel> GetInstalledModsManifestAsync(CancellationToken cancellationToken = default);
|
Task<InstalledModsManifestModel> GetInstalledModsManifestAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AlayaCore.Installation;
|
using AlayaCore.Installation;
|
||||||
using AlayaCore.Models.Manifests;
|
using AlayaCore.Models.Manifests;
|
||||||
|
using AlayaCore.Models.Progress;
|
||||||
|
|
||||||
namespace AlayaCore.Abstractions.Interfaces.Services
|
namespace AlayaCore.Abstractions.Interfaces.Services
|
||||||
{
|
{
|
||||||
public interface IModService
|
public interface IModService
|
||||||
{
|
{
|
||||||
Task ProcessModsAsync(ManifestModel manifest, InstallEnvironment environment, CancellationToken cancellationToken = default);
|
Task ProcessModsAsync(
|
||||||
|
ManifestModel manifest,
|
||||||
|
InstallEnvironment environment,
|
||||||
|
IProgress<DownloadProgress>? progress = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,12 @@ namespace AlayaCore.Abstractions.Interfaces.Services
|
|||||||
GameOptions GameOptions { get; }
|
GameOptions GameOptions { get; }
|
||||||
|
|
||||||
Task SetForceReinstallAsync(bool value, CancellationToken cancellationToken = default);
|
Task SetForceReinstallAsync(bool value, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task UpdateLaunchVersionAsync(string newVersion, CancellationToken cancellationToken = default);
|
Task UpdateLaunchVersionAsync(string newVersion, CancellationToken cancellationToken = default);
|
||||||
|
Task SetMinimumRamMbAsync(int minimumRamMb, CancellationToken cancellationToken = default);
|
||||||
|
Task SetMaximumRamMbAsync(int maximumRamMb, CancellationToken cancellationToken = default);
|
||||||
|
Task SetResolutionAsync(int screenWidth, int screenHeight, CancellationToken cancellationToken = default);
|
||||||
|
Task SetFullscreenAsync(bool fullscreen, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task SaveLauncherOptionsAsync(CancellationToken cancellationToken = default);
|
Task SaveLauncherOptionsAsync(CancellationToken cancellationToken = default);
|
||||||
Task LoadLauncherOptionsAsync(CancellationToken cancellationToken = default);
|
Task LoadLauncherOptionsAsync(CancellationToken cancellationToken = default);
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ using AlayaCore.Abstractions.Interfaces.Services;
|
|||||||
using AlayaCore.Installation;
|
using AlayaCore.Installation;
|
||||||
using AlayaCore.Models.Configuration;
|
using AlayaCore.Models.Configuration;
|
||||||
using AlayaCore.Models.Manifests;
|
using AlayaCore.Models.Manifests;
|
||||||
|
using AlayaCore.Models.Progress;
|
||||||
|
using AlayaCore.Models.Results;
|
||||||
using AlayaCore.States;
|
using AlayaCore.States;
|
||||||
using AlayaCore.Utilities.Enums;
|
using AlayaCore.Utilities.Enums;
|
||||||
using CmlLib.Core;
|
using CmlLib.Core;
|
||||||
using CmlLib.Core.Installers;
|
using CmlLib.Core.Installers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace AlayaCore
|
namespace AlayaCore
|
||||||
{
|
{
|
||||||
@@ -26,11 +29,14 @@ namespace AlayaCore
|
|||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly IAuthService _authService;
|
private readonly IAuthService _authService;
|
||||||
private readonly LauncherOptions _options;
|
private readonly LauncherOptions _options;
|
||||||
|
private readonly ILogger<LaunchDirector> _logger;
|
||||||
|
|
||||||
public bool CanRun { get; private set; }
|
public bool CanRun { get; private set; }
|
||||||
public bool NeedsUpdating { get; private set; }
|
public bool NeedsUpdating { get; private set; }
|
||||||
public LaunchPlan? CurrentPlan { get; private set; }
|
public LaunchPlan? CurrentPlan { get; private set; }
|
||||||
|
|
||||||
|
public bool IsUpdatingLauncher { get; private set; }
|
||||||
|
|
||||||
public LaunchDirector(
|
public LaunchDirector(
|
||||||
IManifestService manifestService,
|
IManifestService manifestService,
|
||||||
IUpdateService updateService,
|
IUpdateService updateService,
|
||||||
@@ -40,7 +46,8 @@ namespace AlayaCore
|
|||||||
IGameInstallService gameInstallService,
|
IGameInstallService gameInstallService,
|
||||||
ISettingsService settingsService,
|
ISettingsService settingsService,
|
||||||
IAuthService authService,
|
IAuthService authService,
|
||||||
LauncherOptions options)
|
LauncherOptions options,
|
||||||
|
ILogger<LaunchDirector> logger)
|
||||||
{
|
{
|
||||||
_manifestService = manifestService ?? throw new ArgumentNullException(nameof(manifestService));
|
_manifestService = manifestService ?? throw new ArgumentNullException(nameof(manifestService));
|
||||||
_updateService = updateService ?? throw new ArgumentNullException(nameof(updateService));
|
_updateService = updateService ?? throw new ArgumentNullException(nameof(updateService));
|
||||||
@@ -51,18 +58,23 @@ namespace AlayaCore
|
|||||||
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
|
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
|
||||||
_authService = authService ?? throw new ArgumentNullException(nameof(authService));
|
_authService = authService ?? throw new ArgumentNullException(nameof(authService));
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LaunchPlan> EvaluateAsync(CancellationToken cancellationToken = default)
|
public async Task<LaunchPlan> EvaluateAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogInformation("Evaluating launcher state.");
|
||||||
|
|
||||||
bool launcherNeedsUpdate = await _updateService
|
bool launcherNeedsUpdate = await _updateService
|
||||||
.DoesLauncherNeedUpdating(cancellationToken)
|
.DoesLauncherNeedUpdating(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (launcherNeedsUpdate)
|
if (launcherNeedsUpdate)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Launcher update is required.");
|
||||||
|
|
||||||
LaunchPlan launcherUpdatePlan = new LaunchPlan(
|
LaunchPlan launcherUpdatePlan = new LaunchPlan(
|
||||||
launcherNeedsUpdate: true,
|
launcherNeedsUpdate: true,
|
||||||
minecraftNeedsInstallOrUpdate: false,
|
minecraftNeedsInstallOrUpdate: false,
|
||||||
@@ -106,25 +118,38 @@ namespace AlayaCore
|
|||||||
modsNeedSync: modsNeedSync,
|
modsNeedSync: modsNeedSync,
|
||||||
needAuthenticating: needAuthenticating);
|
needAuthenticating: needAuthenticating);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Launcher evaluation completed. State: {State}, NeedAuthenticating: {NeedAuthenticating}, ForceReinstall: {ForceReinstall}, MinecraftNeedsInstallOrUpdate: {MinecraftNeedsInstallOrUpdate}, NeoForgeNeedsInstallOrUpdate: {NeoForgeNeedsInstallOrUpdate}, ModsNeedSync: {ModsNeedSync}",
|
||||||
|
plan.State,
|
||||||
|
needAuthenticating,
|
||||||
|
_options.ForceReinstall,
|
||||||
|
minecraftNeedsInstallOrUpdate,
|
||||||
|
neoforgeNeedsInstallOrUpdate,
|
||||||
|
modsNeedSync);
|
||||||
|
|
||||||
ApplyPlan(plan);
|
ApplyPlan(plan);
|
||||||
return plan;
|
return plan;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InstallOrUpdateAsync(
|
public async Task<InstallOrUpdateResult> InstallOrUpdateAsync(
|
||||||
CancellationToken cancellationToken = default,
|
CancellationToken cancellationToken = default,
|
||||||
EventHandler<InstallerProgressChangedEventArgs>? minecraftProgress = null,
|
IProgress<LauncherProgress>? progress = null)
|
||||||
EventHandler<ByteProgress>? byteProgress = null,
|
|
||||||
IProgress<InstallerProgressChangedEventArgs>? neoForgeProgress = null,
|
|
||||||
IProgress<ByteProgress>? neoForgeByteProgress = null)
|
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
IsUpdatingLauncher = false;
|
||||||
|
|
||||||
|
_logger.LogInformation("Starting install or update workflow.");
|
||||||
|
ReportProgress(progress, LaunchState.Checking, "Checking launcher state...");
|
||||||
|
|
||||||
LaunchPlan plan = await EvaluateAsync(cancellationToken).ConfigureAwait(false);
|
LaunchPlan plan = await EvaluateAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
while (!plan.CanRun)
|
while (!plan.CanRun)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogInformation("Processing launch state {LaunchState}.", plan.State);
|
||||||
|
|
||||||
ManifestModel manifest;
|
ManifestModel manifest;
|
||||||
InstallEnvironment environment;
|
InstallEnvironment environment;
|
||||||
|
|
||||||
@@ -132,6 +157,12 @@ namespace AlayaCore
|
|||||||
{
|
{
|
||||||
case LaunchState.LauncherNeedsUpdate:
|
case LaunchState.LauncherNeedsUpdate:
|
||||||
{
|
{
|
||||||
|
IsUpdatingLauncher = true;
|
||||||
|
|
||||||
|
_logger.LogWarning("Launcher updater handoff is beginning.");
|
||||||
|
|
||||||
|
ReportProgress(progress, LaunchState.LauncherNeedsUpdate, "Launching updater...");
|
||||||
|
|
||||||
LauncherManifestModel launcherManifest = await _manifestService
|
LauncherManifestModel launcherManifest = await _manifestService
|
||||||
.GetLauncherManifestAsync(cancellationToken)
|
.GetLauncherManifestAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
@@ -141,77 +172,115 @@ namespace AlayaCore
|
|||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
ApplyPlan(plan);
|
ApplyPlan(plan);
|
||||||
return;
|
|
||||||
|
_logger.LogInformation("Updater launched successfully. Returning UpdaterLaunched result.");
|
||||||
|
return InstallOrUpdateResult.UpdaterLaunched;
|
||||||
}
|
}
|
||||||
|
|
||||||
case LaunchState.NeedAuthenticating:
|
case LaunchState.NeedAuthenticating:
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Authentication is required.");
|
||||||
|
|
||||||
|
ReportProgress(progress, LaunchState.NeedAuthenticating, "Signing in...");
|
||||||
|
|
||||||
await _authService
|
await _authService
|
||||||
.AuthenticateAsync(cancellationToken)
|
.AuthenticateAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Authentication completed successfully.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case LaunchState.InstallMinecraft:
|
case LaunchState.InstallMinecraft:
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Minecraft installation or repair is required.");
|
||||||
|
|
||||||
|
ReportProgress(progress, LaunchState.InstallMinecraft, "Preparing Minecraft installation...");
|
||||||
|
|
||||||
manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
|
manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
|
||||||
environment = await _installStateService
|
environment = await _installStateService
|
||||||
.GetCurrentEnvironmentAsync(cancellationToken)
|
.GetCurrentEnvironmentAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
EventHandler<InstallerProgressChangedEventArgs>? minecraftFileProgress =
|
||||||
|
CreateMinecraftFileProgressHandler(progress);
|
||||||
|
|
||||||
|
EventHandler<ByteProgress>? minecraftByteProgress =
|
||||||
|
CreateMinecraftByteProgressHandler(progress);
|
||||||
|
|
||||||
await _gameInstallService
|
await _gameInstallService
|
||||||
.EnsureMinecraftInstalledAsync(
|
.EnsureMinecraftInstalledAsync(
|
||||||
manifest,
|
manifest,
|
||||||
environment,
|
environment,
|
||||||
cancellationToken,
|
cancellationToken,
|
||||||
minecraftProgress,
|
minecraftFileProgress,
|
||||||
byteProgress)
|
minecraftByteProgress)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Minecraft installation or repair step completed.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case LaunchState.InstallNeoforge:
|
case LaunchState.InstallNeoforge:
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("NeoForge installation or repair is required.");
|
||||||
|
|
||||||
|
ReportProgress(progress, LaunchState.InstallNeoforge, "Preparing NeoForge installation...");
|
||||||
|
|
||||||
manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
|
manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
|
||||||
environment = await _installStateService
|
environment = await _installStateService
|
||||||
.GetCurrentEnvironmentAsync(cancellationToken)
|
.GetCurrentEnvironmentAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
IProgress<InstallerProgressChangedEventArgs>? neoForgeFileProgress =
|
||||||
|
CreateNeoForgeFileProgress(progress);
|
||||||
|
|
||||||
|
IProgress<ByteProgress>? neoForgeByteProgress =
|
||||||
|
CreateNeoForgeByteProgress(progress);
|
||||||
|
|
||||||
await _gameInstallService
|
await _gameInstallService
|
||||||
.EnsureNeoForgeInstalledAsync(
|
.EnsureNeoForgeInstalledAsync(
|
||||||
manifest,
|
manifest,
|
||||||
environment,
|
environment,
|
||||||
cancellationToken,
|
cancellationToken,
|
||||||
neoForgeProgress,
|
neoForgeFileProgress,
|
||||||
neoForgeByteProgress)
|
neoForgeByteProgress)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("NeoForge installation or repair step completed.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case LaunchState.SyncMods:
|
case LaunchState.SyncMods:
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Mod synchronization is required.");
|
||||||
|
|
||||||
|
ReportProgress(progress, LaunchState.SyncMods, "Checking mod files...");
|
||||||
|
|
||||||
manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
|
manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
|
||||||
environment = await _installStateService
|
environment = await _installStateService
|
||||||
.GetCurrentEnvironmentAsync(cancellationToken)
|
.GetCurrentEnvironmentAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
IProgress<DownloadProgress>? modProgress = CreateModProgress(progress);
|
||||||
|
|
||||||
await _modService
|
await _modService
|
||||||
.ProcessModsAsync(manifest, environment, cancellationToken)
|
.ProcessModsAsync(manifest, environment, modProgress, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Mod synchronization step completed.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case LaunchState.Ready:
|
case LaunchState.Ready:
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Launch state is Ready inside install loop.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Unsupported launch state encountered: {LaunchState}.", plan.State);
|
||||||
throw new InvalidOperationException($"Unsupported launch state '{plan.State}'.");
|
throw new InvalidOperationException($"Unsupported launch state '{plan.State}'.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,6 +290,8 @@ namespace AlayaCore
|
|||||||
|
|
||||||
if (_options.ForceReinstall)
|
if (_options.ForceReinstall)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Force reinstall flag was set. Resetting it after successful install/update workflow.");
|
||||||
|
|
||||||
await _settingsService
|
await _settingsService
|
||||||
.SetForceReinstallAsync(false, cancellationToken)
|
.SetForceReinstallAsync(false, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
@@ -230,21 +301,34 @@ namespace AlayaCore
|
|||||||
|
|
||||||
if (!plan.CanRun)
|
if (!plan.CanRun)
|
||||||
{
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Install or update workflow completed, but launcher is still not runnable. Final state: {LaunchState}.",
|
||||||
|
plan.State);
|
||||||
|
|
||||||
throw new InvalidOperationException("Install/update completed, but the launcher is still not in a runnable state.");
|
throw new InvalidOperationException("Install/update completed, but the launcher is still not in a runnable state.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ReportProgress(progress, LaunchState.Ready, "Launcher is ready.");
|
||||||
|
|
||||||
|
_logger.LogInformation("Install or update workflow completed successfully. Launcher is ready.");
|
||||||
|
return InstallOrUpdateResult.Ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LaunchAsync(CancellationToken cancellationToken = default)
|
public async Task LaunchAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogInformation("Launch requested.");
|
||||||
|
|
||||||
if (CurrentPlan == null)
|
if (CurrentPlan == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("No current launch plan was available. Evaluating launcher state before launch.");
|
||||||
await EvaluateAsync(cancellationToken).ConfigureAwait(false);
|
await EvaluateAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!CanRun)
|
if (!CanRun)
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Launch was requested while the launcher was not in a runnable state.");
|
||||||
throw new InvalidOperationException("Launcher cannot run because installation or updates are still required.");
|
throw new InvalidOperationException("Launcher cannot run because installation or updates are still required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,33 +338,56 @@ namespace AlayaCore
|
|||||||
|
|
||||||
ManifestModel manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
|
ManifestModel manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Starting game launch. MinecraftVersion: {MinecraftVersion}, NeoForgeVersion: {NeoForgeVersion}",
|
||||||
|
environment.MinecraftVersion,
|
||||||
|
environment.NeoforgedVersion);
|
||||||
|
|
||||||
await _gameLaunchService
|
await _gameLaunchService
|
||||||
.LaunchAsync(manifest, environment, cancellationToken)
|
.LaunchAsync(manifest, environment, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Game launch call completed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ManifestModel> EnsureCurrentManifestAsync(CancellationToken cancellationToken)
|
private async Task<ManifestModel> EnsureCurrentManifestAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Ensuring current Alaya manifest is available.");
|
||||||
|
|
||||||
ManifestModel? localManifest = await _manifestService
|
ManifestModel? localManifest = await _manifestService
|
||||||
.GetLocalCoreManifestAsync(cancellationToken)
|
.GetLocalAlayaManifestAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
Version remoteVersion = await _manifestService
|
Version remoteVersion = await _manifestService
|
||||||
.GetRemoteCoreManifestVersionAsync(cancellationToken)
|
.GetRemoteCoreManifestVersionAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (localManifest == null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No local Alaya manifest was found. Downloading remote manifest.");
|
||||||
|
}
|
||||||
|
else if (localManifest.AlayaVersion != remoteVersion)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Local Alaya manifest version {LocalVersion} differs from remote version {RemoteVersion}. Downloading updated manifest.",
|
||||||
|
localManifest.AlayaVersion,
|
||||||
|
remoteVersion);
|
||||||
|
}
|
||||||
|
|
||||||
if (localManifest == null || localManifest.AlayaVersion != remoteVersion)
|
if (localManifest == null || localManifest.AlayaVersion != remoteVersion)
|
||||||
{
|
{
|
||||||
localManifest = await _manifestService
|
localManifest = await _manifestService
|
||||||
.GetCoreManifestAsync(cancellationToken)
|
.GetAlayaManifestAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localManifest == null)
|
if (localManifest == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Local Alaya manifest was still unavailable after refresh.");
|
||||||
throw new FileNotFoundException("Local core manifest was not found after refresh.");
|
throw new FileNotFoundException("Local core manifest was not found after refresh.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Current Alaya manifest is available. Version: {AlayaVersion}", localManifest.AlayaVersion);
|
||||||
return localManifest;
|
return localManifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +443,129 @@ namespace AlayaCore
|
|||||||
CurrentPlan = plan ?? throw new ArgumentNullException(nameof(plan));
|
CurrentPlan = plan ?? throw new ArgumentNullException(nameof(plan));
|
||||||
NeedsUpdating = plan.NeedsUpdating;
|
NeedsUpdating = plan.NeedsUpdating;
|
||||||
CanRun = plan.CanRun;
|
CanRun = plan.CanRun;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Applied launch plan. State: {LaunchState}, CanRun: {CanRun}, NeedsUpdating: {NeedsUpdating}, IsUpdatingLauncher: {IsUpdatingLauncher}",
|
||||||
|
plan.State,
|
||||||
|
CanRun,
|
||||||
|
NeedsUpdating,
|
||||||
|
IsUpdatingLauncher);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReportProgress(
|
||||||
|
IProgress<LauncherProgress>? progress,
|
||||||
|
LaunchState phase,
|
||||||
|
string statusMessage,
|
||||||
|
string? currentItemName = null,
|
||||||
|
long? bytesCompleted = null,
|
||||||
|
long? bytesTotal = null,
|
||||||
|
double? bytesPerSecond = null,
|
||||||
|
int? tasksCompleted = null,
|
||||||
|
int? tasksTotal = null)
|
||||||
|
{
|
||||||
|
progress?.Report(new LauncherProgress(
|
||||||
|
phase,
|
||||||
|
statusMessage,
|
||||||
|
currentItemName,
|
||||||
|
bytesCompleted,
|
||||||
|
bytesTotal,
|
||||||
|
bytesPerSecond,
|
||||||
|
tasksCompleted,
|
||||||
|
tasksTotal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EventHandler<InstallerProgressChangedEventArgs>? CreateMinecraftFileProgressHandler(
|
||||||
|
IProgress<LauncherProgress>? progress)
|
||||||
|
{
|
||||||
|
if (progress == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (_, args) =>
|
||||||
|
{
|
||||||
|
progress.Report(new LauncherProgress(
|
||||||
|
phase: LaunchState.InstallMinecraft,
|
||||||
|
statusMessage: args.EventType.ToString(),
|
||||||
|
currentItemName: args.Name,
|
||||||
|
tasksCompleted: args.ProgressedTasks,
|
||||||
|
tasksTotal: args.TotalTasks));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EventHandler<ByteProgress>? CreateMinecraftByteProgressHandler(
|
||||||
|
IProgress<LauncherProgress>? progress)
|
||||||
|
{
|
||||||
|
if (progress == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (_, args) =>
|
||||||
|
{
|
||||||
|
progress.Report(new LauncherProgress(
|
||||||
|
phase: LaunchState.InstallMinecraft,
|
||||||
|
statusMessage: "Downloading Minecraft files...",
|
||||||
|
bytesCompleted: args.ProgressedBytes,
|
||||||
|
bytesTotal: args.TotalBytes));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IProgress<InstallerProgressChangedEventArgs>? CreateNeoForgeFileProgress(
|
||||||
|
IProgress<LauncherProgress>? progress)
|
||||||
|
{
|
||||||
|
if (progress == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Progress<InstallerProgressChangedEventArgs>(args =>
|
||||||
|
{
|
||||||
|
progress.Report(new LauncherProgress(
|
||||||
|
phase: LaunchState.InstallNeoforge,
|
||||||
|
statusMessage: args.EventType.ToString(),
|
||||||
|
currentItemName: args.Name,
|
||||||
|
tasksCompleted: args.ProgressedTasks,
|
||||||
|
tasksTotal: args.TotalTasks));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IProgress<ByteProgress>? CreateNeoForgeByteProgress(
|
||||||
|
IProgress<LauncherProgress>? progress)
|
||||||
|
{
|
||||||
|
if (progress == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Progress<ByteProgress>(args =>
|
||||||
|
{
|
||||||
|
progress.Report(new LauncherProgress(
|
||||||
|
phase: LaunchState.InstallNeoforge,
|
||||||
|
statusMessage: "Downloading NeoForge files...",
|
||||||
|
bytesCompleted: args.ProgressedBytes,
|
||||||
|
bytesTotal: args.TotalBytes));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IProgress<DownloadProgress>? CreateModProgress(
|
||||||
|
IProgress<LauncherProgress>? progress)
|
||||||
|
{
|
||||||
|
if (progress == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Progress<DownloadProgress>(downloadProgress =>
|
||||||
|
{
|
||||||
|
progress.Report(new LauncherProgress(
|
||||||
|
phase: LaunchState.SyncMods,
|
||||||
|
statusMessage: downloadProgress.StatusMessage ?? "Downloading mod...",
|
||||||
|
currentItemName: downloadProgress.FileName,
|
||||||
|
bytesCompleted: downloadProgress.BytesDownloaded,
|
||||||
|
bytesTotal: downloadProgress.TotalBytes,
|
||||||
|
bytesPerSecond: downloadProgress.BytesPerSecond));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,29 +2,28 @@ using AlayaCore.Abstractions.Configuration;
|
|||||||
|
|
||||||
namespace AlayaCore.Models.Configuration
|
namespace AlayaCore.Models.Configuration
|
||||||
{
|
{
|
||||||
public class GameOptions : BaseConfig
|
public sealed class GameOptions : BaseConfig
|
||||||
{
|
{
|
||||||
public override string FileName => "Game.json";
|
public override string FileName => "Game.json";
|
||||||
|
|
||||||
public string? LaunchVersion { get; set; }
|
public string? LaunchVersion { get; set; } = null;
|
||||||
|
|
||||||
public int MinimumRamMB { get; set; }
|
public int MinimumRamMb { get; set; } = 1024;
|
||||||
public int MaximumRamMB { get; set; }
|
public int MaximumRamMb { get; set; } = 2048;
|
||||||
|
|
||||||
public int ScreenWidth { get; set; }
|
public int ScreenWidth { get; set; } = 1920;
|
||||||
public int ScreenHeight { get; set; }
|
public int ScreenHeight { get; set; } = 1080;
|
||||||
|
|
||||||
public bool Fullscreen { get; set; }
|
public bool Fullscreen { get; set; } = false;
|
||||||
|
|
||||||
public static GameOptions Default { get; } = new GameOptions
|
public static GameOptions Default { get; } = new GameOptions
|
||||||
{
|
{
|
||||||
LaunchVersion = null,
|
LaunchVersion = null,
|
||||||
MinimumRamMB = 1024,
|
MinimumRamMb = 1024,
|
||||||
MaximumRamMB = 2048,
|
MaximumRamMb = 2048,
|
||||||
ScreenWidth = 1920,
|
ScreenWidth = 1920,
|
||||||
ScreenHeight = 1080,
|
ScreenHeight = 1080,
|
||||||
Fullscreen = false,
|
Fullscreen = false
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ namespace AlayaCore.Models.Configuration
|
|||||||
{
|
{
|
||||||
public sealed class LauncherOptions : BaseConfig
|
public sealed class LauncherOptions : BaseConfig
|
||||||
{
|
{
|
||||||
public bool ForceReinstall { get; set; }
|
public bool ForceReinstall { get; set; } = false;
|
||||||
|
|
||||||
public override string FileName => "Launcher.json";
|
public override string FileName => "Launcher.json";
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace AlayaCore.Models.Manifests
|
|||||||
{
|
{
|
||||||
public Version? Version { get; }
|
public Version? Version { get; }
|
||||||
public string Sha512Hash { get; }
|
public string Sha512Hash { get; }
|
||||||
public Uri DownloadUri { get; }
|
public Uri? DownloadUri { get; }
|
||||||
|
|
||||||
public LauncherManifestModel(
|
public LauncherManifestModel(
|
||||||
Version version,
|
Version version,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace AlayaCore.Models.Manifests
|
|||||||
public string MinecraftVersion { get; }
|
public string MinecraftVersion { get; }
|
||||||
public string NeoforgedVersion { get; }
|
public string NeoforgedVersion { get; }
|
||||||
|
|
||||||
public Uri ServerUrl { get; }
|
public Uri? ServerUrl { get; }
|
||||||
public int ServerPort { get; }
|
public int ServerPort { get; }
|
||||||
public IReadOnlyList<ModFileEntry> Files { get; }
|
public IReadOnlyList<ModFileEntry> Files { get; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,62 @@
|
|||||||
using System;
|
using AlayaCore.States;
|
||||||
using AlayaCore.Utilities.Enums;
|
|
||||||
|
|
||||||
namespace AlayaCore.Models.Progress
|
namespace AlayaCore.Models.Progress
|
||||||
{
|
{
|
||||||
public sealed class LauncherProgress
|
public sealed class LauncherProgress
|
||||||
{
|
{
|
||||||
public LauncherStage Stage { get; }
|
public LaunchState Phase { get; }
|
||||||
public string StatusMessage { get; }
|
public string StatusMessage { get; }
|
||||||
public double? PercentComplete { get; }
|
public string? CurrentItemName { get; }
|
||||||
|
|
||||||
|
public long? BytesCompleted { get; }
|
||||||
|
public long? BytesTotal { get; }
|
||||||
|
public double? BytesPerSecond { get; }
|
||||||
|
|
||||||
|
public int? TasksCompleted { get; }
|
||||||
|
public int? TasksTotal { get; }
|
||||||
|
|
||||||
|
public double? PercentComplete
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (BytesCompleted.HasValue && BytesTotal.HasValue && BytesTotal.Value > 0)
|
||||||
|
{
|
||||||
|
return (double)BytesCompleted.Value / BytesTotal.Value * 100d;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TasksCompleted.HasValue && TasksTotal.HasValue && TasksTotal.Value > 0)
|
||||||
|
{
|
||||||
|
return (double)TasksCompleted.Value / TasksTotal.Value * 100d;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public LauncherProgress(
|
public LauncherProgress(
|
||||||
LauncherStage stage,
|
LaunchState phase,
|
||||||
string statusMessage,
|
string statusMessage,
|
||||||
double? percentComplete = null)
|
string? currentItemName = null,
|
||||||
|
long? bytesCompleted = null,
|
||||||
|
long? bytesTotal = null,
|
||||||
|
double? bytesPerSecond = null,
|
||||||
|
int? tasksCompleted = null,
|
||||||
|
int? tasksTotal = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(statusMessage))
|
if (string.IsNullOrWhiteSpace(statusMessage))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Status message cannot be null or empty.", nameof(statusMessage));
|
throw new System.ArgumentException("Status message cannot be null, empty, or whitespace.", nameof(statusMessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
Stage = stage;
|
Phase = phase;
|
||||||
StatusMessage = statusMessage;
|
StatusMessage = statusMessage;
|
||||||
PercentComplete = percentComplete;
|
CurrentItemName = currentItemName;
|
||||||
|
BytesCompleted = bytesCompleted;
|
||||||
|
BytesTotal = bytesTotal;
|
||||||
|
BytesPerSecond = bytesPerSecond;
|
||||||
|
TasksCompleted = tasksCompleted;
|
||||||
|
TasksTotal = tasksTotal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ namespace AlayaCore.Models.Results
|
|||||||
{
|
{
|
||||||
public enum InstallOrUpdateResult
|
public enum InstallOrUpdateResult
|
||||||
{
|
{
|
||||||
|
Ready,
|
||||||
|
UpdaterLaunched
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ using AlayaCore.Abstractions.Interfaces.Services;
|
|||||||
using AlayaCore.Utilities.Enums;
|
using AlayaCore.Utilities.Enums;
|
||||||
using CmlLib.Core.Auth;
|
using CmlLib.Core.Auth;
|
||||||
using CmlLib.Core.Auth.Microsoft;
|
using CmlLib.Core.Auth.Microsoft;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Identity.Client;
|
using Microsoft.Identity.Client;
|
||||||
using XboxAuthNet.Game.Msal;
|
using XboxAuthNet.Game.Msal;
|
||||||
using XboxAuthNet.Game.Msal.OAuth;
|
using XboxAuthNet.Game.Msal.OAuth;
|
||||||
@@ -16,14 +17,18 @@ namespace AlayaCore.Services
|
|||||||
public sealed class AuthService : IAuthService
|
public sealed class AuthService : IAuthService
|
||||||
{
|
{
|
||||||
private readonly IFileStore _fileStore;
|
private readonly IFileStore _fileStore;
|
||||||
|
private readonly ILogger<AuthService> _logger;
|
||||||
|
|
||||||
private MSession? _session;
|
private MSession? _session;
|
||||||
private IPublicClientApplication? _clientApp;
|
private IPublicClientApplication? _clientApp;
|
||||||
private JELoginHandler? _loginHandler;
|
private JELoginHandler? _loginHandler;
|
||||||
|
|
||||||
public AuthService(IFileStore fileStore)
|
public AuthService(
|
||||||
|
IFileStore fileStore,
|
||||||
|
ILogger<AuthService> logger)
|
||||||
{
|
{
|
||||||
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> IsAuthenticatedAsync(CancellationToken cancellationToken = default)
|
public async Task<bool> IsAuthenticatedAsync(CancellationToken cancellationToken = default)
|
||||||
@@ -32,9 +37,12 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (_session != null && _session.CheckIsValid())
|
if (_session != null && _session.CheckIsValid())
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Authentication check succeeded using the cached in-memory session.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("No valid cached in-memory session was found. Attempting silent authentication check.");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
JELoginHandler loginHandler = await BuildHandlerAsync().ConfigureAwait(false);
|
JELoginHandler loginHandler = await BuildHandlerAsync().ConfigureAwait(false);
|
||||||
@@ -43,15 +51,28 @@ namespace AlayaCore.Services
|
|||||||
.AuthenticateSilently(cancellationToken: cancellationToken)
|
.AuthenticateSilently(cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
return _session != null && _session.CheckIsValid();
|
bool isAuthenticated = _session != null && _session.CheckIsValid();
|
||||||
|
|
||||||
|
if (isAuthenticated)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Silent authentication check succeeded.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Silent authentication completed but did not produce a valid Minecraft session.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAuthenticated;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Authentication check was cancelled.");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_session = null;
|
_session = null;
|
||||||
|
_logger.LogInformation(ex, "Silent authentication check failed. The user is not currently authenticated.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,27 +81,53 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogInformation("Starting authentication flow.");
|
||||||
|
|
||||||
JELoginHandler loginHandler = await BuildHandlerAsync().ConfigureAwait(false);
|
JELoginHandler loginHandler = await BuildHandlerAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Attempting silent authentication.");
|
||||||
|
|
||||||
_session = await loginHandler
|
_session = await loginHandler
|
||||||
.AuthenticateSilently(cancellationToken: cancellationToken)
|
.AuthenticateSilently(cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (_session != null && _session.CheckIsValid())
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Silent authentication succeeded.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Silent authentication completed but did not return a valid session.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Authentication was cancelled during silent authentication.");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation(ex, "Silent authentication failed. Falling back to interactive authentication.");
|
||||||
|
|
||||||
_session = await loginHandler
|
_session = await loginHandler
|
||||||
.AuthenticateInteractively(cancellationToken: cancellationToken)
|
.AuthenticateInteractively(cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (_session != null && _session.CheckIsValid())
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Interactive authentication succeeded.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Interactive authentication completed but did not return a valid session.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_session == null || !_session.CheckIsValid())
|
if (_session == null || !_session.CheckIsValid())
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Authentication failed because no valid Minecraft session was produced.");
|
||||||
throw new InvalidOperationException("Authentication did not produce a valid Minecraft session.");
|
throw new InvalidOperationException("Authentication did not produce a valid Minecraft session.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,10 +136,14 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogInformation("Signing out the current user.");
|
||||||
|
|
||||||
JELoginHandler loginHandler = await BuildHandlerAsync().ConfigureAwait(false);
|
JELoginHandler loginHandler = await BuildHandlerAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
await loginHandler.Signout(cancellationToken).ConfigureAwait(false);
|
await loginHandler.Signout(cancellationToken).ConfigureAwait(false);
|
||||||
_session = null;
|
_session = null;
|
||||||
|
|
||||||
|
_logger.LogInformation("Sign-out completed and cached session was cleared.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MSession> GetSessionAsync(CancellationToken cancellationToken = default)
|
public async Task<MSession> GetSessionAsync(CancellationToken cancellationToken = default)
|
||||||
@@ -101,16 +152,21 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (_session != null && _session.CheckIsValid())
|
if (_session != null && _session.CheckIsValid())
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Returning a valid cached Minecraft session.");
|
||||||
return _session;
|
return _session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("No valid cached session was available. Attempting authentication before returning a session.");
|
||||||
|
|
||||||
await AuthenticateAsync(cancellationToken).ConfigureAwait(false);
|
await AuthenticateAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (_session == null || !_session.CheckIsValid())
|
if (_session == null || !_session.CheckIsValid())
|
||||||
{
|
{
|
||||||
|
_logger.LogError("A valid Minecraft session was not available after authentication.");
|
||||||
throw new InvalidOperationException("No valid Minecraft session is available.");
|
throw new InvalidOperationException("No valid Minecraft session is available.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Returning Minecraft session obtained from authentication flow.");
|
||||||
return _session;
|
return _session;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,12 +174,15 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
if (_loginHandler != null)
|
if (_loginHandler != null)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Reusing existing JELoginHandler instance.");
|
||||||
return _loginHandler;
|
return _loginHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
string accountDirectory = _fileStore.GetOrCreate(FolderLocation.Data);
|
string accountDirectory = _fileStore.GetOrCreate(FolderLocation.Data);
|
||||||
string accountFilePath = Path.Combine(accountDirectory, "accounts.json");
|
string accountFilePath = Path.Combine(accountDirectory, "accounts.json");
|
||||||
|
|
||||||
|
_logger.LogInformation("Building MSAL client and login handler. Account cache path: {AccountFilePath}", accountFilePath);
|
||||||
|
|
||||||
_clientApp = await MsalClientHelper
|
_clientApp = await MsalClientHelper
|
||||||
.BuildApplicationWithCache("d91042d4-3eb5-43e4-b3ed-600e1d0760ff")
|
.BuildApplicationWithCache("d91042d4-3eb5-43e4-b3ed-600e1d0760ff")
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
@@ -133,6 +192,8 @@ namespace AlayaCore.Services
|
|||||||
.WithAccountManager(accountFilePath)
|
.WithAccountManager(accountFilePath)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
_logger.LogInformation("MSAL client and JELoginHandler were created successfully.");
|
||||||
|
|
||||||
return _loginHandler;
|
return _loginHandler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,25 +12,29 @@ using CmlLib.Core;
|
|||||||
using CmlLib.Core.Installer.NeoForge;
|
using CmlLib.Core.Installer.NeoForge;
|
||||||
using CmlLib.Core.Installer.NeoForge.Installers;
|
using CmlLib.Core.Installer.NeoForge.Installers;
|
||||||
using CmlLib.Core.Installers;
|
using CmlLib.Core.Installers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace AlayaCore.Services
|
namespace AlayaCore.Services
|
||||||
{
|
{
|
||||||
public sealed class GameInstallService : IGameInstallService
|
public sealed class GameInstallService : IGameInstallService
|
||||||
{
|
{
|
||||||
private const string InstalledModsManifestFileName = "InstalledModsManifest.json";
|
private const string INSTALLED_MODS_MANIFEST_FILE_NAME = "InstalledModsManifest.json";
|
||||||
|
|
||||||
private readonly IFileStore _fileStore;
|
private readonly IFileStore _fileStore;
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
|
private readonly ILogger<GameInstallService> _logger;
|
||||||
|
|
||||||
private AlayaPath? _gamePath;
|
private AlayaPath? _gamePath;
|
||||||
private MinecraftLauncher? _minecraftLauncher;
|
private MinecraftLauncher? _minecraftLauncher;
|
||||||
|
|
||||||
public GameInstallService(
|
public GameInstallService(
|
||||||
IFileStore fileStore,
|
IFileStore fileStore,
|
||||||
ISettingsService settingsService)
|
ISettingsService settingsService,
|
||||||
|
ILogger<GameInstallService> logger)
|
||||||
{
|
{
|
||||||
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
||||||
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
|
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task EnsureMinecraftInstalledAsync(
|
public async Task EnsureMinecraftInstalledAsync(
|
||||||
@@ -54,15 +58,23 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(manifest.MinecraftVersion))
|
if (string.IsNullOrWhiteSpace(manifest.MinecraftVersion))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Minecraft installation could not start because the manifest did not contain a Minecraft version.");
|
||||||
throw new InvalidDataException("Minecraft version is missing.");
|
throw new InvalidDataException("Minecraft version is missing.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Ensuring Minecraft is installed. Required version: {RequiredVersion}, Installed: {Installed}, Current version: {CurrentVersion}",
|
||||||
|
manifest.MinecraftVersion,
|
||||||
|
environment.MinecraftInstalled,
|
||||||
|
environment.MinecraftVersion);
|
||||||
|
|
||||||
bool alreadyInstalled =
|
bool alreadyInstalled =
|
||||||
environment.MinecraftInstalled &&
|
environment.MinecraftInstalled &&
|
||||||
string.Equals(environment.MinecraftVersion, manifest.MinecraftVersion, StringComparison.OrdinalIgnoreCase);
|
string.Equals(environment.MinecraftVersion, manifest.MinecraftVersion, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (alreadyInstalled)
|
if (alreadyInstalled)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Minecraft version {MinecraftVersion} is already installed and matches the manifest.", manifest.MinecraftVersion);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,14 +84,23 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (versionMismatch)
|
if (versionMismatch)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Minecraft version mismatch detected. Installed version: {InstalledVersion}, Required version: {RequiredVersion}. Cleaning old install.",
|
||||||
|
environment.MinecraftVersion,
|
||||||
|
manifest.MinecraftVersion);
|
||||||
|
|
||||||
await CleanOldInstallAsync(cancellationToken).ConfigureAwait(false);
|
await CleanOldInstallAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
MinecraftLauncher launcher = GetOrCreateLauncher(minecraftProgress, byteProgress);
|
MinecraftLauncher launcher = GetOrCreateLauncher(minecraftProgress, byteProgress);
|
||||||
|
|
||||||
|
_logger.LogInformation("Starting Minecraft installation for version {MinecraftVersion}.", manifest.MinecraftVersion);
|
||||||
|
|
||||||
await launcher
|
await launcher
|
||||||
.InstallAsync(manifest.MinecraftVersion, cancellationToken)
|
.InstallAsync(manifest.MinecraftVersion, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Minecraft installation completed for version {MinecraftVersion}.", manifest.MinecraftVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task EnsureNeoForgeInstalledAsync(
|
public async Task EnsureNeoForgeInstalledAsync(
|
||||||
@@ -103,20 +124,29 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(manifest.MinecraftVersion))
|
if (string.IsNullOrWhiteSpace(manifest.MinecraftVersion))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("NeoForge installation could not start because the manifest did not contain a Minecraft version.");
|
||||||
throw new InvalidDataException("Minecraft version is missing.");
|
throw new InvalidDataException("Minecraft version is missing.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(manifest.NeoforgedVersion))
|
if (string.IsNullOrWhiteSpace(manifest.NeoforgedVersion))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("NeoForge installation could not start because the manifest did not contain a NeoForge version.");
|
||||||
throw new InvalidDataException("NeoForge version is missing.");
|
throw new InvalidDataException("NeoForge version is missing.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Ensuring NeoForge is installed. Required version: {RequiredVersion}, Installed: {Installed}, Current version: {CurrentVersion}",
|
||||||
|
manifest.NeoforgedVersion,
|
||||||
|
environment.NeoforgedInstalled,
|
||||||
|
environment.NeoforgedVersion);
|
||||||
|
|
||||||
bool alreadyInstalled =
|
bool alreadyInstalled =
|
||||||
environment.NeoforgedInstalled &&
|
environment.NeoforgedInstalled &&
|
||||||
string.Equals(environment.NeoforgedVersion, manifest.NeoforgedVersion, StringComparison.OrdinalIgnoreCase);
|
string.Equals(environment.NeoforgedVersion, manifest.NeoforgedVersion, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (alreadyInstalled)
|
if (alreadyInstalled)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("NeoForge version {NeoForgeVersion} is already installed and matches the manifest.", manifest.NeoforgedVersion);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +156,11 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (neoForgeMismatch)
|
if (neoForgeMismatch)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"NeoForge version mismatch detected. Installed version: {InstalledVersion}, Required version: {RequiredVersion}. Cleaning old install and returning control to the director.",
|
||||||
|
environment.NeoforgedVersion,
|
||||||
|
manifest.NeoforgedVersion);
|
||||||
|
|
||||||
await CleanOldInstallAsync(cancellationToken).ConfigureAwait(false);
|
await CleanOldInstallAsync(cancellationToken).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -133,16 +168,27 @@ namespace AlayaCore.Services
|
|||||||
if (!environment.MinecraftInstalled ||
|
if (!environment.MinecraftInstalled ||
|
||||||
!string.Equals(environment.MinecraftVersion, manifest.MinecraftVersion, StringComparison.OrdinalIgnoreCase))
|
!string.Equals(environment.MinecraftVersion, manifest.MinecraftVersion, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Minecraft base installation is missing or mismatched before NeoForge installation. Ensuring Minecraft version {MinecraftVersion} first.",
|
||||||
|
manifest.MinecraftVersion);
|
||||||
|
|
||||||
await EnsureMinecraftInstalledAsync(manifest, environment, cancellationToken).ConfigureAwait(false);
|
await EnsureMinecraftInstalledAsync(manifest, environment, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(environment.JavaPath))
|
if (string.IsNullOrWhiteSpace(environment.JavaPath))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("NeoForge installation cannot continue because no valid Java path was found.");
|
||||||
throw new InvalidOperationException("A valid Java installation is required before installing NeoForge.");
|
throw new InvalidOperationException("A valid Java installation is required before installing NeoForge.");
|
||||||
}
|
}
|
||||||
|
|
||||||
MinecraftLauncher launcher = GetOrCreateLauncher();
|
MinecraftLauncher launcher = GetOrCreateLauncher();
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Starting NeoForge installation. Minecraft version: {MinecraftVersion}, NeoForge version: {NeoForgeVersion}, Java path: {JavaPath}",
|
||||||
|
manifest.MinecraftVersion,
|
||||||
|
manifest.NeoforgedVersion,
|
||||||
|
environment.JavaPath);
|
||||||
|
|
||||||
await InstallNeoForgeAsync(
|
await InstallNeoForgeAsync(
|
||||||
launcher,
|
launcher,
|
||||||
manifest,
|
manifest,
|
||||||
@@ -151,9 +197,15 @@ namespace AlayaCore.Services
|
|||||||
progress,
|
progress,
|
||||||
byteProgress).ConfigureAwait(false);
|
byteProgress).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"NeoForge installation completed. Verifying Minecraft files for version {MinecraftVersion}.",
|
||||||
|
manifest.MinecraftVersion);
|
||||||
|
|
||||||
await launcher
|
await launcher
|
||||||
.InstallAsync(manifest.MinecraftVersion, cancellationToken)
|
.InstallAsync(manifest.MinecraftVersion, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Minecraft file verification completed after NeoForge installation.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task VerifyFilesAsync(
|
public async Task VerifyFilesAsync(
|
||||||
@@ -169,14 +221,19 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(manifest.MinecraftVersion))
|
if (string.IsNullOrWhiteSpace(manifest.MinecraftVersion))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("File verification could not start because the manifest did not contain a Minecraft version.");
|
||||||
throw new InvalidDataException("Minecraft version is missing.");
|
throw new InvalidDataException("Minecraft version is missing.");
|
||||||
}
|
}
|
||||||
|
|
||||||
MinecraftLauncher launcher = GetOrCreateLauncher();
|
MinecraftLauncher launcher = GetOrCreateLauncher();
|
||||||
|
|
||||||
|
_logger.LogInformation("Verifying Minecraft files for version {MinecraftVersion}.", manifest.MinecraftVersion);
|
||||||
|
|
||||||
await launcher
|
await launcher
|
||||||
.InstallAsync(manifest.MinecraftVersion, cancellationToken)
|
.InstallAsync(manifest.MinecraftVersion, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Minecraft file verification completed for version {MinecraftVersion}.", manifest.MinecraftVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CleanOldInstallAsync(CancellationToken cancellationToken)
|
private async Task CleanOldInstallAsync(CancellationToken cancellationToken)
|
||||||
@@ -185,26 +242,42 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
string gamePath = GetMinecraftPath();
|
string gamePath = GetMinecraftPath();
|
||||||
|
|
||||||
|
_logger.LogInformation("Cleaning old game installation at path {GamePath}.", gamePath);
|
||||||
|
|
||||||
if (Directory.Exists(gamePath))
|
if (Directory.Exists(gamePath))
|
||||||
{
|
{
|
||||||
Directory.Delete(gamePath, recursive: true);
|
Directory.Delete(gamePath, recursive: true);
|
||||||
|
_logger.LogInformation("Deleted game directory {GamePath}.", gamePath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Game directory {GamePath} did not exist. No game files needed deletion.", gamePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
string installedModsManifestPath = Path.Combine(
|
string installedModsManifestPath = Path.Combine(
|
||||||
_fileStore.Get(FolderLocation.Manifests),
|
_fileStore.Get(FolderLocation.Manifests),
|
||||||
InstalledModsManifestFileName);
|
INSTALLED_MODS_MANIFEST_FILE_NAME);
|
||||||
|
|
||||||
if (File.Exists(installedModsManifestPath))
|
if (File.Exists(installedModsManifestPath))
|
||||||
{
|
{
|
||||||
File.Delete(installedModsManifestPath);
|
File.Delete(installedModsManifestPath);
|
||||||
|
_logger.LogInformation("Deleted installed mods manifest at {InstalledModsManifestPath}.", installedModsManifestPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Installed mods manifest {InstalledModsManifestPath} did not exist.", installedModsManifestPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
_gamePath = null;
|
_gamePath = null;
|
||||||
_minecraftLauncher = null;
|
_minecraftLauncher = null;
|
||||||
|
|
||||||
|
_logger.LogDebug("Cleared cached Minecraft launcher state.");
|
||||||
|
|
||||||
await _settingsService
|
await _settingsService
|
||||||
.UpdateLaunchVersionAsync(string.Empty, cancellationToken)
|
.UpdateLaunchVersionAsync(string.Empty, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Cleared stored launch version after cleaning the old install.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private MinecraftLauncher GetOrCreateLauncher(
|
private MinecraftLauncher GetOrCreateLauncher(
|
||||||
@@ -213,20 +286,25 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
if (_minecraftLauncher != null)
|
if (_minecraftLauncher != null)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Reusing existing MinecraftLauncher instance.");
|
||||||
return _minecraftLauncher;
|
return _minecraftLauncher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Creating a new MinecraftLauncher instance.");
|
||||||
|
|
||||||
_gamePath = new AlayaPath(_fileStore);
|
_gamePath = new AlayaPath(_fileStore);
|
||||||
_minecraftLauncher = new MinecraftLauncher(_gamePath);
|
_minecraftLauncher = new MinecraftLauncher(_gamePath);
|
||||||
|
|
||||||
if (byteProgress != null)
|
if (byteProgress != null)
|
||||||
{
|
{
|
||||||
_minecraftLauncher.ByteProgressChanged += byteProgress;
|
_minecraftLauncher.ByteProgressChanged += byteProgress;
|
||||||
|
_logger.LogDebug("Attached Minecraft byte progress handler.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minecraftProgress != null)
|
if (minecraftProgress != null)
|
||||||
{
|
{
|
||||||
_minecraftLauncher.FileProgressChanged += minecraftProgress;
|
_minecraftLauncher.FileProgressChanged += minecraftProgress;
|
||||||
|
_logger.LogDebug("Attached Minecraft file progress handler.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return _minecraftLauncher;
|
return _minecraftLauncher;
|
||||||
@@ -245,6 +323,11 @@ namespace AlayaCore.Services
|
|||||||
throw new ArgumentNullException(nameof(launcher));
|
throw new ArgumentNullException(nameof(launcher));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Configuring NeoForge installer. Minecraft version: {MinecraftVersion}, NeoForge version: {NeoForgeVersion}",
|
||||||
|
manifest.MinecraftVersion,
|
||||||
|
manifest.NeoforgedVersion);
|
||||||
|
|
||||||
NeoForgeInstaller installer = new NeoForgeInstaller(launcher);
|
NeoForgeInstaller installer = new NeoForgeInstaller(launcher);
|
||||||
|
|
||||||
NeoForgeInstallOptions options = new NeoForgeInstallOptions
|
NeoForgeInstallOptions options = new NeoForgeInstallOptions
|
||||||
@@ -257,25 +340,33 @@ namespace AlayaCore.Services
|
|||||||
if (progress != null)
|
if (progress != null)
|
||||||
{
|
{
|
||||||
options.FileProgress = progress;
|
options.FileProgress = progress;
|
||||||
|
_logger.LogDebug("Attached NeoForge file progress reporter.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (byteProgress != null)
|
if (byteProgress != null)
|
||||||
{
|
{
|
||||||
options.ByteProgress = byteProgress;
|
options.ByteProgress = byteProgress;
|
||||||
|
_logger.LogDebug("Attached NeoForge byte progress reporter.");
|
||||||
}
|
}
|
||||||
|
|
||||||
string version = await installer
|
string version = await installer
|
||||||
.Install(manifest.MinecraftVersion, manifest.NeoforgedVersion, options)
|
.Install(manifest.MinecraftVersion, manifest.NeoforgedVersion, options)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("NeoForge installer returned launch version {LaunchVersion}.", version);
|
||||||
|
|
||||||
await _settingsService
|
await _settingsService
|
||||||
.UpdateLaunchVersionAsync(version, cancellationToken)
|
.UpdateLaunchVersionAsync(version, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Persisted launch version {LaunchVersion} to settings.", version);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetMinecraftPath()
|
private string GetMinecraftPath()
|
||||||
{
|
{
|
||||||
return _fileStore.GetOrCreate(FolderLocation.Game);
|
string gamePath = _fileStore.GetOrCreate(FolderLocation.Game);
|
||||||
|
_logger.LogDebug("Resolved Minecraft game path to {GamePath}.", gamePath);
|
||||||
|
return gamePath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ using AlayaCore.Models.Configuration;
|
|||||||
using AlayaCore.Models.Manifests;
|
using AlayaCore.Models.Manifests;
|
||||||
using CmlLib.Core;
|
using CmlLib.Core;
|
||||||
using CmlLib.Core.ProcessBuilder;
|
using CmlLib.Core.ProcessBuilder;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace AlayaCore.Services
|
namespace AlayaCore.Services
|
||||||
{
|
{
|
||||||
@@ -19,6 +20,7 @@ namespace AlayaCore.Services
|
|||||||
private readonly IFileStore _fileStore;
|
private readonly IFileStore _fileStore;
|
||||||
private readonly ILaunchDirector _director;
|
private readonly ILaunchDirector _director;
|
||||||
private readonly GameOptions _gameOptions;
|
private readonly GameOptions _gameOptions;
|
||||||
|
private readonly ILogger<GameLaunchService> _logger;
|
||||||
|
|
||||||
private MinecraftLauncher? _minecraftLauncher;
|
private MinecraftLauncher? _minecraftLauncher;
|
||||||
|
|
||||||
@@ -26,12 +28,14 @@ namespace AlayaCore.Services
|
|||||||
IAuthService authService,
|
IAuthService authService,
|
||||||
IFileStore fileStore,
|
IFileStore fileStore,
|
||||||
ILaunchDirector director,
|
ILaunchDirector director,
|
||||||
GameOptions gameOptions)
|
GameOptions gameOptions,
|
||||||
|
ILogger<GameLaunchService> logger)
|
||||||
{
|
{
|
||||||
_authService = authService ?? throw new ArgumentNullException(nameof(authService));
|
_authService = authService ?? throw new ArgumentNullException(nameof(authService));
|
||||||
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
||||||
_director = director ?? throw new ArgumentNullException(nameof(director));
|
_director = director ?? throw new ArgumentNullException(nameof(director));
|
||||||
_gameOptions = gameOptions ?? throw new ArgumentNullException(nameof(gameOptions));
|
_gameOptions = gameOptions ?? throw new ArgumentNullException(nameof(gameOptions));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LaunchAsync(
|
public async Task LaunchAsync(
|
||||||
@@ -41,6 +45,7 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
if (!_director.CanRun)
|
if (!_director.CanRun)
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Game launch was requested while the launcher was not in a runnable state.");
|
||||||
throw new InvalidOperationException("The launcher is not in a runnable state.");
|
throw new InvalidOperationException("The launcher is not in a runnable state.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,16 +61,27 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_gameOptions.LaunchVersion))
|
if (string.IsNullOrWhiteSpace(_gameOptions.LaunchVersion))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Game launch could not start because GameOptions.LaunchVersion is not configured.");
|
||||||
throw new InvalidDataException("GameOptions.LaunchVersion is not configured.");
|
throw new InvalidDataException("GameOptions.LaunchVersion is not configured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(environment.JavaPath))
|
if (string.IsNullOrWhiteSpace(environment.JavaPath))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Game launch could not start because no valid Java path was available.");
|
||||||
throw new InvalidOperationException("A valid Java path is required to launch the game.");
|
throw new InvalidOperationException("A valid Java path is required to launch the game.");
|
||||||
}
|
}
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Starting game launch. LaunchVersion: {LaunchVersion}, JavaPath: {JavaPath}, Resolution: {Width}x{Height}, MinRamMb: {MinRamMb}, MaxRamMb: {MaxRamMb}",
|
||||||
|
_gameOptions.LaunchVersion,
|
||||||
|
environment.JavaPath,
|
||||||
|
_gameOptions.ScreenWidth,
|
||||||
|
_gameOptions.ScreenHeight,
|
||||||
|
_gameOptions.MinimumRamMb,
|
||||||
|
_gameOptions.MaximumRamMb);
|
||||||
|
|
||||||
MLaunchOption option = await BuildLaunchOptionsAsync(
|
MLaunchOption option = await BuildLaunchOptionsAsync(
|
||||||
manifest,
|
manifest,
|
||||||
environment,
|
environment,
|
||||||
@@ -73,23 +89,39 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
MinecraftLauncher launcher = GetOrCreateLauncher();
|
MinecraftLauncher launcher = GetOrCreateLauncher();
|
||||||
|
|
||||||
|
_logger.LogInformation("Creating Minecraft process for launch version {LaunchVersion}.", _gameOptions.LaunchVersion);
|
||||||
|
|
||||||
var process = await launcher
|
var process = await launcher
|
||||||
.CreateProcessAsync(_gameOptions.LaunchVersion, option)
|
.CreateProcessAsync(_gameOptions.LaunchVersion, option)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Minecraft process was created successfully. ProcessId: {ProcessId}",
|
||||||
|
process.Id);
|
||||||
|
|
||||||
var processWrapper = new ProcessWrapper(process);
|
var processWrapper = new ProcessWrapper(process);
|
||||||
|
|
||||||
|
_logger.LogInformation("Starting Minecraft process.");
|
||||||
processWrapper.StartWithEvents();
|
processWrapper.StartWithEvents();
|
||||||
|
|
||||||
|
_logger.LogInformation("Waiting for Minecraft process to exit.");
|
||||||
await processWrapper.WaitForExitTaskAsync().ConfigureAwait(false);
|
await processWrapper.WaitForExitTaskAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Minecraft process exited. ProcessId: {ProcessId}, ExitCode: {ExitCode}",
|
||||||
|
process.Id,
|
||||||
|
process.ExitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private MinecraftLauncher GetOrCreateLauncher()
|
private MinecraftLauncher GetOrCreateLauncher()
|
||||||
{
|
{
|
||||||
if (_minecraftLauncher != null)
|
if (_minecraftLauncher != null)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Reusing existing MinecraftLauncher instance for game launch.");
|
||||||
return _minecraftLauncher;
|
return _minecraftLauncher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Creating a new MinecraftLauncher instance for game launch.");
|
||||||
_minecraftLauncher = new MinecraftLauncher(new AlayaPath(_fileStore));
|
_minecraftLauncher = new MinecraftLauncher(new AlayaPath(_fileStore));
|
||||||
return _minecraftLauncher;
|
return _minecraftLauncher;
|
||||||
}
|
}
|
||||||
@@ -99,17 +131,40 @@ namespace AlayaCore.Services
|
|||||||
InstallEnvironment environment,
|
InstallEnvironment environment,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
if (manifest == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(manifest));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (environment == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(environment));
|
||||||
|
}
|
||||||
|
|
||||||
if (manifest.ServerUrl == null)
|
if (manifest.ServerUrl == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Launch options could not be built because Manifest.ServerUrl is not configured.");
|
||||||
throw new InvalidDataException("Manifest ServerUrl is not configured.");
|
throw new InvalidDataException("Manifest ServerUrl is not configured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Building launch options. ServerHost: {ServerHost}, ServerPort: {ServerPort}, LaunchVersion: {LaunchVersion}",
|
||||||
|
manifest.ServerUrl.Host,
|
||||||
|
manifest.ServerPort,
|
||||||
|
_gameOptions.LaunchVersion);
|
||||||
|
|
||||||
|
var session = await _authService
|
||||||
|
.GetSessionAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogDebug("A valid Minecraft session was acquired for launch.");
|
||||||
|
|
||||||
return new MLaunchOption
|
return new MLaunchOption
|
||||||
{
|
{
|
||||||
Session = await _authService.GetSessionAsync(cancellationToken).ConfigureAwait(false),
|
Session = session,
|
||||||
JavaPath = environment.JavaPath,
|
JavaPath = environment.JavaPath,
|
||||||
MinimumRamMb = _gameOptions.MinimumRamMB,
|
MinimumRamMb = _gameOptions.MinimumRamMb,
|
||||||
MaximumRamMb = _gameOptions.MaximumRamMB,
|
MaximumRamMb = _gameOptions.MaximumRamMb,
|
||||||
ScreenWidth = _gameOptions.ScreenWidth,
|
ScreenWidth = _gameOptions.ScreenWidth,
|
||||||
ScreenHeight = _gameOptions.ScreenHeight,
|
ScreenHeight = _gameOptions.ScreenHeight,
|
||||||
ServerIp = manifest.ServerUrl.Host,
|
ServerIp = manifest.ServerUrl.Host,
|
||||||
|
|||||||
@@ -9,18 +9,23 @@ using AlayaCore.Abstractions.Interfaces.Clients;
|
|||||||
using AlayaCore.Abstractions.Interfaces.Services;
|
using AlayaCore.Abstractions.Interfaces.Services;
|
||||||
using AlayaCore.Models.Progress;
|
using AlayaCore.Models.Progress;
|
||||||
using AlayaCore.Models.Results;
|
using AlayaCore.Models.Results;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace AlayaCore.Services
|
namespace AlayaCore.Services
|
||||||
{
|
{
|
||||||
public sealed class HttpDownloadService : IDownloadService
|
public sealed class HttpDownloadService : IDownloadService
|
||||||
{
|
{
|
||||||
private const int BufferSize = 81920;
|
private const int BUFFER_SIZE = 81920;
|
||||||
|
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClient _httpClient;
|
||||||
|
private readonly ILogger<HttpDownloadService> _logger;
|
||||||
|
|
||||||
public HttpDownloadService(IHttpClient httpClient)
|
public HttpDownloadService(
|
||||||
|
IHttpClient httpClient,
|
||||||
|
ILogger<HttpDownloadService> logger)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DownloadResult> DownloadFileAsync(
|
public async Task<DownloadResult> DownloadFileAsync(
|
||||||
@@ -54,12 +59,22 @@ namespace AlayaCore.Services
|
|||||||
throw new ArgumentException("Destination path must include a file name.", nameof(destinationPath));
|
throw new ArgumentException("Destination path must include a file name.", nameof(destinationPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Starting download workflow for {FileName} from {SourceUri} to {DestinationPath}.",
|
||||||
|
fileName,
|
||||||
|
sourceUri,
|
||||||
|
destinationPath);
|
||||||
|
|
||||||
EnsureDestinationDirectoryExists(destinationPath);
|
EnsureDestinationDirectoryExists(destinationPath);
|
||||||
|
|
||||||
if (File.Exists(destinationPath) && VerifyFileHash(destinationPath, normalizedExpectedHash))
|
if (File.Exists(destinationPath) && VerifyFileHash(destinationPath, normalizedExpectedHash))
|
||||||
{
|
{
|
||||||
long existingLength = new FileInfo(destinationPath).Length;
|
long existingLength = new FileInfo(destinationPath).Length;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Skipped download for {FileName} because the destination file already exists and passed SHA-512 verification.",
|
||||||
|
fileName);
|
||||||
|
|
||||||
progress?.Report(new DownloadProgress(
|
progress?.Report(new DownloadProgress(
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
destinationPath: destinationPath,
|
destinationPath: destinationPath,
|
||||||
@@ -78,6 +93,13 @@ namespace AlayaCore.Services
|
|||||||
bool destinationExisted = File.Exists(destinationPath);
|
bool destinationExisted = File.Exists(destinationPath);
|
||||||
string tempFilePath = destinationPath + ".download";
|
string tempFilePath = destinationPath + ".download";
|
||||||
|
|
||||||
|
if (destinationExisted)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Destination file for {FileName} already existed but was not valid. A replacement download will be attempted.",
|
||||||
|
fileName);
|
||||||
|
}
|
||||||
|
|
||||||
DeleteFileIfExists(tempFilePath);
|
DeleteFileIfExists(tempFilePath);
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -92,6 +114,11 @@ namespace AlayaCore.Services
|
|||||||
long? totalBytes = response.Content.Headers.ContentLength;
|
long? totalBytes = response.Content.Headers.ContentLength;
|
||||||
long bytesDownloaded = 0;
|
long bytesDownloaded = 0;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Download response received for {FileName}. Content-Length: {TotalBytes}.",
|
||||||
|
fileName,
|
||||||
|
totalBytes);
|
||||||
|
|
||||||
progress?.Report(new DownloadProgress(
|
progress?.Report(new DownloadProgress(
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
destinationPath: destinationPath,
|
destinationPath: destinationPath,
|
||||||
@@ -109,14 +136,16 @@ namespace AlayaCore.Services
|
|||||||
FileMode.Create,
|
FileMode.Create,
|
||||||
FileAccess.Write,
|
FileAccess.Write,
|
||||||
FileShare.None,
|
FileShare.None,
|
||||||
BufferSize,
|
BUFFER_SIZE,
|
||||||
useAsync: true);
|
useAsync: true);
|
||||||
|
|
||||||
using SHA512 sha512 = SHA512.Create();
|
using SHA512 sha512 = SHA512.Create();
|
||||||
|
|
||||||
byte[] buffer = new byte[BufferSize];
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
_logger.LogDebug("Streaming download content for {FileName} into temporary file {TempFilePath}.", fileName, tempFilePath);
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
int bytesRead = await responseStream.ReadAsync(
|
int bytesRead = await responseStream.ReadAsync(
|
||||||
@@ -161,10 +190,20 @@ namespace AlayaCore.Services
|
|||||||
string actualHash = ConvertToLowerHex(sha512.Hash);
|
string actualHash = ConvertToLowerHex(sha512.Hash);
|
||||||
if (!string.Equals(actualHash, normalizedExpectedHash, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(actualHash, normalizedExpectedHash, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Hash verification failed for downloaded file {FileName}. Expected SHA-512: {ExpectedHash}. Actual SHA-512: {ActualHash}.",
|
||||||
|
fileName,
|
||||||
|
normalizedExpectedHash,
|
||||||
|
actualHash);
|
||||||
|
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"Downloaded file hash mismatch. Expected '{normalizedExpectedHash}', got '{actualHash}'.");
|
$"Downloaded file hash mismatch. Expected '{normalizedExpectedHash}', got '{actualHash}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Hash verification succeeded for {FileName}. Replacing destination file with downloaded content.",
|
||||||
|
fileName);
|
||||||
|
|
||||||
ReplaceDestinationFile(tempFilePath, destinationPath);
|
ReplaceDestinationFile(tempFilePath, destinationPath);
|
||||||
|
|
||||||
double? finalBytesPerSecond = null;
|
double? finalBytesPerSecond = null;
|
||||||
@@ -181,14 +220,40 @@ namespace AlayaCore.Services
|
|||||||
bytesPerSecond: finalBytesPerSecond,
|
bytesPerSecond: finalBytesPerSecond,
|
||||||
statusMessage: "Download complete."));
|
statusMessage: "Download complete."));
|
||||||
|
|
||||||
|
DownloadOutcome outcome = destinationExisted
|
||||||
|
? DownloadOutcome.ReplacedInvalid
|
||||||
|
: DownloadOutcome.Downloaded;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Download completed successfully for {FileName}. Outcome: {Outcome}. Bytes downloaded: {BytesDownloaded}.",
|
||||||
|
fileName,
|
||||||
|
outcome,
|
||||||
|
bytesDownloaded);
|
||||||
|
|
||||||
return new DownloadResult(
|
return new DownloadResult(
|
||||||
destinationPath,
|
destinationPath,
|
||||||
destinationExisted ? DownloadOutcome.ReplacedInvalid : DownloadOutcome.Downloaded,
|
outcome,
|
||||||
hashVerified: true,
|
hashVerified: true,
|
||||||
bytesDownloaded: bytesDownloaded);
|
bytesDownloaded: bytesDownloaded);
|
||||||
}
|
}
|
||||||
catch
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Download for {FileName} was cancelled. Cleaning up temporary file {TempFilePath}.",
|
||||||
|
fileName,
|
||||||
|
tempFilePath);
|
||||||
|
|
||||||
|
DeleteFileIfExists(tempFilePath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
ex,
|
||||||
|
"Download failed for {FileName}. Cleaning up temporary file {TempFilePath}.",
|
||||||
|
fileName,
|
||||||
|
tempFilePath);
|
||||||
|
|
||||||
DeleteFileIfExists(tempFilePath);
|
DeleteFileIfExists(tempFilePath);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
@@ -205,6 +270,7 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (!File.Exists(filePath))
|
if (!File.Exists(filePath))
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Hash verification skipped because file does not exist at {FilePath}.", filePath);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +280,22 @@ namespace AlayaCore.Services
|
|||||||
byte[] hashBytes = sha512.ComputeHash(fileStream);
|
byte[] hashBytes = sha512.ComputeHash(fileStream);
|
||||||
string actualHash = ConvertToLowerHex(hashBytes);
|
string actualHash = ConvertToLowerHex(hashBytes);
|
||||||
|
|
||||||
return string.Equals(actualHash, normalizedExpectedHash, StringComparison.OrdinalIgnoreCase);
|
bool matches = string.Equals(actualHash, normalizedExpectedHash, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (matches)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("SHA-512 verification succeeded for {FilePath}.", filePath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"SHA-512 verification failed for {FilePath}. Expected SHA-512: {ExpectedHash}. Actual SHA-512: {ActualHash}.",
|
||||||
|
filePath,
|
||||||
|
normalizedExpectedHash,
|
||||||
|
actualHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void EnsureDestinationDirectoryExists(string destinationPath)
|
private static void EnsureDestinationDirectoryExists(string destinationPath)
|
||||||
|
|||||||
@@ -11,29 +11,35 @@ using AlayaCore.Abstractions.Interfaces.Services;
|
|||||||
using AlayaCore.Installation;
|
using AlayaCore.Installation;
|
||||||
using AlayaCore.Models.Manifests;
|
using AlayaCore.Models.Manifests;
|
||||||
using AlayaCore.Utilities.Enums;
|
using AlayaCore.Utilities.Enums;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace AlayaCore.Services
|
namespace AlayaCore.Services
|
||||||
{
|
{
|
||||||
public sealed class InstallationStateService : IInstallStateService
|
public sealed class InstallationStateService : IInstallStateService
|
||||||
{
|
{
|
||||||
private const string VersionsFolderName = "versions";
|
private const string VERSIONS_FOLDER_NAME = "versions";
|
||||||
|
|
||||||
private readonly IFileStore _fileStore;
|
private readonly IFileStore _fileStore;
|
||||||
private readonly IManifestService _manifestService;
|
private readonly IManifestService _manifestService;
|
||||||
|
private readonly ILogger<InstallationStateService> _logger;
|
||||||
|
|
||||||
public InstallationStateService(
|
public InstallationStateService(
|
||||||
IFileStore fileStore,
|
IFileStore fileStore,
|
||||||
IManifestService manifestService)
|
IManifestService manifestService,
|
||||||
|
ILogger<InstallationStateService> logger)
|
||||||
{
|
{
|
||||||
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
||||||
_manifestService = manifestService ?? throw new ArgumentNullException(nameof(manifestService));
|
_manifestService = manifestService ?? throw new ArgumentNullException(nameof(manifestService));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<InstallEnvironment> GetCurrentEnvironmentAsync(CancellationToken cancellationToken = default)
|
public async Task<InstallEnvironment> GetCurrentEnvironmentAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogDebug("Building current installation environment state.");
|
||||||
|
|
||||||
OSPlatform platform = GetCurrentPlatform();
|
OSPlatform platform = GetCurrentPlatform();
|
||||||
|
|
||||||
bool javaInstalled = TryGetJavaPath(out string? javaPath);
|
bool javaInstalled = TryGetJavaPath(out string? javaPath);
|
||||||
@@ -41,15 +47,20 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (javaInstalled)
|
if (javaInstalled)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Java runtime was detected at {JavaPath}. Attempting to read Java version.", javaPath);
|
||||||
javaVersion = GetJavaVersion(javaPath!);
|
javaVersion = GetJavaVersion(javaPath!);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No managed Java runtime was detected.");
|
||||||
|
}
|
||||||
|
|
||||||
InstalledVersionState versionState = GetInstalledVersionState();
|
InstalledVersionState versionState = GetInstalledVersionState();
|
||||||
|
|
||||||
InstalledModsManifestModel installedModsManifest =
|
InstalledModsManifestModel installedModsManifest =
|
||||||
await _manifestService.GetInstalledModsManifestAsync(cancellationToken).ConfigureAwait(false);
|
await _manifestService.GetInstalledModsManifestAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
return new InstallEnvironment(
|
InstallEnvironment environment = new InstallEnvironment(
|
||||||
osPlatform: platform,
|
osPlatform: platform,
|
||||||
javaInstalled: javaInstalled,
|
javaInstalled: javaInstalled,
|
||||||
javaPath: javaPath,
|
javaPath: javaPath,
|
||||||
@@ -59,6 +70,19 @@ namespace AlayaCore.Services
|
|||||||
neoforgedInstalled: !string.IsNullOrWhiteSpace(versionState.NeoForgeVersion),
|
neoforgedInstalled: !string.IsNullOrWhiteSpace(versionState.NeoForgeVersion),
|
||||||
neoforgedVersion: versionState.NeoForgeVersion,
|
neoforgedVersion: versionState.NeoForgeVersion,
|
||||||
installedModsManifest: installedModsManifest);
|
installedModsManifest: installedModsManifest);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Installation environment resolved. Platform: {Platform}, JavaInstalled: {JavaInstalled}, JavaVersion: {JavaVersion}, MinecraftInstalled: {MinecraftInstalled}, MinecraftVersion: {MinecraftVersion}, NeoForgeInstalled: {NeoForgeInstalled}, NeoForgeVersion: {NeoForgeVersion}, InstalledModsCount: {InstalledModsCount}",
|
||||||
|
platform,
|
||||||
|
environment.JavaInstalled,
|
||||||
|
environment.JavaVersion,
|
||||||
|
environment.MinecraftInstalled,
|
||||||
|
environment.MinecraftVersion,
|
||||||
|
environment.NeoforgedInstalled,
|
||||||
|
environment.NeoforgedVersion,
|
||||||
|
environment.InstalledModsManifest.Mods.Count);
|
||||||
|
|
||||||
|
return environment;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static OSPlatform GetCurrentPlatform()
|
private static OSPlatform GetCurrentPlatform()
|
||||||
@@ -81,7 +105,7 @@ namespace AlayaCore.Services
|
|||||||
throw new PlatformNotSupportedException("The current operating system is not supported.");
|
throw new PlatformNotSupportedException("The current operating system is not supported.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? GetJavaVersion(string javaPath)
|
private string? GetJavaVersion(string javaPath)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(javaPath))
|
if (string.IsNullOrWhiteSpace(javaPath))
|
||||||
{
|
{
|
||||||
@@ -93,6 +117,8 @@ namespace AlayaCore.Services
|
|||||||
throw new FileNotFoundException("Java executable was not found.", javaPath);
|
throw new FileNotFoundException("Java executable was not found.", javaPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Reading Java version from executable at {JavaPath}.", javaPath);
|
||||||
|
|
||||||
using var process = new Process
|
using var process = new Process
|
||||||
{
|
{
|
||||||
StartInfo = new ProcessStartInfo
|
StartInfo = new ProcessStartInfo
|
||||||
@@ -113,10 +139,26 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (process.ExitCode != 0 && string.IsNullOrWhiteSpace(standardError))
|
if (process.ExitCode != 0 && string.IsNullOrWhiteSpace(standardError))
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Java version check for {JavaPath} exited with code {ExitCode} and produced no version output.",
|
||||||
|
javaPath,
|
||||||
|
process.ExitCode);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ParseJavaVersion(standardError);
|
string? version = ParseJavaVersion(standardError);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(version))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Java version output from {JavaPath} could not be parsed.", javaPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Parsed Java version {JavaVersion} from {JavaPath}.", version, javaPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? ParseJavaVersion(string processOutput)
|
private static string? ParseJavaVersion(string processOutput)
|
||||||
@@ -144,10 +186,12 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (!File.Exists(fullPath))
|
if (!File.Exists(fullPath))
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Managed Java executable was not found at {JavaPath}.", fullPath);
|
||||||
javaPath = null;
|
javaPath = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Managed Java executable was found at {JavaPath}.", fullPath);
|
||||||
javaPath = fullPath;
|
javaPath = fullPath;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -156,8 +200,11 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
string versionsPath = GetVersionsPath();
|
string versionsPath = GetVersionsPath();
|
||||||
|
|
||||||
|
_logger.LogDebug("Inspecting installed version metadata under {VersionsPath}.", versionsPath);
|
||||||
|
|
||||||
if (!Directory.Exists(versionsPath))
|
if (!Directory.Exists(versionsPath))
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Versions directory does not exist at {VersionsPath}. No Minecraft or NeoForge installation was detected.", versionsPath);
|
||||||
return InstalledVersionState.Empty();
|
return InstalledVersionState.Empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +215,7 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (versionDirectories.Length == 0)
|
if (versionDirectories.Length == 0)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Versions directory at {VersionsPath} was empty.", versionsPath);
|
||||||
return InstalledVersionState.Empty();
|
return InstalledVersionState.Empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +228,7 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(versionFolderName))
|
if (string.IsNullOrWhiteSpace(versionFolderName))
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Skipping version directory with an invalid folder name: {VersionDirectory}.", versionDirectory);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,11 +236,13 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (!File.Exists(versionJsonPath))
|
if (!File.Exists(versionJsonPath))
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Skipping version directory {VersionDirectory} because version metadata file {VersionJsonPath} was not found.", versionDirectory, versionJsonPath);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryLoadJson(versionJsonPath, out JObject? versionJson))
|
if (!TryLoadJson(versionJsonPath, out JObject? versionJson))
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning("Skipping version metadata file {VersionJsonPath} because it could not be read or parsed.", versionJsonPath);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +251,7 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(id))
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Skipping version metadata file {VersionJsonPath} because it did not contain a valid id.", versionJsonPath);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,18 +264,32 @@ namespace AlayaCore.Services
|
|||||||
minecraftVersion = inheritsFrom;
|
minecraftVersion = inheritsFrom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Detected NeoForge version metadata. Id: {NeoForgeVersion}, InheritsFrom: {MinecraftVersion}",
|
||||||
|
id,
|
||||||
|
inheritsFrom);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
minecraftVersion ??= id;
|
minecraftVersion ??= id;
|
||||||
|
|
||||||
|
_logger.LogDebug("Detected Minecraft version metadata. Id: {MinecraftVersion}", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Installed version state resolved. MinecraftVersion: {MinecraftVersion}, NeoForgeVersion: {NeoForgeVersion}",
|
||||||
|
minecraftVersion,
|
||||||
|
neoForgeVersion);
|
||||||
|
|
||||||
return new InstalledVersionState(minecraftVersion, neoForgeVersion);
|
return new InstalledVersionState(minecraftVersion, neoForgeVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetVersionsPath()
|
private string GetVersionsPath()
|
||||||
{
|
{
|
||||||
return Path.Combine(_fileStore.Get(FolderLocation.Game), VersionsFolderName);
|
string versionsPath = Path.Combine(_fileStore.Get(FolderLocation.Game), VERSIONS_FOLDER_NAME);
|
||||||
|
_logger.LogDebug("Resolved versions path to {VersionsPath}.", versionsPath);
|
||||||
|
return versionsPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsNeoForgeVersion(string? id, string? inheritsFrom)
|
private static bool IsNeoForgeVersion(string? id, string? inheritsFrom)
|
||||||
@@ -242,7 +308,7 @@ namespace AlayaCore.Services
|
|||||||
value.Contains("neoforged", StringComparison.OrdinalIgnoreCase);
|
value.Contains("neoforged", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryLoadJson(string path, out JObject? jsonObject)
|
private bool TryLoadJson(string path, out JObject? jsonObject)
|
||||||
{
|
{
|
||||||
jsonObject = null;
|
jsonObject = null;
|
||||||
|
|
||||||
@@ -251,20 +317,22 @@ namespace AlayaCore.Services
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
string json = File.ReadAllText(path);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
string json = File.ReadAllText(path);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("JSON file at {Path} was empty.", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
jsonObject = JObject.Parse(json);
|
jsonObject = JObject.Parse(json);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to load or parse JSON file at {Path}.", path);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
|||||||
using AlayaCore.Abstractions.Interfaces.Services;
|
using AlayaCore.Abstractions.Interfaces.Services;
|
||||||
using AlayaCore.Models.Configuration;
|
using AlayaCore.Models.Configuration;
|
||||||
using AlayaCore.Models.Manifests;
|
using AlayaCore.Models.Manifests;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace AlayaCore.Services
|
namespace AlayaCore.Services
|
||||||
{
|
{
|
||||||
@@ -13,40 +14,49 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
private readonly IManifestService _manifestService;
|
private readonly IManifestService _manifestService;
|
||||||
private readonly LauncherUpdateServiceOptions _options;
|
private readonly LauncherUpdateServiceOptions _options;
|
||||||
|
private readonly ILogger<LauncherUpdateService> _logger;
|
||||||
|
|
||||||
public LauncherUpdateService(
|
public LauncherUpdateService(
|
||||||
IManifestService manifestService,
|
IManifestService manifestService,
|
||||||
LauncherUpdateServiceOptions options)
|
LauncherUpdateServiceOptions options,
|
||||||
|
ILogger<LauncherUpdateService> logger)
|
||||||
{
|
{
|
||||||
_manifestService = manifestService ?? throw new ArgumentNullException(nameof(manifestService));
|
_manifestService = manifestService ?? throw new ArgumentNullException(nameof(manifestService));
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> DoesLauncherNeedUpdating(CancellationToken cancellationToken = default)
|
public async Task<bool> DoesLauncherNeedUpdating(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_options.ForceUpdate)
|
if (_options.ForceUpdate)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning("Launcher update check is being forced by configuration.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogDebug("Checking whether the launcher requires an update.");
|
||||||
|
|
||||||
LauncherManifestModel? localManifest = await _manifestService
|
LauncherManifestModel? localManifest = await _manifestService
|
||||||
.GetLocalLauncherManifestAsync(cancellationToken)
|
.GetLocalLauncherManifestAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (localManifest == null)
|
if (localManifest == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("No local launcher manifest was found. The launcher will be treated as requiring an update.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localManifest.Version == null)
|
if (localManifest.Version == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning("Local launcher manifest did not contain a valid version. The launcher will be treated as requiring an update.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(localManifest.Sha512Hash))
|
if (string.IsNullOrWhiteSpace(localManifest.Sha512Hash))
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning("Local launcher manifest did not contain a valid SHA-512 hash. The launcher will be treated as requiring an update.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,16 +66,19 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (remoteManifest == null)
|
if (remoteManifest == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Remote launcher manifest could not be loaded.");
|
||||||
throw new InvalidOperationException("Remote launcher manifest could not be loaded.");
|
throw new InvalidOperationException("Remote launcher manifest could not be loaded.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remoteManifest.Version == null)
|
if (remoteManifest.Version == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Remote launcher manifest did not contain a valid version.");
|
||||||
throw new InvalidOperationException("Remote launcher manifest returned an invalid version.");
|
throw new InvalidOperationException("Remote launcher manifest returned an invalid version.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(remoteManifest.Sha512Hash))
|
if (string.IsNullOrWhiteSpace(remoteManifest.Sha512Hash))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Remote launcher manifest did not contain a valid SHA-512 hash.");
|
||||||
throw new InvalidOperationException("Remote launcher manifest returned an invalid SHA-512 hash.");
|
throw new InvalidOperationException("Remote launcher manifest returned an invalid SHA-512 hash.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +88,13 @@ namespace AlayaCore.Services
|
|||||||
remoteManifest.Sha512Hash.Trim(),
|
remoteManifest.Sha512Hash.Trim(),
|
||||||
StringComparison.OrdinalIgnoreCase);
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Launcher update check complete. LocalVersion: {LocalVersion}, RemoteVersion: {RemoteVersion}, VersionMismatch: {VersionMismatch}, HashMismatch: {HashMismatch}",
|
||||||
|
localManifest.Version,
|
||||||
|
remoteManifest.Version,
|
||||||
|
versionMismatch,
|
||||||
|
hashMismatch);
|
||||||
|
|
||||||
return versionMismatch || hashMismatch;
|
return versionMismatch || hashMismatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,11 +111,13 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (newManifest.DownloadUri == null)
|
if (newManifest.DownloadUri == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Updater launch failed because the launcher manifest did not contain a download URI.");
|
||||||
throw new InvalidOperationException("Launcher manifest does not contain a download URI.");
|
throw new InvalidOperationException("Launcher manifest does not contain a download URI.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newManifest.DownloadUri.IsAbsoluteUri)
|
if (!newManifest.DownloadUri.IsAbsoluteUri)
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Updater launch failed because the launcher download URI was not absolute. DownloadUri: {DownloadUri}", newManifest.DownloadUri);
|
||||||
throw new InvalidOperationException("Launcher download URI must be absolute.");
|
throw new InvalidOperationException("Launcher download URI must be absolute.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,16 +125,19 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(updaterPath))
|
if (string.IsNullOrWhiteSpace(updaterPath))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Updater launch failed because the updater path was not configured.");
|
||||||
throw new InvalidOperationException("Updater path is not configured.");
|
throw new InvalidOperationException("Updater path is not configured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Path.IsPathFullyQualified(updaterPath))
|
if (!Path.IsPathFullyQualified(updaterPath))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Updater launch failed because the updater path was not absolute. UpdaterPath: {UpdaterPath}", updaterPath);
|
||||||
throw new InvalidOperationException("Updater path must be absolute.");
|
throw new InvalidOperationException("Updater path must be absolute.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!File.Exists(updaterPath))
|
if (!File.Exists(updaterPath))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Updater launch failed because the updater executable was not found at {UpdaterPath}.", updaterPath);
|
||||||
throw new FileNotFoundException("Alaya updater program was not found.", updaterPath);
|
throw new FileNotFoundException("Alaya updater program was not found.", updaterPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,17 +155,30 @@ namespace AlayaCore.Services
|
|||||||
CreateNoWindow = true
|
CreateNoWindow = true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Launching updater process. UpdaterPath: {UpdaterPath}, WorkingDirectory: {WorkingDirectory}, DownloadUri: {DownloadUri}",
|
||||||
|
updaterPath,
|
||||||
|
workingDirectory,
|
||||||
|
newManifest.DownloadUri.AbsoluteUri);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Process? process = Process.Start(startInfo);
|
Process? process = Process.Start(startInfo);
|
||||||
|
|
||||||
if (process == null)
|
if (process == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Updater process start returned null for executable {UpdaterPath}.", updaterPath);
|
||||||
throw new InvalidOperationException("Failed to start updater process.");
|
throw new InvalidOperationException("Failed to start updater process.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Updater process launched successfully. ProcessId: {ProcessId}, UpdaterPath: {UpdaterPath}",
|
||||||
|
process.Id,
|
||||||
|
updaterPath);
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
_logger.LogError(ex, "Failed to launch updater process from {UpdaterPath}.", updaterPath);
|
||||||
throw new InvalidOperationException("Failed to launch updater process.", ex);
|
throw new InvalidOperationException("Failed to launch updater process.", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,37 +11,46 @@ using AlayaCore.Models.Manifests;
|
|||||||
using AlayaCore.Models.Manifests.DTO;
|
using AlayaCore.Models.Manifests.DTO;
|
||||||
using AlayaCore.Utilities.Enums;
|
using AlayaCore.Utilities.Enums;
|
||||||
using AlayaCore.Utilities.Extensions;
|
using AlayaCore.Utilities.Extensions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace AlayaCore.Services
|
namespace AlayaCore.Services
|
||||||
{
|
{
|
||||||
public sealed class ManifestService : IManifestService
|
public sealed class ManifestService : IManifestService
|
||||||
{
|
{
|
||||||
private const string CoreManifestFileName = "CoreManifest.json";
|
private const string ALAYA_MANIFEST_FILE_NAME = "AlayaManifest.json";
|
||||||
private const string LauncherManifestFileName = "LauncherManifest.json";
|
private const string LAUNCHER_MANIFEST_FILE_NAME = "LauncherManifest.json";
|
||||||
private const string InstalledModsManifestFileName = "InstalledModsManifest.json";
|
private const string INSTALLED_MODS_MANIFEST_FILE_NAME = "InstalledModsManifest.json";
|
||||||
|
|
||||||
private readonly IDownloadService _downloadService;
|
private readonly IDownloadService _downloadService;
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly IFileStore _fileStore;
|
private readonly IFileStore _fileStore;
|
||||||
private readonly ManifestServiceOptions _options;
|
private readonly ManifestServiceOptions _options;
|
||||||
|
private readonly ILogger<ManifestService> _logger;
|
||||||
|
|
||||||
public ManifestService(
|
public ManifestService(
|
||||||
IDownloadService downloadService,
|
IDownloadService downloadService,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IFileStore fileStore,
|
IFileStore fileStore,
|
||||||
ManifestServiceOptions options)
|
ManifestServiceOptions options,
|
||||||
|
ILogger<ManifestService> logger)
|
||||||
{
|
{
|
||||||
_downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService));
|
_downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService));
|
||||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||||
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<ManifestModel> GetCoreManifestAsync(CancellationToken cancellationToken = default)
|
public Task<ManifestModel> GetAlayaManifestAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
string destinationPath = GetCoreManifestPath();
|
string destinationPath = GetCoreManifestPath();
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Downloading and loading Alaya manifest from {ManifestUri} to {DestinationPath}.",
|
||||||
|
_options.CoreManifestUri,
|
||||||
|
destinationPath);
|
||||||
|
|
||||||
return DownloadAndLoadManifestAsync<ManifestDto, ManifestModel>(
|
return DownloadAndLoadManifestAsync<ManifestDto, ManifestModel>(
|
||||||
_options.CoreManifestUri,
|
_options.CoreManifestUri,
|
||||||
destinationPath,
|
destinationPath,
|
||||||
@@ -55,8 +64,11 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
string path = GetInstalledModsManifestPath();
|
string path = GetInstalledModsManifestPath();
|
||||||
|
|
||||||
|
_logger.LogDebug("Loading installed mods manifest from {ManifestPath}.", path);
|
||||||
|
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Installed mods manifest was not found at {ManifestPath}. Returning an empty manifest.", path);
|
||||||
return InstalledModsManifestModel.Empty();
|
return InstalledModsManifestModel.Empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +76,7 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning("Installed mods manifest at {ManifestPath} was empty. Returning an empty manifest.", path);
|
||||||
return InstalledModsManifestModel.Empty();
|
return InstalledModsManifestModel.Empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,13 +87,29 @@ namespace AlayaCore.Services
|
|||||||
static dto => dto.ToModel(),
|
static dto => dto.ToModel(),
|
||||||
swallowDeserializationErrors: true);
|
swallowDeserializationErrors: true);
|
||||||
|
|
||||||
return manifest ?? InstalledModsManifestModel.Empty();
|
if (manifest == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Installed mods manifest at {ManifestPath} could not be deserialized. Returning an empty manifest.", path);
|
||||||
|
return InstalledModsManifestModel.Empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Loaded installed mods manifest from {ManifestPath}. Mod count: {ModCount}",
|
||||||
|
path,
|
||||||
|
manifest.Mods.Count);
|
||||||
|
|
||||||
|
return manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<LauncherManifestModel> GetLauncherManifestAsync(CancellationToken cancellationToken = default)
|
public Task<LauncherManifestModel> GetLauncherManifestAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
string destinationPath = GetLauncherManifestPath();
|
string destinationPath = GetLauncherManifestPath();
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Downloading and loading launcher manifest from {ManifestUri} to {DestinationPath}.",
|
||||||
|
_options.LauncherManifestUri,
|
||||||
|
destinationPath);
|
||||||
|
|
||||||
return DownloadAndLoadManifestAsync<LauncherManifestDto, LauncherManifestModel>(
|
return DownloadAndLoadManifestAsync<LauncherManifestDto, LauncherManifestModel>(
|
||||||
_options.LauncherManifestUri,
|
_options.LauncherManifestUri,
|
||||||
destinationPath,
|
destinationPath,
|
||||||
@@ -89,24 +118,34 @@ namespace AlayaCore.Services
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<ManifestModel?> GetLocalCoreManifestAsync(CancellationToken cancellationToken = default)
|
public Task<ManifestModel?> GetLocalAlayaManifestAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
string path = GetCoreManifestPath();
|
||||||
|
|
||||||
|
_logger.LogDebug("Loading local Alaya manifest from {ManifestPath}.", path);
|
||||||
|
|
||||||
return LoadLocalManifestAsync<ManifestDto, ManifestModel>(
|
return LoadLocalManifestAsync<ManifestDto, ManifestModel>(
|
||||||
GetCoreManifestPath(),
|
path,
|
||||||
static dto => dto.ToModel(),
|
static dto => dto.ToModel(),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<LauncherManifestModel?> GetLocalLauncherManifestAsync(CancellationToken cancellationToken = default)
|
public Task<LauncherManifestModel?> GetLocalLauncherManifestAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
string path = GetLauncherManifestPath();
|
||||||
|
|
||||||
|
_logger.LogDebug("Loading local launcher manifest from {ManifestPath}.", path);
|
||||||
|
|
||||||
return LoadLocalManifestAsync<LauncherManifestDto, LauncherManifestModel>(
|
return LoadLocalManifestAsync<LauncherManifestDto, LauncherManifestModel>(
|
||||||
GetLauncherManifestPath(),
|
path,
|
||||||
static dto => dto.ToModel(),
|
static dto => dto.ToModel(),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Version> GetRemoteCoreManifestVersionAsync(CancellationToken cancellationToken = default)
|
public async Task<Version> GetRemoteCoreManifestVersionAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Fetching remote Alaya manifest version from {ManifestUri}.", _options.CoreManifestUri);
|
||||||
|
|
||||||
ManifestModel remoteManifest = await GetRemoteManifestAsync<ManifestDto, ManifestModel>(
|
ManifestModel remoteManifest = await GetRemoteManifestAsync<ManifestDto, ManifestModel>(
|
||||||
_options.CoreManifestUri,
|
_options.CoreManifestUri,
|
||||||
static dto => dto.ToModel(),
|
static dto => dto.ToModel(),
|
||||||
@@ -114,16 +153,24 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (remoteManifest.AlayaVersion == null)
|
if (remoteManifest.AlayaVersion == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Remote Alaya manifest from {ManifestUri} did not contain a valid version.", _options.CoreManifestUri);
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"Remote core manifest from '{_options.CoreManifestUri}' does not contain a valid version.");
|
$"Remote core manifest from '{_options.CoreManifestUri}' does not contain a valid version.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Fetched remote Alaya manifest version {RemoteVersion} from {ManifestUri}.",
|
||||||
|
remoteManifest.AlayaVersion,
|
||||||
|
_options.CoreManifestUri);
|
||||||
|
|
||||||
return remoteManifest.AlayaVersion;
|
return remoteManifest.AlayaVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LauncherManifestModel> GetRemoteLauncherManifestAsync(
|
public async Task<LauncherManifestModel> GetRemoteLauncherManifestAsync(
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Fetching remote launcher manifest from {ManifestUri}.", _options.LauncherManifestUri);
|
||||||
|
|
||||||
LauncherManifestModel remoteManifest = await GetRemoteManifestAsync<LauncherManifestDto, LauncherManifestModel>(
|
LauncherManifestModel remoteManifest = await GetRemoteManifestAsync<LauncherManifestDto, LauncherManifestModel>(
|
||||||
_options.LauncherManifestUri,
|
_options.LauncherManifestUri,
|
||||||
static dto => dto.ToModel(),
|
static dto => dto.ToModel(),
|
||||||
@@ -131,32 +178,45 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(remoteManifest.Sha512Hash))
|
if (string.IsNullOrWhiteSpace(remoteManifest.Sha512Hash))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Remote launcher manifest from {ManifestUri} did not contain a valid SHA-512 hash.", _options.LauncherManifestUri);
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"Remote launcher manifest from '{_options.LauncherManifestUri}' does not contain a valid SHA-512 hash.");
|
$"Remote launcher manifest from '{_options.LauncherManifestUri}' does not contain a valid SHA-512 hash.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remoteManifest.DownloadUri == null || !remoteManifest.DownloadUri.IsAbsoluteUri)
|
if (remoteManifest.DownloadUri == null || !remoteManifest.DownloadUri.IsAbsoluteUri)
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Remote launcher manifest from {ManifestUri} did not contain a valid download URI.", _options.LauncherManifestUri);
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"Remote launcher manifest from '{_options.LauncherManifestUri}' does not contain a valid download URI.");
|
$"Remote launcher manifest from '{_options.LauncherManifestUri}' does not contain a valid download URI.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Fetched remote launcher manifest. Version: {Version}, DownloadUri: {DownloadUri}",
|
||||||
|
remoteManifest.Version,
|
||||||
|
remoteManifest.DownloadUri);
|
||||||
|
|
||||||
return remoteManifest;
|
return remoteManifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetLauncherManifestPath()
|
public string GetLauncherManifestPath()
|
||||||
{
|
{
|
||||||
return Path.Combine(_fileStore.Get(FolderLocation.Manifests), LauncherManifestFileName);
|
string path = Path.Combine(_fileStore.Get(FolderLocation.Manifests), LAUNCHER_MANIFEST_FILE_NAME);
|
||||||
|
_logger.LogDebug("Resolved launcher manifest path to {ManifestPath}.", path);
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetCoreManifestPath()
|
public string GetCoreManifestPath()
|
||||||
{
|
{
|
||||||
return Path.Combine(_fileStore.Get(FolderLocation.Manifests), CoreManifestFileName);
|
string path = Path.Combine(_fileStore.Get(FolderLocation.Manifests), ALAYA_MANIFEST_FILE_NAME);
|
||||||
|
_logger.LogDebug("Resolved Alaya manifest path to {ManifestPath}.", path);
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetInstalledModsManifestPath()
|
public string GetInstalledModsManifestPath()
|
||||||
{
|
{
|
||||||
return Path.Combine(_fileStore.Get(FolderLocation.Manifests), InstalledModsManifestFileName);
|
string path = Path.Combine(_fileStore.Get(FolderLocation.Manifests), INSTALLED_MODS_MANIFEST_FILE_NAME);
|
||||||
|
_logger.LogDebug("Resolved installed mods manifest path to {ManifestPath}.", path);
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TModel?> LoadLocalManifestAsync<TDto, TModel>(
|
private async Task<TModel?> LoadLocalManifestAsync<TDto, TModel>(
|
||||||
@@ -177,6 +237,7 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Local manifest was not found at {ManifestPath}.", path);
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,14 +245,26 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning("Local manifest at {ManifestPath} was empty.", path);
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
return DeserializeAndMapManifest<TDto, TModel>(
|
TModel? model = DeserializeAndMapManifest(
|
||||||
json,
|
json,
|
||||||
path,
|
path,
|
||||||
map,
|
map,
|
||||||
swallowDeserializationErrors: true);
|
swallowDeserializationErrors: true);
|
||||||
|
|
||||||
|
if (model == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Local manifest at {ManifestPath} could not be deserialized or mapped.", path);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Successfully loaded local manifest from {ManifestPath}.", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TModel> DownloadAndLoadManifestAsync<TDto, TModel>(
|
private async Task<TModel> DownloadAndLoadManifestAsync<TDto, TModel>(
|
||||||
@@ -231,6 +304,11 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
EnsureDirectoryExists(destinationPath);
|
EnsureDirectoryExists(destinationPath);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Downloading manifest from {ManifestUri} to {DestinationPath}.",
|
||||||
|
manifestUri,
|
||||||
|
destinationPath);
|
||||||
|
|
||||||
await _downloadService.DownloadFileAsync(
|
await _downloadService.DownloadFileAsync(
|
||||||
manifestUri,
|
manifestUri,
|
||||||
destinationPath,
|
destinationPath,
|
||||||
@@ -239,6 +317,7 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (!File.Exists(destinationPath))
|
if (!File.Exists(destinationPath))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Manifest file was not found after download at {DestinationPath}.", destinationPath);
|
||||||
throw new FileNotFoundException("Manifest file was not found after download.", destinationPath);
|
throw new FileNotFoundException("Manifest file was not found after download.", destinationPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,14 +325,22 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Downloaded manifest file at {DestinationPath} was empty.", destinationPath);
|
||||||
throw new InvalidDataException($"Manifest file '{destinationPath}' was empty.");
|
throw new InvalidDataException($"Manifest file '{destinationPath}' was empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return DeserializeAndMapManifest<TDto, TModel>(
|
TModel model = DeserializeAndMapManifest(
|
||||||
json,
|
json,
|
||||||
destinationPath,
|
destinationPath,
|
||||||
map,
|
map,
|
||||||
swallowDeserializationErrors: false)!;
|
swallowDeserializationErrors: false)!;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Downloaded and loaded manifest successfully from {ManifestUri} into {DestinationPath}.",
|
||||||
|
manifestUri,
|
||||||
|
destinationPath);
|
||||||
|
|
||||||
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TModel> GetRemoteManifestAsync<TDto, TModel>(
|
private async Task<TModel> GetRemoteManifestAsync<TDto, TModel>(
|
||||||
@@ -279,6 +366,8 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogDebug("Fetching remote manifest from {ManifestUri}.", manifestUri);
|
||||||
|
|
||||||
using HttpResponseMessage response = await _httpClient.GetAsync(
|
using HttpResponseMessage response = await _httpClient.GetAsync(
|
||||||
manifestUri,
|
manifestUri,
|
||||||
HttpCompletionOption.ResponseContentRead,
|
HttpCompletionOption.ResponseContentRead,
|
||||||
@@ -290,18 +379,23 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Remote manifest response from {ManifestUri} was empty.", manifestUri);
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"Remote manifest response from '{manifestUri}' was empty.");
|
$"Remote manifest response from '{manifestUri}' was empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return DeserializeAndMapManifest<TDto, TModel>(
|
TModel model = DeserializeAndMapManifest(
|
||||||
json,
|
json,
|
||||||
manifestUri.ToString(),
|
manifestUri.ToString(),
|
||||||
map,
|
map,
|
||||||
swallowDeserializationErrors: false)!;
|
swallowDeserializationErrors: false)!;
|
||||||
|
|
||||||
|
_logger.LogDebug("Successfully fetched and mapped remote manifest from {ManifestUri}.", manifestUri);
|
||||||
|
|
||||||
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TModel? DeserializeAndMapManifest<TDto, TModel>(
|
private TModel? DeserializeAndMapManifest<TDto, TModel>(
|
||||||
string json,
|
string json,
|
||||||
string sourceName,
|
string sourceName,
|
||||||
Func<TDto, TModel> map,
|
Func<TDto, TModel> map,
|
||||||
@@ -312,9 +406,11 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
if (swallowDeserializationErrors)
|
if (swallowDeserializationErrors)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning("Manifest source {SourceName} was empty and deserialization errors were configured to be swallowed.", sourceName);
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogError("Manifest source {SourceName} was empty.", sourceName);
|
||||||
throw new InvalidDataException($"Manifest source '{sourceName}' was empty.");
|
throw new InvalidDataException($"Manifest source '{sourceName}' was empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,9 +433,11 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
if (swallowDeserializationErrors)
|
if (swallowDeserializationErrors)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to deserialize manifest source {SourceName} to {DtoType}. Returning default because deserialization errors are being swallowed.", sourceName, typeof(TDto).Name);
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogError(ex, "Failed to deserialize manifest source {SourceName} to {DtoType}.", sourceName, typeof(TDto).Name);
|
||||||
throw new JsonSerializationException(
|
throw new JsonSerializationException(
|
||||||
$"Failed to deserialize manifest source '{sourceName}' to {typeof(TDto).Name}.",
|
$"Failed to deserialize manifest source '{sourceName}' to {typeof(TDto).Name}.",
|
||||||
ex);
|
ex);
|
||||||
@@ -349,24 +447,34 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
if (swallowDeserializationErrors)
|
if (swallowDeserializationErrors)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning("Deserialization of manifest source {SourceName} to {DtoType} returned null. Returning default because deserialization errors are being swallowed.", sourceName, typeof(TDto).Name);
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogError("Deserialization of manifest source {SourceName} to {DtoType} returned null.", sourceName, typeof(TDto).Name);
|
||||||
throw new JsonSerializationException(
|
throw new JsonSerializationException(
|
||||||
$"Failed to deserialize manifest source '{sourceName}' to {typeof(TDto).Name}.");
|
$"Failed to deserialize manifest source '{sourceName}' to {typeof(TDto).Name}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return map(dto);
|
TModel model = map(dto);
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Mapped manifest source {SourceName} from {DtoType} to {ModelType}.",
|
||||||
|
sourceName,
|
||||||
|
typeof(TDto).Name,
|
||||||
|
typeof(TModel).Name);
|
||||||
|
return model;
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
{
|
{
|
||||||
if (swallowDeserializationErrors)
|
if (swallowDeserializationErrors)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning(ex, "Manifest source {SourceName} was deserialized but could not be mapped to {ModelType}. Returning default because mapping errors are being swallowed.", sourceName, typeof(TModel).Name);
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogError(ex, "Manifest source {SourceName} was deserialized but could not be mapped to {ModelType}.", sourceName, typeof(TModel).Name);
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"Manifest source '{sourceName}' was deserialized but could not be mapped to {typeof(TModel).Name}.",
|
$"Manifest source '{sourceName}' was deserialized but could not be mapped to {typeof(TModel).Name}.",
|
||||||
ex);
|
ex);
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ using AlayaCore.Models;
|
|||||||
using AlayaCore.Models.Configuration;
|
using AlayaCore.Models.Configuration;
|
||||||
using AlayaCore.Models.Manifests;
|
using AlayaCore.Models.Manifests;
|
||||||
using AlayaCore.Models.Manifests.DTO;
|
using AlayaCore.Models.Manifests.DTO;
|
||||||
|
using AlayaCore.Models.Progress;
|
||||||
using AlayaCore.Utilities.Enums;
|
using AlayaCore.Utilities.Enums;
|
||||||
using AlayaCore.Utilities.Extensions;
|
using AlayaCore.Utilities.Extensions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
@@ -28,22 +30,26 @@ namespace AlayaCore.Services
|
|||||||
private readonly ModrinthConnectionOptions _options;
|
private readonly ModrinthConnectionOptions _options;
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly IFileStore _fileStore;
|
private readonly IFileStore _fileStore;
|
||||||
|
private readonly ILogger<ModService> _logger;
|
||||||
|
|
||||||
public ModService(
|
public ModService(
|
||||||
IDownloadService downloadService,
|
IDownloadService downloadService,
|
||||||
ModrinthConnectionOptions options,
|
ModrinthConnectionOptions options,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IFileStore fileStore)
|
IFileStore fileStore,
|
||||||
|
ILogger<ModService> logger)
|
||||||
{
|
{
|
||||||
_downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService));
|
_downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService));
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||||
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ProcessModsAsync(
|
public async Task ProcessModsAsync(
|
||||||
ManifestModel manifest,
|
ManifestModel manifest,
|
||||||
InstallEnvironment environment,
|
InstallEnvironment environment,
|
||||||
|
IProgress<DownloadProgress>? progress = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (manifest == null)
|
if (manifest == null)
|
||||||
@@ -62,9 +68,15 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
List<ModFileEntry> requiredMods = manifest.Files
|
List<ModFileEntry> requiredMods = manifest.Files
|
||||||
.Where(file => file.Type == FileType.Mod)
|
.Where(file => file.Type == FileType.Mod)
|
||||||
|
.OrderBy(file => file.FileName, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
RemoveStaleMods(requiredMods);
|
_logger.LogInformation(
|
||||||
|
"Starting mod sync. RequiredMods: {RequiredModCount}, InstalledModsManifestEntries: {InstalledModCount}",
|
||||||
|
requiredMods.Count,
|
||||||
|
installedMods.Count);
|
||||||
|
|
||||||
|
RemoveStaleMods(requiredMods, cancellationToken);
|
||||||
|
|
||||||
List<ModFileEntry> finalInstalledMods = new List<ModFileEntry>();
|
List<ModFileEntry> finalInstalledMods = new List<ModFileEntry>();
|
||||||
|
|
||||||
@@ -72,6 +84,12 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Processing required mod {FileName}. Expected SHA-512: {Sha512Hash}, Expected Size: {Size}",
|
||||||
|
requiredMod.FileName,
|
||||||
|
requiredMod.Sha512Hash,
|
||||||
|
requiredMod.Size);
|
||||||
|
|
||||||
ModFileEntry? installedMod = installedMods.FirstOrDefault(
|
ModFileEntry? installedMod = installedMods.FirstOrDefault(
|
||||||
mod => string.Equals(mod.FileName, requiredMod.FileName, StringComparison.OrdinalIgnoreCase));
|
mod => string.Equals(mod.FileName, requiredMod.FileName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
@@ -85,18 +103,50 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (isValidInstalledMod)
|
if (isValidInstalledMod)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Mod {FileName} is already installed and valid. Skipping download.",
|
||||||
|
requiredMod.FileName);
|
||||||
|
|
||||||
finalInstalledMods.Add(installedMod!);
|
finalInstalledMods.Add(installedMod!);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (installedMod == null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Mod {FileName} is missing locally and will be downloaded.",
|
||||||
|
requiredMod.FileName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Mod {FileName} is present but invalid or outdated. Stored SHA-512: {InstalledHash}, Expected SHA-512: {RequiredHash}, Stored Size: {InstalledSize}, Expected Size: {RequiredSize}",
|
||||||
|
requiredMod.FileName,
|
||||||
|
installedMod.Sha512Hash,
|
||||||
|
requiredMod.Sha512Hash,
|
||||||
|
installedMod.Size,
|
||||||
|
requiredMod.Size);
|
||||||
|
}
|
||||||
|
|
||||||
Uri modUri = await ResolveModUrlAsync(requiredMod, cancellationToken).ConfigureAwait(false);
|
Uri modUri = await ResolveModUrlAsync(requiredMod, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Downloading mod {FileName} from {ModUri} to {DestinationPath}.",
|
||||||
|
requiredMod.FileName,
|
||||||
|
modUri,
|
||||||
|
destinationPath);
|
||||||
|
|
||||||
await _downloadService.DownloadFileAsync(
|
await _downloadService.DownloadFileAsync(
|
||||||
modUri,
|
modUri,
|
||||||
destinationPath,
|
destinationPath,
|
||||||
requiredMod.Sha512Hash,
|
requiredMod.Sha512Hash,
|
||||||
|
progress,
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Download completed successfully for mod {FileName}.",
|
||||||
|
requiredMod.FileName);
|
||||||
|
|
||||||
finalInstalledMods.Add(new ModFileEntry(
|
finalInstalledMods.Add(new ModFileEntry(
|
||||||
requiredMod.FileName,
|
requiredMod.FileName,
|
||||||
requiredMod.Type,
|
requiredMod.Type,
|
||||||
@@ -105,6 +155,10 @@ namespace AlayaCore.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
await FlushInstalledModsManifestAsync(finalInstalledMods, cancellationToken).ConfigureAwait(false);
|
await FlushInstalledModsManifestAsync(finalInstalledMods, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Mod sync completed successfully. Final installed mod count: {InstalledModCount}",
|
||||||
|
finalInstalledMods.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsInstalledModUpToDate(
|
private static bool IsInstalledModUpToDate(
|
||||||
@@ -145,11 +199,20 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(fileEntry.Sha512Hash))
|
if (string.IsNullOrWhiteSpace(fileEntry.Sha512Hash))
|
||||||
{
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Failed to resolve mod URL because mod {FileName} did not contain a SHA-512 hash.",
|
||||||
|
fileEntry.FileName);
|
||||||
|
|
||||||
throw new InvalidOperationException($"Mod '{fileEntry.FileName}' does not contain a SHA-512 hash.");
|
throw new InvalidOperationException($"Mod '{fileEntry.FileName}' does not contain a SHA-512 hash.");
|
||||||
}
|
}
|
||||||
|
|
||||||
string versionEndpoint = BuildVersionEndpoint(fileEntry.Sha512Hash);
|
string versionEndpoint = BuildVersionEndpoint(fileEntry.Sha512Hash);
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Resolving mod URL for {FileName} using Modrinth endpoint {VersionEndpoint}.",
|
||||||
|
fileEntry.FileName,
|
||||||
|
versionEndpoint);
|
||||||
|
|
||||||
using HttpResponseMessage response = await _httpClient.GetAsync(
|
using HttpResponseMessage response = await _httpClient.GetAsync(
|
||||||
new Uri(versionEndpoint, UriKind.Absolute),
|
new Uri(versionEndpoint, UriKind.Absolute),
|
||||||
HttpCompletionOption.ResponseContentRead,
|
HttpCompletionOption.ResponseContentRead,
|
||||||
@@ -161,6 +224,11 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
{
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Mod metadata response for {FileName} from {VersionEndpoint} was empty.",
|
||||||
|
fileEntry.FileName,
|
||||||
|
versionEndpoint);
|
||||||
|
|
||||||
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' was empty.");
|
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' was empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +237,10 @@ namespace AlayaCore.Services
|
|||||||
JArray? filesArray = jsonObject["files"] as JArray;
|
JArray? filesArray = jsonObject["files"] as JArray;
|
||||||
if (filesArray == null || filesArray.Count == 0)
|
if (filesArray == null || filesArray.Count == 0)
|
||||||
{
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Mod metadata response for {FileName} did not contain any files.",
|
||||||
|
fileEntry.FileName);
|
||||||
|
|
||||||
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain any files.");
|
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain any files.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,12 +251,20 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (selectedFile == null)
|
if (selectedFile == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Mod metadata response for {FileName} did not contain a usable file entry.",
|
||||||
|
fileEntry.FileName);
|
||||||
|
|
||||||
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain a usable file entry.");
|
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain a usable file entry.");
|
||||||
}
|
}
|
||||||
|
|
||||||
JObject? hashesObject = selectedFile["hashes"] as JObject;
|
JObject? hashesObject = selectedFile["hashes"] as JObject;
|
||||||
if (hashesObject == null)
|
if (hashesObject == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Mod metadata response for {FileName} did not contain a hashes object.",
|
||||||
|
fileEntry.FileName);
|
||||||
|
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"The mod metadata response for '{fileEntry.FileName}' did not contain a hashes object.");
|
$"The mod metadata response for '{fileEntry.FileName}' did not contain a hashes object.");
|
||||||
}
|
}
|
||||||
@@ -192,12 +272,22 @@ namespace AlayaCore.Services
|
|||||||
string? remoteSha512Hash = hashesObject.Value<string>("sha512");
|
string? remoteSha512Hash = hashesObject.Value<string>("sha512");
|
||||||
if (string.IsNullOrWhiteSpace(remoteSha512Hash))
|
if (string.IsNullOrWhiteSpace(remoteSha512Hash))
|
||||||
{
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Mod metadata response for {FileName} did not contain a valid SHA-512 hash.",
|
||||||
|
fileEntry.FileName);
|
||||||
|
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid SHA-512 hash.");
|
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid SHA-512 hash.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.Equals(remoteSha512Hash, fileEntry.Sha512Hash, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(remoteSha512Hash, fileEntry.Sha512Hash, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Mod metadata hash mismatch for {FileName}. Remote SHA-512: {RemoteHash}, Required SHA-512: {RequiredHash}",
|
||||||
|
fileEntry.FileName,
|
||||||
|
remoteSha512Hash,
|
||||||
|
fileEntry.Sha512Hash);
|
||||||
|
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"The mod metadata SHA-512 hash for '{fileEntry.FileName}' did not match the required manifest hash.");
|
$"The mod metadata SHA-512 hash for '{fileEntry.FileName}' did not match the required manifest hash.");
|
||||||
}
|
}
|
||||||
@@ -205,12 +295,22 @@ namespace AlayaCore.Services
|
|||||||
long? size = selectedFile.Value<long?>("size");
|
long? size = selectedFile.Value<long?>("size");
|
||||||
if (!size.HasValue || size.Value <= 0)
|
if (!size.HasValue || size.Value <= 0)
|
||||||
{
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Mod metadata response for {FileName} did not contain a valid file size.",
|
||||||
|
fileEntry.FileName);
|
||||||
|
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file size.");
|
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file size.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size.Value != fileEntry.Size)
|
if (size.Value != fileEntry.Size)
|
||||||
{
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Mod metadata size mismatch for {FileName}. Remote Size: {RemoteSize}, Required Size: {RequiredSize}",
|
||||||
|
fileEntry.FileName,
|
||||||
|
size.Value,
|
||||||
|
fileEntry.Size);
|
||||||
|
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"The mod metadata size for '{fileEntry.FileName}' did not match the required manifest size.");
|
$"The mod metadata size for '{fileEntry.FileName}' did not match the required manifest size.");
|
||||||
}
|
}
|
||||||
@@ -218,16 +318,30 @@ namespace AlayaCore.Services
|
|||||||
string? modUrl = selectedFile.Value<string>("url");
|
string? modUrl = selectedFile.Value<string>("url");
|
||||||
if (string.IsNullOrWhiteSpace(modUrl))
|
if (string.IsNullOrWhiteSpace(modUrl))
|
||||||
{
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Mod metadata response for {FileName} did not contain a valid file URL.",
|
||||||
|
fileEntry.FileName);
|
||||||
|
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file URL.");
|
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file URL.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Uri.TryCreate(modUrl, UriKind.Absolute, out Uri? result))
|
if (!Uri.TryCreate(modUrl, UriKind.Absolute, out Uri? result))
|
||||||
{
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Mod metadata response for {FileName} contained an invalid file URL: {ModUrl}",
|
||||||
|
fileEntry.FileName,
|
||||||
|
modUrl);
|
||||||
|
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"The mod metadata response for '{fileEntry.FileName}' contained an invalid file URL.");
|
$"The mod metadata response for '{fileEntry.FileName}' contained an invalid file URL.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Resolved download URL for mod {FileName} to {ModUrl}.",
|
||||||
|
fileEntry.FileName,
|
||||||
|
result);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,26 +366,37 @@ namespace AlayaCore.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
string modsDirectory = GetModsDirectoryPath();
|
string modsDirectory = GetModsDirectoryPath();
|
||||||
|
string destinationPath = Path.Combine(modsDirectory, fileEntry.FileName);
|
||||||
|
|
||||||
return Path.Combine(modsDirectory, fileEntry.FileName);
|
_logger.LogDebug(
|
||||||
|
"Resolved destination path for mod {FileName} to {DestinationPath}.",
|
||||||
|
fileEntry.FileName,
|
||||||
|
destinationPath);
|
||||||
|
|
||||||
|
return destinationPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetModsDirectoryPath()
|
private string GetModsDirectoryPath()
|
||||||
{
|
{
|
||||||
return _fileStore.GetOrCreate(FolderLocation.Mods);
|
string modsDirectory = _fileStore.GetOrCreate(FolderLocation.Mods);
|
||||||
|
_logger.LogDebug("Resolved mods directory to {ModsDirectory}.", modsDirectory);
|
||||||
|
return modsDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveStaleMods(IEnumerable<ModFileEntry> requiredMods)
|
private void RemoveStaleMods(
|
||||||
|
IEnumerable<ModFileEntry> requiredMods,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (requiredMods == null)
|
if (requiredMods == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(requiredMods));
|
throw new ArgumentNullException(nameof(requiredMods));
|
||||||
}
|
}
|
||||||
|
|
||||||
string modsDirectory = GetModsDirectoryPath();
|
string modsDirectory = _fileStore.Get(FolderLocation.Mods);
|
||||||
|
|
||||||
if (!Directory.Exists(modsDirectory))
|
if (!Directory.Exists(modsDirectory))
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Mods directory {ModsDirectory} does not exist. No stale mods need removal.", modsDirectory);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,10 +407,17 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
foreach (string filePath in Directory.GetFiles(modsDirectory))
|
foreach (string filePath in Directory.GetFiles(modsDirectory))
|
||||||
{
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
string fileName = Path.GetFileName(filePath);
|
string fileName = Path.GetFileName(filePath);
|
||||||
|
|
||||||
if (!requiredFileNames.Contains(fileName))
|
if (!requiredFileNames.Contains(fileName))
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Removing stale mod file {FileName} at {FilePath}.",
|
||||||
|
fileName,
|
||||||
|
filePath);
|
||||||
|
|
||||||
File.Delete(filePath);
|
File.Delete(filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,14 +444,25 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
string json = JsonConvert.SerializeObject(dto, Formatting.Indented);
|
string json = JsonConvert.SerializeObject(dto, Formatting.Indented);
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Writing installed mods manifest to temporary path {TemporaryManifestPath}. EntryCount: {EntryCount}",
|
||||||
|
temporaryManifestPath,
|
||||||
|
entries.Count);
|
||||||
|
|
||||||
await File.WriteAllTextAsync(temporaryManifestPath, json, cancellationToken).ConfigureAwait(false);
|
await File.WriteAllTextAsync(temporaryManifestPath, json, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (File.Exists(manifestPath))
|
if (File.Exists(manifestPath))
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Deleting previous installed mods manifest at {ManifestPath}.", manifestPath);
|
||||||
File.Delete(manifestPath);
|
File.Delete(manifestPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
File.Move(temporaryManifestPath, manifestPath);
|
File.Move(temporaryManifestPath, manifestPath);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Installed mods manifest updated successfully at {ManifestPath}. EntryCount: {EntryCount}",
|
||||||
|
manifestPath,
|
||||||
|
entries.Count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ using AlayaCore.Abstractions.Interfaces;
|
|||||||
using AlayaCore.Abstractions.Interfaces.Services;
|
using AlayaCore.Abstractions.Interfaces.Services;
|
||||||
using AlayaCore.Models.Configuration;
|
using AlayaCore.Models.Configuration;
|
||||||
using AlayaCore.Utilities.Enums;
|
using AlayaCore.Utilities.Enums;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace AlayaCore.Services
|
namespace AlayaCore.Services
|
||||||
@@ -14,6 +15,7 @@ namespace AlayaCore.Services
|
|||||||
public sealed class SettingsService : ISettingsService
|
public sealed class SettingsService : ISettingsService
|
||||||
{
|
{
|
||||||
private readonly IFileStore _fileStore;
|
private readonly IFileStore _fileStore;
|
||||||
|
private readonly ILogger<SettingsService> _logger;
|
||||||
|
|
||||||
public LauncherOptions LauncherOptions { get; }
|
public LauncherOptions LauncherOptions { get; }
|
||||||
public GameOptions GameOptions { get; }
|
public GameOptions GameOptions { get; }
|
||||||
@@ -21,84 +23,326 @@ namespace AlayaCore.Services
|
|||||||
public SettingsService(
|
public SettingsService(
|
||||||
LauncherOptions launcherOptions,
|
LauncherOptions launcherOptions,
|
||||||
GameOptions gameOptions,
|
GameOptions gameOptions,
|
||||||
IFileStore fileStore)
|
IFileStore fileStore,
|
||||||
|
ILogger<SettingsService> logger)
|
||||||
{
|
{
|
||||||
LauncherOptions = launcherOptions ?? throw new ArgumentNullException(nameof(launcherOptions));
|
LauncherOptions = launcherOptions ?? throw new ArgumentNullException(nameof(launcherOptions));
|
||||||
GameOptions = gameOptions ?? throw new ArgumentNullException(nameof(gameOptions));
|
GameOptions = gameOptions ?? throw new ArgumentNullException(nameof(gameOptions));
|
||||||
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetForceReinstallAsync(bool value, CancellationToken cancellationToken = default)
|
public async Task SetForceReinstallAsync(bool value, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Updating launcher option ForceReinstall to {ForceReinstall}.", value);
|
||||||
|
|
||||||
LauncherOptions.ForceReinstall = value;
|
LauncherOptions.ForceReinstall = value;
|
||||||
|
|
||||||
await SaveLauncherOptionsAsync(cancellationToken).ConfigureAwait(false);
|
await SaveLauncherOptionsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Launcher option ForceReinstall was updated and saved successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateLaunchVersionAsync(string newVersion, CancellationToken cancellationToken = default)
|
public async Task UpdateLaunchVersionAsync(string newVersion, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
GameOptions.LaunchVersion = newVersion ?? string.Empty;
|
string? normalizedVersion = string.IsNullOrWhiteSpace(newVersion)
|
||||||
|
? null
|
||||||
|
: newVersion.Trim();
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Updating game option LaunchVersion from {CurrentLaunchVersion} to {NewLaunchVersion}.",
|
||||||
|
GameOptions.LaunchVersion,
|
||||||
|
normalizedVersion);
|
||||||
|
|
||||||
|
GameOptions.LaunchVersion = normalizedVersion;
|
||||||
|
|
||||||
await SaveGameOptionsAsync(cancellationToken).ConfigureAwait(false);
|
await SaveGameOptionsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Game option LaunchVersion was updated and saved successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetMinimumRamMbAsync(int minimumRamMb, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (minimumRamMb <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(minimumRamMb), "Minimum RAM must be greater than zero.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GameOptions.MaximumRamMb > 0 && minimumRamMb > GameOptions.MaximumRamMb)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Minimum RAM cannot be greater than maximum RAM.", nameof(minimumRamMb));
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Updating game option MinimumRamMb from {CurrentMinimumRamMb} to {NewMinimumRamMb}.",
|
||||||
|
GameOptions.MinimumRamMb,
|
||||||
|
minimumRamMb);
|
||||||
|
|
||||||
|
GameOptions.MinimumRamMb = minimumRamMb;
|
||||||
|
|
||||||
|
await SaveGameOptionsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Game option MinimumRamMb was updated and saved successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetMaximumRamMbAsync(int maximumRamMb, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (maximumRamMb <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(maximumRamMb), "Maximum RAM must be greater than zero.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GameOptions.MinimumRamMb > 0 && maximumRamMb < GameOptions.MinimumRamMb)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Maximum RAM cannot be less than minimum RAM.", nameof(maximumRamMb));
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Updating game option MaximumRamMb from {CurrentMaximumRamMb} to {NewMaximumRamMb}.",
|
||||||
|
GameOptions.MaximumRamMb,
|
||||||
|
maximumRamMb);
|
||||||
|
|
||||||
|
GameOptions.MaximumRamMb = maximumRamMb;
|
||||||
|
|
||||||
|
await SaveGameOptionsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Game option MaximumRamMb was updated and saved successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetResolutionAsync(int screenWidth, int screenHeight, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (screenWidth <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(screenWidth), "Screen width must be greater than zero.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screenHeight <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(screenHeight), "Screen height must be greater than zero.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Updating game resolution from {CurrentWidth}x{CurrentHeight} to {NewWidth}x{NewHeight}.",
|
||||||
|
GameOptions.ScreenWidth,
|
||||||
|
GameOptions.ScreenHeight,
|
||||||
|
screenWidth,
|
||||||
|
screenHeight);
|
||||||
|
|
||||||
|
GameOptions.ScreenWidth = screenWidth;
|
||||||
|
GameOptions.ScreenHeight = screenHeight;
|
||||||
|
|
||||||
|
await SaveGameOptionsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Game resolution was updated and saved successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetFullscreenAsync(bool fullscreen, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Updating game option Fullscreen from {CurrentFullscreen} to {NewFullscreen}.",
|
||||||
|
GameOptions.Fullscreen,
|
||||||
|
fullscreen);
|
||||||
|
|
||||||
|
GameOptions.Fullscreen = fullscreen;
|
||||||
|
|
||||||
|
await SaveGameOptionsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Game option Fullscreen was updated and saved successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveLauncherOptionsAsync(CancellationToken cancellationToken = default)
|
public async Task SaveLauncherOptionsAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Saving launcher options to disk.");
|
||||||
|
|
||||||
await SaveAsync(
|
await SaveAsync(
|
||||||
LauncherOptions.FileName,
|
LauncherOptions.FileName,
|
||||||
LauncherOptions,
|
LauncherOptions,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Launcher options were saved successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadLauncherOptionsAsync(CancellationToken cancellationToken = default)
|
public async Task LoadLauncherOptionsAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Loading launcher options from disk.");
|
||||||
|
|
||||||
LauncherOptions? loadedOptions = await LoadAsync<LauncherOptions>(
|
LauncherOptions? loadedOptions = await LoadAsync<LauncherOptions>(
|
||||||
LauncherOptions.FileName,
|
LauncherOptions.FileName,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (loadedOptions == null)
|
if (loadedOptions == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("No launcher options file was found or it was empty. Existing in-memory launcher options will be kept.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LauncherOptions.ForceReinstall = loadedOptions.ForceReinstall;
|
LauncherOptions.ForceReinstall = loadedOptions.ForceReinstall;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Launcher options were loaded successfully. ForceReinstall: {ForceReinstall}",
|
||||||
|
LauncherOptions.ForceReinstall);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveGameOptionsAsync(CancellationToken cancellationToken = default)
|
public async Task SaveGameOptionsAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
ValidateGameOptions(GameOptions);
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Saving game options to disk. LaunchVersion: {LaunchVersion}, MinimumRamMb: {MinimumRamMb}, MaximumRamMb: {MaximumRamMb}, Resolution: {ScreenWidth}x{ScreenHeight}, Fullscreen: {Fullscreen}",
|
||||||
|
GameOptions.LaunchVersion,
|
||||||
|
GameOptions.MinimumRamMb,
|
||||||
|
GameOptions.MaximumRamMb,
|
||||||
|
GameOptions.ScreenWidth,
|
||||||
|
GameOptions.ScreenHeight,
|
||||||
|
GameOptions.Fullscreen);
|
||||||
|
|
||||||
await SaveAsync(
|
await SaveAsync(
|
||||||
GameOptions.FileName,
|
GameOptions.FileName,
|
||||||
GameOptions,
|
GameOptions,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Game options were saved successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadGameOptionsAsync(CancellationToken cancellationToken = default)
|
public async Task LoadGameOptionsAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Loading game options from disk.");
|
||||||
|
|
||||||
GameOptions? loadedOptions = await LoadAsync<GameOptions>(
|
GameOptions? loadedOptions = await LoadAsync<GameOptions>(
|
||||||
GameOptions.FileName,
|
GameOptions.FileName,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (loadedOptions == null)
|
if (loadedOptions == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("No game options file was found or it was empty. Applying default game options.");
|
||||||
|
ApplyGameOptions(GameOptions.Default);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GameOptions.LaunchVersion = loadedOptions.LaunchVersion;
|
ApplyGameOptions(loadedOptions);
|
||||||
GameOptions.MinimumRamMB = loadedOptions.MinimumRamMB;
|
|
||||||
GameOptions.MaximumRamMB = loadedOptions.MaximumRamMB;
|
_logger.LogInformation(
|
||||||
GameOptions.ScreenWidth = loadedOptions.ScreenWidth;
|
"Game options were loaded successfully. LaunchVersion: {LaunchVersion}, MinimumRamMb: {MinimumRamMb}, MaximumRamMb: {MaximumRamMb}, Resolution: {ScreenWidth}x{ScreenHeight}, Fullscreen: {Fullscreen}",
|
||||||
GameOptions.ScreenHeight = loadedOptions.ScreenHeight;
|
GameOptions.LaunchVersion,
|
||||||
GameOptions.Fullscreen = loadedOptions.Fullscreen;
|
GameOptions.MinimumRamMb,
|
||||||
|
GameOptions.MaximumRamMb,
|
||||||
|
GameOptions.ScreenWidth,
|
||||||
|
GameOptions.ScreenHeight,
|
||||||
|
GameOptions.Fullscreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadAllAsync(CancellationToken cancellationToken = default)
|
public async Task LoadAllAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Loading all settings from disk.");
|
||||||
|
|
||||||
await LoadLauncherOptionsAsync(cancellationToken).ConfigureAwait(false);
|
await LoadLauncherOptionsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
await LoadGameOptionsAsync(cancellationToken).ConfigureAwait(false);
|
await LoadGameOptionsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("All settings were loaded successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveAllAsync(CancellationToken cancellationToken = default)
|
public async Task SaveAllAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Saving all settings to disk.");
|
||||||
|
|
||||||
await SaveLauncherOptionsAsync(cancellationToken).ConfigureAwait(false);
|
await SaveLauncherOptionsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
await SaveGameOptionsAsync(cancellationToken).ConfigureAwait(false);
|
await SaveGameOptionsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("All settings were saved successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyGameOptions(GameOptions source)
|
||||||
|
{
|
||||||
|
if (source == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Applying game options from source. LaunchVersion: {LaunchVersion}, MinimumRamMb: {MinimumRamMb}, MaximumRamMb: {MaximumRamMb}, Resolution: {ScreenWidth}x{ScreenHeight}, Fullscreen: {Fullscreen}",
|
||||||
|
source.LaunchVersion,
|
||||||
|
source.MinimumRamMb,
|
||||||
|
source.MaximumRamMb,
|
||||||
|
source.ScreenWidth,
|
||||||
|
source.ScreenHeight,
|
||||||
|
source.Fullscreen);
|
||||||
|
|
||||||
|
GameOptions.LaunchVersion = string.IsNullOrWhiteSpace(source.LaunchVersion)
|
||||||
|
? null
|
||||||
|
: source.LaunchVersion.Trim();
|
||||||
|
|
||||||
|
GameOptions.MinimumRamMb = source.MinimumRamMb > 0
|
||||||
|
? source.MinimumRamMb
|
||||||
|
: GameOptions.Default.MinimumRamMb;
|
||||||
|
|
||||||
|
GameOptions.MaximumRamMb = source.MaximumRamMb > 0
|
||||||
|
? source.MaximumRamMb
|
||||||
|
: GameOptions.Default.MaximumRamMb;
|
||||||
|
|
||||||
|
GameOptions.ScreenWidth = source.ScreenWidth > 0
|
||||||
|
? source.ScreenWidth
|
||||||
|
: GameOptions.Default.ScreenWidth;
|
||||||
|
|
||||||
|
GameOptions.ScreenHeight = source.ScreenHeight > 0
|
||||||
|
? source.ScreenHeight
|
||||||
|
: GameOptions.Default.ScreenHeight;
|
||||||
|
|
||||||
|
GameOptions.Fullscreen = source.Fullscreen;
|
||||||
|
|
||||||
|
ValidateGameOptions(GameOptions);
|
||||||
|
|
||||||
|
_logger.LogDebug("Game options were applied successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateGameOptions(GameOptions options)
|
||||||
|
{
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Validating game options. MinimumRamMb: {MinimumRamMb}, MaximumRamMb: {MaximumRamMb}, ScreenWidth: {ScreenWidth}, ScreenHeight: {ScreenHeight}",
|
||||||
|
options.MinimumRamMb,
|
||||||
|
options.MaximumRamMb,
|
||||||
|
options.ScreenWidth,
|
||||||
|
options.ScreenHeight);
|
||||||
|
|
||||||
|
if (options.MinimumRamMb <= 0)
|
||||||
|
{
|
||||||
|
_logger.LogError("Game options validation failed because MinimumRamMb was {MinimumRamMb}.", options.MinimumRamMb);
|
||||||
|
throw new InvalidDataException("Minimum RAM must be greater than zero.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.MaximumRamMb <= 0)
|
||||||
|
{
|
||||||
|
_logger.LogError("Game options validation failed because MaximumRamMb was {MaximumRamMb}.", options.MaximumRamMb);
|
||||||
|
throw new InvalidDataException("Maximum RAM must be greater than zero.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.MinimumRamMb > options.MaximumRamMb)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Game options validation failed because MinimumRamMb {MinimumRamMb} was greater than MaximumRamMb {MaximumRamMb}.",
|
||||||
|
options.MinimumRamMb,
|
||||||
|
options.MaximumRamMb);
|
||||||
|
|
||||||
|
throw new InvalidDataException("Minimum RAM cannot be greater than maximum RAM.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.ScreenWidth <= 0)
|
||||||
|
{
|
||||||
|
_logger.LogError("Game options validation failed because ScreenWidth was {ScreenWidth}.", options.ScreenWidth);
|
||||||
|
throw new InvalidDataException("Screen width must be greater than zero.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.ScreenHeight <= 0)
|
||||||
|
{
|
||||||
|
_logger.LogError("Game options validation failed because ScreenHeight was {ScreenHeight}.", options.ScreenHeight);
|
||||||
|
throw new InvalidDataException("Screen height must be greater than zero.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Game options validation completed successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveAsync<T>(
|
private async Task SaveAsync<T>(
|
||||||
@@ -123,6 +367,7 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(directoryPath))
|
if (string.IsNullOrWhiteSpace(directoryPath))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Could not resolve the settings directory path for file {FileName}.", fileName);
|
||||||
throw new InvalidOperationException("Could not resolve the settings directory path.");
|
throw new InvalidOperationException("Could not resolve the settings directory path.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,14 +376,23 @@ namespace AlayaCore.Services
|
|||||||
string temporaryPath = fullPath + ".tmp";
|
string temporaryPath = fullPath + ".tmp";
|
||||||
string json = JsonConvert.SerializeObject(value, Formatting.Indented);
|
string json = JsonConvert.SerializeObject(value, Formatting.Indented);
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Writing settings file {FileName} to temporary path {TemporaryPath} before replacing {FullPath}.",
|
||||||
|
fileName,
|
||||||
|
temporaryPath,
|
||||||
|
fullPath);
|
||||||
|
|
||||||
await File.WriteAllTextAsync(temporaryPath, json, cancellationToken).ConfigureAwait(false);
|
await File.WriteAllTextAsync(temporaryPath, json, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (File.Exists(fullPath))
|
if (File.Exists(fullPath))
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Deleting existing settings file at {FullPath}.", fullPath);
|
||||||
File.Delete(fullPath);
|
File.Delete(fullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
File.Move(temporaryPath, fullPath);
|
File.Move(temporaryPath, fullPath);
|
||||||
|
|
||||||
|
_logger.LogInformation("Settings file {FileName} was saved successfully to {FullPath}.", fileName, fullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<T?> LoadAsync<T>(
|
private async Task<T?> LoadAsync<T>(
|
||||||
@@ -156,22 +410,38 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (!File.Exists(fullPath))
|
if (!File.Exists(fullPath))
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Settings file {FileName} was not found at {FullPath}.", fileName, fullPath);
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Loading settings file {FileName} from {FullPath}.", fileName, fullPath);
|
||||||
|
|
||||||
string json = await File.ReadAllTextAsync(fullPath, cancellationToken).ConfigureAwait(false);
|
string json = await File.ReadAllTextAsync(fullPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning("Settings file {FileName} at {FullPath} was empty.", fileName, fullPath);
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return JsonConvert.DeserializeObject<T>(json);
|
T? result = JsonConvert.DeserializeObject<T>(json);
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Deserializing settings file {FileName} at {FullPath} returned null.", fileName, fullPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Settings file {FileName} was deserialized successfully.", fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
|
_logger.LogError(ex, "Failed to deserialize settings file {FileName} at {FullPath} to {TypeName}.", fileName, fullPath, typeof(T).Name);
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"Failed to deserialize settings file '{fullPath}' to {typeof(T).Name}.",
|
$"Failed to deserialize settings file '{fullPath}' to {typeof(T).Name}.",
|
||||||
ex);
|
ex);
|
||||||
@@ -180,7 +450,9 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
private string GetFullPath(string fileName)
|
private string GetFullPath(string fileName)
|
||||||
{
|
{
|
||||||
return Path.Combine(_fileStore.GetOrCreate(FolderLocation.Config), fileName);
|
string fullPath = Path.Combine(_fileStore.GetOrCreate(FolderLocation.Config), fileName);
|
||||||
|
_logger.LogDebug("Resolved settings file path for {FileName} to {FullPath}.", fileName, fullPath);
|
||||||
|
return fullPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ namespace AlayaCore.States
|
|||||||
{
|
{
|
||||||
public enum LaunchState
|
public enum LaunchState
|
||||||
{
|
{
|
||||||
|
Checking,
|
||||||
Ready,
|
Ready,
|
||||||
LauncherNeedsUpdate,
|
LauncherNeedsUpdate,
|
||||||
NeedAuthenticating,
|
NeedAuthenticating,
|
||||||
|
|||||||
Reference in New Issue
Block a user