Compare commits
12 Commits
9596d15d94
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6938635ee4 | |||
| 491a3d420d | |||
| 11863088e4 | |||
| 8a8292d2c3 | |||
| 8521f695a9 | |||
| 0415d6ad80 | |||
| 7b66d8ceac | |||
| c148672c08 | |||
| 823ccf4b87 | |||
| ef135b1164 | |||
| c48f670eab | |||
| 3298299764 |
6
.idea/.idea.AlayaCore/.idea/vcs.xml
generated
Normal file
6
.idea/.idea.AlayaCore/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -1,3 +1,26 @@
|
|||||||
<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_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_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_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_003AJELoginHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2dce88ad0d604d86b0d410923cb59f4bb200_003Fcd_003Ffe209c30_003FJELoginHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMinecraftJavaPathResolver_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003F5f_003F3f4c19e9_003FMinecraftJavaPathResolver_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMinecraftLauncherParameters_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003F67_003Fd3bb8531_003FMinecraftLauncherParameters_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMinecraftLauncher_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003F93_003F1a552b3b_003FMinecraftLauncher_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMinecraftPath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F75bf69b9634b4f328866dbcd8b90304a2600_003F3c_003Fef2111c9_003FMinecraftPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMinecraftProcessBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003Fc0_003F15c03583_003FMinecraftProcessBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMsalClientHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fee6114ec878e41eeb911687986e3a3fd6600_003F4b_003F0b7d25f6_003FMsalClientHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMSession_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F75bf69b9634b4f328866dbcd8b90304a2600_003Fa0_003F9cf699df_003FMSession_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANeoForgeInstaller_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F16599bff24b948d2b1e1d928222947e1de00_003F39_003F21a3bcfd_003FNeoForgeInstaller_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANeoForgeInstallOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F16599bff24b948d2b1e1d928222947e1de00_003Fa3_003F837206c4_003FNeoForgeInstallOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOperatingSystem_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb5d933e666c84a3394cfc49363e3e5bdd2b08_003Fb9_003Fd737a5e2_003FOperatingSystem_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOperatingSystem_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb5d933e666c84a3394cfc49363e3e5bdd2b08_003Fb9_003Fd737a5e2_003FOperatingSystem_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AVersionConverter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003F346166506159999f4fec5f7c475ba964d2495ee825dd6e4c48dedef117f086_003FVersionConverter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AProcessWrapper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003F97_003F3a1ed5f7_003FProcessWrapper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APublicClientApplicationBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fe7dcae430a36a6030304d2dec7149bc9e40cb379e3a8e0efb762d5d19da5c_003FPublicClientApplicationBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARandom_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb5d933e666c84a3394cfc49363e3e5bdd2b08_003F5f_003F5739d1f6_003FRandom_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AVersionConverter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003F346166506159999f4fec5f7c475ba964d2495ee825dd6e4c48dedef117f086_003FVersionConverter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AVersionMetadataCollection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003Ff7_003Fb6ecd842_003FVersionMetadataCollection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
||||||
7
AlayaCore/Abstractions/Configuration/BaseConfig.cs
Normal file
7
AlayaCore/Abstractions/Configuration/BaseConfig.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace AlayaCore.Abstractions.Configuration
|
||||||
|
{
|
||||||
|
public abstract class BaseConfig
|
||||||
|
{
|
||||||
|
public abstract string FileName { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
12
AlayaCore/Abstractions/Interfaces/IFileStore.cs
Normal file
12
AlayaCore/Abstractions/Interfaces/IFileStore.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using AlayaCore.Utilities.Enums;
|
||||||
|
|
||||||
|
namespace AlayaCore.Abstractions.Interfaces
|
||||||
|
{
|
||||||
|
public interface IFileStore
|
||||||
|
{
|
||||||
|
string Get(FolderLocation location);
|
||||||
|
string GetOrCreate(FolderLocation location);
|
||||||
|
string Combine(FolderLocation location, params string[] paths);
|
||||||
|
bool Exists(FolderLocation location);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,27 @@
|
|||||||
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Models.Progress;
|
||||||
|
using AlayaCore.Models.Results;
|
||||||
|
using AlayaCore.States;
|
||||||
|
|
||||||
namespace AlayaCore.Abstractions.Interfaces
|
namespace AlayaCore.Abstractions.Interfaces
|
||||||
{
|
{
|
||||||
public interface ILaunchDirector
|
public interface ILaunchDirector
|
||||||
{
|
{
|
||||||
Task RunAsync(CancellationToken cancellationToken = default);
|
bool CanRun { get; }
|
||||||
|
bool NeedsUpdating { get; }
|
||||||
|
|
||||||
|
bool IsUpdatingLauncher { 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
19
AlayaCore/Abstractions/Interfaces/Policies/IRetryPolicy.cs
Normal file
19
AlayaCore/Abstractions/Interfaces/Policies/IRetryPolicy.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AlayaCore.Abstractions.Interfaces.Policies
|
||||||
|
{
|
||||||
|
public interface IRetryPolicy
|
||||||
|
{
|
||||||
|
Task ExecuteAsync(
|
||||||
|
Func<CancellationToken, Task> operation,
|
||||||
|
string operationName,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<T> ExecuteAsync<T>(
|
||||||
|
Func<CancellationToken, Task<T>> operation,
|
||||||
|
string operationName,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
AlayaCore/Abstractions/Interfaces/Services/IAuthService.cs
Normal file
14
AlayaCore/Abstractions/Interfaces/Services/IAuthService.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CmlLib.Core.Auth;
|
||||||
|
|
||||||
|
namespace AlayaCore.Abstractions.Interfaces.Services
|
||||||
|
{
|
||||||
|
public interface IAuthService
|
||||||
|
{
|
||||||
|
Task<bool> IsAuthenticatedAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task AuthenticateAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task SignOutAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<MSession> GetSessionAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Installation;
|
||||||
|
using AlayaCore.Models.Manifests;
|
||||||
|
using CmlLib.Core;
|
||||||
|
using CmlLib.Core.Installers;
|
||||||
|
|
||||||
|
namespace AlayaCore.Abstractions.Interfaces.Services
|
||||||
|
{
|
||||||
|
public interface IGameInstallService
|
||||||
|
{
|
||||||
|
|
||||||
|
Task EnsureMinecraftInstalledAsync(
|
||||||
|
ManifestModel manifest,
|
||||||
|
InstallEnvironment environment,
|
||||||
|
CancellationToken cancellationToken = default,
|
||||||
|
EventHandler<InstallerProgressChangedEventArgs>? minecraftProgess = null,
|
||||||
|
EventHandler<ByteProgress>? byteProgress = null);
|
||||||
|
|
||||||
|
Task EnsureNeoForgeInstalledAsync(
|
||||||
|
ManifestModel manifest,
|
||||||
|
InstallEnvironment environment,
|
||||||
|
CancellationToken cancellationToken = default,
|
||||||
|
IProgress<InstallerProgressChangedEventArgs>? progress = null,
|
||||||
|
IProgress<ByteProgress>? byteProgress = null);
|
||||||
|
|
||||||
|
Task VerifyFilesAsync(
|
||||||
|
ManifestModel manifest,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,9 @@ using AlayaCore.Models.Manifests;
|
|||||||
|
|
||||||
namespace AlayaCore.Abstractions.Interfaces.Services
|
namespace AlayaCore.Abstractions.Interfaces.Services
|
||||||
{
|
{
|
||||||
public interface IJavaService
|
public interface IGameLaunchService
|
||||||
{
|
{
|
||||||
Task EnsureValidJavaInstalledAsync(
|
Task LaunchAsync(
|
||||||
ManifestModel manifest,
|
ManifestModel manifest,
|
||||||
InstallEnvironment environment,
|
InstallEnvironment environment,
|
||||||
CancellationToken cancellationToken = default);
|
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);
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ namespace AlayaCore.Abstractions.Interfaces.Services
|
|||||||
|
|
||||||
Task<LauncherManifestModel?> GetLocalLauncherManifestAsync(CancellationToken cancellationToken = default);
|
Task<LauncherManifestModel?> GetLocalLauncherManifestAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task<string> GetRemoteLauncherManifestHashAsync(CancellationToken cancellationToken = default);
|
Task<LauncherManifestModel> GetRemoteLauncherManifestAsync(CancellationToken cancellationToken = default);
|
||||||
Task<Version> GetRemoteCoreManifestVersionAsync(CancellationToken cancellationToken = default);
|
Task<Version> GetRemoteCoreManifestVersionAsync(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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Models.Configuration;
|
||||||
|
|
||||||
|
namespace AlayaCore.Abstractions.Interfaces.Services
|
||||||
|
{
|
||||||
|
public interface ISettingsService
|
||||||
|
{
|
||||||
|
LauncherOptions LauncherOptions { get; }
|
||||||
|
GameOptions GameOptions { get; }
|
||||||
|
|
||||||
|
Task SetForceReinstallAsync(bool value, 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 LoadLauncherOptionsAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task SaveGameOptionsAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task LoadGameOptionsAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task LoadAllAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task SaveAllAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CmlLib.Core" Version="4.0.6" />
|
||||||
|
<PackageReference Include="CmlLib.Core.Auth.Microsoft" Version="3.3.1" />
|
||||||
|
<PackageReference Include="CmlLib.Core.Installer.NeoForge" Version="4.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||||
|
<PackageReference Include="XboxAuthNet.Game.Msal" Version="0.1.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
58
AlayaCore/Clients/DefaultHttpClient.cs
Normal file
58
AlayaCore/Clients/DefaultHttpClient.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Abstractions.Interfaces.Clients;
|
||||||
|
|
||||||
|
namespace AlayaCore.Clients
|
||||||
|
{
|
||||||
|
public sealed class DefaultHttpClient : IHttpClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public DefaultHttpClient(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<HttpResponseMessage> GetAsync(
|
||||||
|
Uri requestUri,
|
||||||
|
HttpCompletionOption completionOption,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (requestUri == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(requestUri));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requestUri.IsAbsoluteUri)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Request URI must be absolute.", nameof(requestUri));
|
||||||
|
}
|
||||||
|
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
return _httpClient.GetAsync(requestUri, completionOption, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_httpClient.Dispose();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThrowIfDisposed()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
throw new ObjectDisposedException(nameof(DefaultHttpClient));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
AlayaCore/Errors/LauncherError.cs
Normal file
19
AlayaCore/Errors/LauncherError.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using AlayaCore.Utilities.Enums;
|
||||||
|
|
||||||
|
namespace AlayaCore.Errors
|
||||||
|
{
|
||||||
|
public sealed class LauncherError
|
||||||
|
{
|
||||||
|
public LauncherErrorType Type { get; }
|
||||||
|
public string Message { get; }
|
||||||
|
public Exception Exception { get; }
|
||||||
|
|
||||||
|
public LauncherError(LauncherErrorType type, string message, Exception exception)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
Message = message;
|
||||||
|
Exception = exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ namespace AlayaCore.Installation
|
|||||||
{
|
{
|
||||||
public sealed class InstallEnvironment
|
public sealed class InstallEnvironment
|
||||||
{
|
{
|
||||||
public OSPlatform OSPlatform { get; }
|
public OSPlatform OsPlatform { get; }
|
||||||
public bool JavaInstalled { get; }
|
public bool JavaInstalled { get; }
|
||||||
public string? JavaVersion { get; }
|
public string? JavaVersion { get; }
|
||||||
public string? JavaPath { get; }
|
public string? JavaPath { get; }
|
||||||
@@ -42,7 +42,7 @@ namespace AlayaCore.Installation
|
|||||||
nameof(javaPath));
|
nameof(javaPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
OSPlatform = osPlatform;
|
OsPlatform = osPlatform;
|
||||||
JavaInstalled = javaInstalled;
|
JavaInstalled = javaInstalled;
|
||||||
JavaPath = javaInstalled ? javaPath : null;
|
JavaPath = javaInstalled ? javaPath : null;
|
||||||
JavaVersion = javaInstalled ? javaVersion : null;
|
JavaVersion = javaInstalled ? javaVersion : null;
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AlayaCore.Abstractions.Interfaces;
|
using AlayaCore.Abstractions.Interfaces;
|
||||||
using AlayaCore.Abstractions.Interfaces.Services;
|
using AlayaCore.Abstractions.Interfaces.Services;
|
||||||
using AlayaCore.Installation;
|
using AlayaCore.Installation;
|
||||||
|
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.Utilities.Enums;
|
||||||
|
using CmlLib.Core;
|
||||||
|
using CmlLib.Core.Installers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace AlayaCore
|
namespace AlayaCore
|
||||||
{
|
{
|
||||||
@@ -14,106 +23,549 @@ namespace AlayaCore
|
|||||||
private readonly IManifestService _manifestService;
|
private readonly IManifestService _manifestService;
|
||||||
private readonly IUpdateService _updateService;
|
private readonly IUpdateService _updateService;
|
||||||
private readonly IInstallStateService _installStateService;
|
private readonly IInstallStateService _installStateService;
|
||||||
private readonly IJavaService _javaService;
|
|
||||||
private readonly IModService _modService;
|
private readonly IModService _modService;
|
||||||
|
private readonly IGameLaunchService _gameLaunchService;
|
||||||
|
private readonly IGameInstallService _gameInstallService;
|
||||||
|
private readonly ISettingsService _settingsService;
|
||||||
|
private readonly IAuthService _authService;
|
||||||
|
private readonly LauncherOptions _options;
|
||||||
|
private readonly ILogger<LaunchDirector> _logger;
|
||||||
|
|
||||||
|
public bool CanRun { get; private set; }
|
||||||
|
public bool NeedsUpdating { 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,
|
||||||
IInstallStateService installStateService,
|
IInstallStateService installStateService,
|
||||||
IJavaService javaService,
|
IModService modService,
|
||||||
IModService modService)
|
IGameLaunchService gameLaunchService,
|
||||||
|
IGameInstallService gameInstallService,
|
||||||
|
ISettingsService settingsService,
|
||||||
|
IAuthService authService,
|
||||||
|
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));
|
||||||
_installStateService = installStateService ?? throw new ArgumentNullException(nameof(installStateService));
|
_installStateService = installStateService ?? throw new ArgumentNullException(nameof(installStateService));
|
||||||
_javaService = javaService ?? throw new ArgumentNullException(nameof(javaService));
|
|
||||||
_modService = modService ?? throw new ArgumentNullException(nameof(modService));
|
_modService = modService ?? throw new ArgumentNullException(nameof(modService));
|
||||||
|
_gameLaunchService = gameLaunchService ?? throw new ArgumentNullException(nameof(gameLaunchService));
|
||||||
|
_gameInstallService = gameInstallService ?? throw new ArgumentNullException(nameof(gameInstallService));
|
||||||
|
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
|
||||||
|
_authService = authService ?? throw new ArgumentNullException(nameof(authService));
|
||||||
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RunAsync(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)
|
||||||
{
|
{
|
||||||
LauncherManifestModel launcherManifest = await _manifestService
|
_logger.LogInformation("Launcher update is required.");
|
||||||
.GetLauncherManifestAsync(cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
await _updateService
|
LaunchPlan launcherUpdatePlan = new LaunchPlan(
|
||||||
.LaunchUpdaterAsync(launcherManifest, cancellationToken)
|
launcherNeedsUpdate: true,
|
||||||
.ConfigureAwait(false);
|
minecraftNeedsInstallOrUpdate: false,
|
||||||
|
neoforgeNeedsInstallOrUpdate: false,
|
||||||
|
modsNeedSync: false,
|
||||||
|
needAuthenticating: false);
|
||||||
|
|
||||||
return;
|
ApplyPlan(launcherUpdatePlan);
|
||||||
|
return launcherUpdatePlan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ManifestModel manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
InstallEnvironment environment = await _installStateService
|
||||||
|
.GetCurrentEnvironmentAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
bool needAuthenticating = !await _authService
|
||||||
|
.IsAuthenticatedAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
bool minecraftNeedsInstallOrUpdate =
|
||||||
|
_options.ForceReinstall ||
|
||||||
|
!environment.JavaInstalled ||
|
||||||
|
!environment.MinecraftInstalled ||
|
||||||
|
!string.Equals(environment.MinecraftVersion, manifest.MinecraftVersion, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
bool neoforgeNeedsInstallOrUpdate =
|
||||||
|
_options.ForceReinstall ||
|
||||||
|
!environment.NeoforgedInstalled ||
|
||||||
|
!string.Equals(environment.NeoforgedVersion, manifest.NeoforgedVersion, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
bool modsNeedSync =
|
||||||
|
_options.ForceReinstall ||
|
||||||
|
DoModsNeedSync(manifest, environment);
|
||||||
|
|
||||||
|
LaunchPlan plan = new LaunchPlan(
|
||||||
|
launcherNeedsUpdate: false,
|
||||||
|
minecraftNeedsInstallOrUpdate: minecraftNeedsInstallOrUpdate,
|
||||||
|
neoforgeNeedsInstallOrUpdate: neoforgeNeedsInstallOrUpdate,
|
||||||
|
modsNeedSync: modsNeedSync,
|
||||||
|
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);
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<InstallOrUpdateResult> InstallOrUpdateAsync(
|
||||||
|
CancellationToken cancellationToken = default,
|
||||||
|
IProgress<LauncherProgress>? progress = null)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
while (!plan.CanRun)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogInformation("Processing launch state {LaunchState}.", plan.State);
|
||||||
|
|
||||||
|
ManifestModel manifest;
|
||||||
|
InstallEnvironment environment;
|
||||||
|
|
||||||
|
switch (plan.State)
|
||||||
|
{
|
||||||
|
case LaunchState.LauncherNeedsUpdate:
|
||||||
|
{
|
||||||
|
IsUpdatingLauncher = true;
|
||||||
|
|
||||||
|
_logger.LogWarning("Launcher updater handoff is beginning.");
|
||||||
|
|
||||||
|
ReportProgress(progress, LaunchState.LauncherNeedsUpdate, "Launching updater...");
|
||||||
|
|
||||||
|
LauncherManifestModel launcherManifest = await _manifestService
|
||||||
|
.GetLauncherManifestAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _updateService
|
||||||
|
.LaunchUpdaterAsync(launcherManifest, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
ApplyPlan(plan);
|
||||||
|
|
||||||
|
_logger.LogInformation("Updater launched successfully. Returning UpdaterLaunched result.");
|
||||||
|
return InstallOrUpdateResult.UpdaterLaunched;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LaunchState.NeedAuthenticating:
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Authentication is required.");
|
||||||
|
|
||||||
|
ReportProgress(progress, LaunchState.NeedAuthenticating, "Signing in...");
|
||||||
|
|
||||||
|
await _authService
|
||||||
|
.AuthenticateAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Authentication completed successfully.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LaunchState.InstallMinecraft:
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Minecraft installation or repair is required.");
|
||||||
|
|
||||||
|
ReportProgress(progress, LaunchState.InstallMinecraft, "Preparing Minecraft installation...");
|
||||||
|
|
||||||
|
manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
environment = await _installStateService
|
||||||
|
.GetCurrentEnvironmentAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
EventHandler<InstallerProgressChangedEventArgs>? minecraftFileProgress =
|
||||||
|
CreateMinecraftFileProgressHandler(progress);
|
||||||
|
|
||||||
|
EventHandler<ByteProgress>? minecraftByteProgress =
|
||||||
|
CreateMinecraftByteProgressHandler(progress);
|
||||||
|
|
||||||
|
await _gameInstallService
|
||||||
|
.EnsureMinecraftInstalledAsync(
|
||||||
|
manifest,
|
||||||
|
environment,
|
||||||
|
cancellationToken,
|
||||||
|
minecraftFileProgress,
|
||||||
|
minecraftByteProgress)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Minecraft installation or repair step completed.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LaunchState.InstallNeoforge:
|
||||||
|
{
|
||||||
|
_logger.LogInformation("NeoForge installation or repair is required.");
|
||||||
|
|
||||||
|
ReportProgress(progress, LaunchState.InstallNeoforge, "Preparing NeoForge installation...");
|
||||||
|
|
||||||
|
manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
environment = await _installStateService
|
||||||
|
.GetCurrentEnvironmentAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
IProgress<InstallerProgressChangedEventArgs>? neoForgeFileProgress =
|
||||||
|
CreateNeoForgeFileProgress(progress);
|
||||||
|
|
||||||
|
IProgress<ByteProgress>? neoForgeByteProgress =
|
||||||
|
CreateNeoForgeByteProgress(progress);
|
||||||
|
|
||||||
|
await _gameInstallService
|
||||||
|
.EnsureNeoForgeInstalledAsync(
|
||||||
|
manifest,
|
||||||
|
environment,
|
||||||
|
cancellationToken,
|
||||||
|
neoForgeFileProgress,
|
||||||
|
neoForgeByteProgress)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("NeoForge installation or repair step completed.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LaunchState.SyncMods:
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Mod synchronization is required.");
|
||||||
|
|
||||||
|
ReportProgress(progress, LaunchState.SyncMods, "Checking mod files...");
|
||||||
|
|
||||||
|
manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
environment = await _installStateService
|
||||||
|
.GetCurrentEnvironmentAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
IProgress<DownloadProgress>? modProgress = CreateModProgress(progress);
|
||||||
|
|
||||||
|
await _modService
|
||||||
|
.ProcessModsAsync(manifest, environment, modProgress, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Mod synchronization step completed.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LaunchState.Ready:
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Launch state is Ready inside install loop.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
_logger.LogError("Unsupported launch state encountered: {LaunchState}.", plan.State);
|
||||||
|
throw new InvalidOperationException($"Unsupported launch state '{plan.State}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plan = await EvaluateAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_options.ForceReinstall)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Force reinstall flag was set. Resetting it after successful install/update workflow.");
|
||||||
|
|
||||||
|
await _settingsService
|
||||||
|
.SetForceReinstallAsync(false, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
plan = await EvaluateAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogInformation("Launch requested.");
|
||||||
|
|
||||||
|
if (CurrentPlan == null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("No current launch plan was available. Evaluating launcher state before launch.");
|
||||||
|
await EvaluateAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
InstallEnvironment environment = await _installStateService
|
||||||
|
.GetCurrentEnvironmentAsync(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
|
||||||
|
.LaunchAsync(manifest, environment, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Game launch call completed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await ProcessManifestAsync(localManifest, cancellationToken).ConfigureAwait(false);
|
_logger.LogDebug("Current Alaya manifest is available. Version: {AlayaVersion}", localManifest.AlayaVersion);
|
||||||
|
return localManifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessManifestAsync(
|
private static bool DoModsNeedSync(ManifestModel manifest, InstallEnvironment environment)
|
||||||
ManifestModel manifest,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
if (manifest == null)
|
if (manifest == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(manifest));
|
throw new ArgumentNullException(nameof(manifest));
|
||||||
}
|
}
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
if (environment == null)
|
||||||
|
|
||||||
InstallEnvironment environment = await _installStateService
|
|
||||||
.GetCurrentEnvironmentAsync(cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!environment.JavaInstalled || environment.JavaVersion != manifest.RequiredJavaVersion)
|
|
||||||
{
|
{
|
||||||
await _javaService
|
throw new ArgumentNullException(nameof(environment));
|
||||||
.EnsureValidJavaInstalledAsync(manifest, environment, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process Minecraft
|
var requiredMods = manifest.Files
|
||||||
|
.Where(file => file.Type == FileType.Mod)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
if (!environment.MinecraftInstalled || environment.MinecraftVersion != manifest.MinecraftVersion)
|
var installedMods = environment.InstalledModsManifest.Mods;
|
||||||
|
|
||||||
|
if (requiredMods.Count != installedMods.Count)
|
||||||
{
|
{
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process Neoforge
|
foreach (ModFileEntry requiredMod in requiredMods)
|
||||||
|
|
||||||
if (!environment.NeoforgedInstalled || environment.NeoforgedVersion != manifest.NeoforgedVersion)
|
|
||||||
{
|
{
|
||||||
|
ModFileEntry? installedMod = installedMods.FirstOrDefault(
|
||||||
|
mod => string.Equals(mod.FileName, requiredMod.FileName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (installedMod == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(installedMod.Sha512Hash, requiredMod.Sha512Hash, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installedMod.Size != requiredMod.Size)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process Mods
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
await _modService.ProcessModsAsync(manifest, environment, cancellationToken).ConfigureAwait(false);
|
private void ApplyPlan(LaunchPlan plan)
|
||||||
|
{
|
||||||
|
CurrentPlan = plan ?? throw new ArgumentNullException(nameof(plan));
|
||||||
|
NeedsUpdating = plan.NeedsUpdating;
|
||||||
|
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));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
27
AlayaCore/Models/AlayaPath.cs
Normal file
27
AlayaCore/Models/AlayaPath.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using AlayaCore.Abstractions.Interfaces;
|
||||||
|
using AlayaCore.Utilities.Enums;
|
||||||
|
using CmlLib.Core;
|
||||||
|
|
||||||
|
namespace AlayaCore.Models
|
||||||
|
{
|
||||||
|
public class AlayaPath : MinecraftPath
|
||||||
|
{
|
||||||
|
public AlayaPath(IFileStore fileStore)
|
||||||
|
{
|
||||||
|
BasePath = NormalizePath(fileStore.Get(FolderLocation.Game));
|
||||||
|
|
||||||
|
Library = NormalizePath(BasePath + "/libraries");
|
||||||
|
Versions = NormalizePath(BasePath + "/versions");
|
||||||
|
Resource = NormalizePath(BasePath + "/resources");
|
||||||
|
|
||||||
|
Runtime = NormalizePath(fileStore.GetOrCreate(FolderLocation.JavaRuntime));
|
||||||
|
Assets = NormalizePath(BasePath + "/assets");
|
||||||
|
|
||||||
|
CreateDirs();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
AlayaCore/Models/Configuration/GameOptions.cs
Normal file
29
AlayaCore/Models/Configuration/GameOptions.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using AlayaCore.Abstractions.Configuration;
|
||||||
|
|
||||||
|
namespace AlayaCore.Models.Configuration
|
||||||
|
{
|
||||||
|
public sealed class GameOptions : BaseConfig
|
||||||
|
{
|
||||||
|
public override string FileName => "Game.json";
|
||||||
|
|
||||||
|
public string? LaunchVersion { get; set; } = null;
|
||||||
|
|
||||||
|
public int MinimumRamMb { get; set; } = 1024;
|
||||||
|
public int MaximumRamMb { get; set; } = 2048;
|
||||||
|
|
||||||
|
public int ScreenWidth { get; set; } = 1920;
|
||||||
|
public int ScreenHeight { get; set; } = 1080;
|
||||||
|
|
||||||
|
public bool Fullscreen { get; set; } = false;
|
||||||
|
|
||||||
|
public static GameOptions Default { get; } = new GameOptions
|
||||||
|
{
|
||||||
|
LaunchVersion = null,
|
||||||
|
MinimumRamMb = 1024,
|
||||||
|
MaximumRamMb = 2048,
|
||||||
|
ScreenWidth = 1920,
|
||||||
|
ScreenHeight = 1080,
|
||||||
|
Fullscreen = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
16
AlayaCore/Models/Configuration/LauncherOptions.cs
Normal file
16
AlayaCore/Models/Configuration/LauncherOptions.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using AlayaCore.Abstractions.Configuration;
|
||||||
|
|
||||||
|
namespace AlayaCore.Models.Configuration
|
||||||
|
{
|
||||||
|
public sealed class LauncherOptions : BaseConfig
|
||||||
|
{
|
||||||
|
public bool ForceReinstall { get; set; } = false;
|
||||||
|
|
||||||
|
public override string FileName => "Launcher.json";
|
||||||
|
|
||||||
|
public static LauncherOptions Default { get; } = new LauncherOptions
|
||||||
|
{
|
||||||
|
ForceReinstall = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace AlayaCore.Models.Configuration
|
||||||
|
{
|
||||||
|
public sealed class LauncherUpdateServiceOptions
|
||||||
|
{
|
||||||
|
public LauncherUpdateServiceOptions(string alayaUpdaterPath, bool forceUpdate)
|
||||||
|
{
|
||||||
|
AlayaUpdaterPath = alayaUpdaterPath;
|
||||||
|
ForceUpdate = forceUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string AlayaUpdaterPath { get; set; }
|
||||||
|
public bool ForceUpdate { get; set; }
|
||||||
|
|
||||||
|
public static LauncherUpdateServiceOptions Default { get; } =
|
||||||
|
new LauncherUpdateServiceOptions(Path.Combine(AppContext.BaseDirectory, "Data", "Updater"), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,25 +4,23 @@ namespace AlayaCore.Models.Configuration
|
|||||||
{
|
{
|
||||||
public sealed class ManifestServiceOptions
|
public sealed class ManifestServiceOptions
|
||||||
{
|
{
|
||||||
public Uri CoreManifestUri { get; }
|
public Uri AlayaManifestUri { get; }
|
||||||
public Uri LauncherManifestUri { get; }
|
public Uri LauncherManifestUri { get; }
|
||||||
public string CoreManifestSha512Hash { get; }
|
public string AlayaManifestSha512Hash { get; }
|
||||||
public string LauncherManifestSha512Hash { get; }
|
public string LauncherManifestSha512Hash { get; }
|
||||||
public string ManifestDirectoryPath { get; }
|
|
||||||
|
|
||||||
public ManifestServiceOptions(
|
public ManifestServiceOptions(
|
||||||
Uri coreManifestUri,
|
Uri alayaManifestUri,
|
||||||
Uri launcherManifestUri,
|
Uri launcherManifestUri,
|
||||||
string coreManifestSha512Hash,
|
string alayaManifestSha512Hash,
|
||||||
string launcherManifestSha512Hash,
|
string launcherManifestSha512Hash)
|
||||||
string manifestDirectoryPath)
|
|
||||||
{
|
{
|
||||||
CoreManifestUri = coreManifestUri ?? throw new ArgumentNullException(nameof(coreManifestUri));
|
AlayaManifestUri = alayaManifestUri ?? throw new ArgumentNullException(nameof(alayaManifestUri));
|
||||||
LauncherManifestUri = launcherManifestUri ?? throw new ArgumentNullException(nameof(launcherManifestUri));
|
LauncherManifestUri = launcherManifestUri ?? throw new ArgumentNullException(nameof(launcherManifestUri));
|
||||||
|
|
||||||
if (!CoreManifestUri.IsAbsoluteUri)
|
if (!AlayaManifestUri.IsAbsoluteUri)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Core manifest URI must be absolute.", nameof(coreManifestUri));
|
throw new ArgumentException("Core manifest URI must be absolute.", nameof(alayaManifestUri));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!LauncherManifestUri.IsAbsoluteUri)
|
if (!LauncherManifestUri.IsAbsoluteUri)
|
||||||
@@ -30,9 +28,9 @@ namespace AlayaCore.Models.Configuration
|
|||||||
throw new ArgumentException("Launcher manifest URI must be absolute.", nameof(launcherManifestUri));
|
throw new ArgumentException("Launcher manifest URI must be absolute.", nameof(launcherManifestUri));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(coreManifestSha512Hash))
|
if (string.IsNullOrWhiteSpace(alayaManifestSha512Hash))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Core manifest SHA-512 hash cannot be null, empty, or whitespace.", nameof(coreManifestSha512Hash));
|
throw new ArgumentException("Core manifest SHA-512 hash cannot be null, empty, or whitespace.", nameof(alayaManifestSha512Hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(launcherManifestSha512Hash))
|
if (string.IsNullOrWhiteSpace(launcherManifestSha512Hash))
|
||||||
@@ -40,14 +38,12 @@ namespace AlayaCore.Models.Configuration
|
|||||||
throw new ArgumentException("Launcher manifest SHA-512 hash cannot be null, empty, or whitespace.", nameof(launcherManifestSha512Hash));
|
throw new ArgumentException("Launcher manifest SHA-512 hash cannot be null, empty, or whitespace.", nameof(launcherManifestSha512Hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(manifestDirectoryPath))
|
AlayaManifestSha512Hash = alayaManifestSha512Hash;
|
||||||
{
|
|
||||||
throw new ArgumentException("Manifest directory path cannot be null, empty, or whitespace.", nameof(manifestDirectoryPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
CoreManifestSha512Hash = coreManifestSha512Hash;
|
|
||||||
LauncherManifestSha512Hash = launcherManifestSha512Hash;
|
LauncherManifestSha512Hash = launcherManifestSha512Hash;
|
||||||
ManifestDirectoryPath = manifestDirectoryPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ManifestServiceOptions Default { get; } = new ManifestServiceOptions(
|
||||||
|
new Uri("INSERT-ALAYA-URL", UriKind.Absolute),
|
||||||
|
new Uri("INSERT-LAUNCHER-URL", UriKind.Absolute), "INSERT-ALAYA-HASH", "INSERT-LAUNCHER-HASH");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,5 +8,8 @@ namespace AlayaCore.Models.Configuration
|
|||||||
}
|
}
|
||||||
|
|
||||||
public string BaseApiUrl { get; }
|
public string BaseApiUrl { get; }
|
||||||
|
|
||||||
|
public static ModrinthConnectionOptions Default { get; } =
|
||||||
|
new ModrinthConnectionOptions("https://api.modrinth.com/v2/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
12
AlayaCore/Models/Configuration/RetryPolicyOptions.cs
Normal file
12
AlayaCore/Models/Configuration/RetryPolicyOptions.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace AlayaCore.Models.Configuration
|
||||||
|
{
|
||||||
|
public sealed class RetryPolicyOptions
|
||||||
|
{
|
||||||
|
public int MaxAttempts { get; set; } = 3;
|
||||||
|
public int BaseDelayMilliseconds { get; set; } = 500;
|
||||||
|
public double BackoffMultiplier { get; set; } = 2.0;
|
||||||
|
public int MaxDelayMilliseconds { get; set; } = 3000;
|
||||||
|
|
||||||
|
public static RetryPolicyOptions Default { get; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace AlayaCore.Models.Configuration
|
|
||||||
{
|
|
||||||
public class UpdateServiceOptions
|
|
||||||
{
|
|
||||||
public string AlayaUpdaterPath { get; set; }
|
|
||||||
public bool ForceUpdate { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace AlayaCore.Models.Manifests.DTO
|
|
||||||
{
|
|
||||||
[Serializable]
|
|
||||||
public class InstalledModEntryDto
|
|
||||||
{
|
|
||||||
[JsonProperty("name", Required = Required.Always)]
|
|
||||||
public string FileName { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonProperty("version", Required = Required.Always)]
|
|
||||||
public string Version { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonProperty("sha512Hash", Required = Required.Always)]
|
|
||||||
public string Sha512Hash { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonProperty("size", Required = Required.Always)]
|
|
||||||
public long Size { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,12 @@ namespace AlayaCore.Models.Manifests.DTO
|
|||||||
{
|
{
|
||||||
public class InstalledModsManifestDto
|
public class InstalledModsManifestDto
|
||||||
{
|
{
|
||||||
|
public InstalledModsManifestDto(List<ModFileEntryDto> installedMods)
|
||||||
|
{
|
||||||
|
InstalledMods = installedMods;
|
||||||
|
}
|
||||||
|
|
||||||
[JsonProperty("installedMods")]
|
[JsonProperty("installedMods")]
|
||||||
public List<InstalledModEntryDto> InstalledMods { get; private set; }
|
public List<ModFileEntryDto> InstalledMods { get; private set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,19 +20,21 @@ namespace AlayaCore.Models.Manifests.DTO
|
|||||||
[JsonConverter(typeof(UriConverter))]
|
[JsonConverter(typeof(UriConverter))]
|
||||||
public Uri RequiredJavaUrl { get; set; } = null!;
|
public Uri RequiredJavaUrl { get; set; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty("javaArchiveHash", Required = Required.Always)]
|
||||||
|
public string JavaArchiveHash { get; set; } = string.Empty;
|
||||||
|
|
||||||
[JsonProperty("minecraftVersion", Required = Required.Always)]
|
[JsonProperty("minecraftVersion", Required = Required.Always)]
|
||||||
public string MinecraftVersion { get; set; } = string.Empty;
|
public string MinecraftVersion { get; set; } = string.Empty;
|
||||||
|
|
||||||
[JsonProperty("minecraftUrl", Required = Required.Always)]
|
|
||||||
[JsonConverter(typeof(UriConverter))]
|
|
||||||
public Uri MinecraftUrl { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonProperty("neoforgedVersion", Required = Required.Always)]
|
[JsonProperty("neoforgedVersion", Required = Required.Always)]
|
||||||
public string NeoforgedVersion { get; set; } = string.Empty;
|
public string NeoforgedVersion { get; set; } = string.Empty;
|
||||||
|
|
||||||
[JsonProperty("neoforgedUrl", Required = Required.Always)]
|
[JsonProperty("serverUrl", Required = Required.Always)]
|
||||||
[JsonConverter(typeof(UriConverter))]
|
[JsonConverter(typeof(UriConverter))]
|
||||||
public Uri NeoforgedUrl { get; set; } = null!;
|
public Uri ServerUrl { get; set; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty("serverPort", Required = Required.Always)]
|
||||||
|
public int ServerPort { get; }
|
||||||
|
|
||||||
[JsonProperty("files", Required = Required.Always)]
|
[JsonProperty("files", Required = Required.Always)]
|
||||||
public List<ModFileEntryDto> Files { get; set; } = new List<ModFileEntryDto>();
|
public List<ModFileEntryDto> Files { get; set; } = new List<ModFileEntryDto>();
|
||||||
|
|||||||
@@ -13,13 +13,10 @@ namespace AlayaCore.Models.Manifests.DTO
|
|||||||
[JsonProperty("type", Required = Required.Always)]
|
[JsonProperty("type", Required = Required.Always)]
|
||||||
public FileType Type { get; set; }
|
public FileType Type { get; set; }
|
||||||
|
|
||||||
[JsonProperty("modrinthId", NullValueHandling = NullValueHandling.Ignore)]
|
|
||||||
public string? ModrinthId { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("modrinthVersionId", NullValueHandling = NullValueHandling.Ignore)]
|
|
||||||
public string? ModrinthVersionId { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("sha512Hash", Required = Required.Always)]
|
[JsonProperty("sha512Hash", Required = Required.Always)]
|
||||||
public string Sha512Hash { get; set; } = string.Empty;
|
public string Sha512Hash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("size", Required = Required.Always)]
|
||||||
|
public long Size { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace AlayaCore.Models.Manifests
|
|
||||||
{
|
|
||||||
[Serializable]
|
|
||||||
public sealed class InstalledModEntry
|
|
||||||
{
|
|
||||||
public string FileName { get; }
|
|
||||||
public string Version { get; }
|
|
||||||
|
|
||||||
public string Sha512Hash { get; }
|
|
||||||
|
|
||||||
public long Size { get; set; }
|
|
||||||
|
|
||||||
public InstalledModEntry(string fileName, string version, string sha512Hash, long size)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(fileName))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Name cannot be null, empty, or whitespace.", nameof(fileName));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(version))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Version cannot be null, empty, or whitespace.", nameof(version));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(sha512Hash))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("SHA512Hash cannot be null, empty, or whitespace.", nameof(sha512Hash));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size == 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Size cannot be 0, empty, or whitespace.", nameof(size));
|
|
||||||
}
|
|
||||||
|
|
||||||
FileName = fileName;
|
|
||||||
Version = version;
|
|
||||||
Sha512Hash = sha512Hash;
|
|
||||||
Size = size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,33 +7,33 @@ namespace AlayaCore.Models.Manifests
|
|||||||
{
|
{
|
||||||
public sealed class InstalledModsManifestModel
|
public sealed class InstalledModsManifestModel
|
||||||
{
|
{
|
||||||
public IReadOnlyList<InstalledModEntry> Mods { get; }
|
public IReadOnlyList<ModFileEntry> Mods { get; }
|
||||||
|
|
||||||
public InstalledModsManifestModel()
|
public InstalledModsManifestModel()
|
||||||
{
|
{
|
||||||
Mods = Array.Empty<InstalledModEntry>();
|
Mods = Array.Empty<ModFileEntry>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public InstalledModsManifestModel(IEnumerable<InstalledModEntry> mods)
|
public InstalledModsManifestModel(IEnumerable<ModFileEntry> mods)
|
||||||
{
|
{
|
||||||
if (mods == null)
|
if (mods == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(mods));
|
throw new ArgumentNullException(nameof(mods));
|
||||||
}
|
}
|
||||||
|
|
||||||
InstalledModEntry[] array = mods.ToArray();
|
ModFileEntry[] array = mods.ToArray();
|
||||||
|
|
||||||
if (array.Any(mod => mod == null))
|
if (array.Any(mod => mod == null))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Mods cannot contain null entries.", nameof(mods));
|
throw new ArgumentException("Mods cannot contain null entries.", nameof(mods));
|
||||||
}
|
}
|
||||||
|
|
||||||
Mods = new ReadOnlyCollection<InstalledModEntry>(array);
|
Mods = new ReadOnlyCollection<ModFileEntry>(array);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InstalledModsManifestModel Empty()
|
public static InstalledModsManifestModel Empty()
|
||||||
{
|
{
|
||||||
return new InstalledModsManifestModel(Array.Empty<InstalledModEntry>());
|
return new InstalledModsManifestModel(Array.Empty<ModFileEntry>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,9 @@ namespace AlayaCore.Models.Manifests
|
|||||||
{
|
{
|
||||||
public sealed class LauncherManifestModel
|
public sealed class LauncherManifestModel
|
||||||
{
|
{
|
||||||
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,
|
||||||
|
|||||||
@@ -9,33 +9,26 @@ namespace AlayaCore.Models.Manifests
|
|||||||
public sealed class ManifestModel
|
public sealed class ManifestModel
|
||||||
{
|
{
|
||||||
public Version AlayaVersion { get; }
|
public Version AlayaVersion { get; }
|
||||||
public string RequiredJavaVersion { get; }
|
|
||||||
public Uri RequiredJavaUrl { get; }
|
|
||||||
public string MinecraftVersion { get; }
|
public string MinecraftVersion { get; }
|
||||||
public Uri MinecraftUrl { get; }
|
|
||||||
public string NeoforgedVersion { get; }
|
public string NeoforgedVersion { get; }
|
||||||
public Uri NeoforgedUrl { get; }
|
|
||||||
|
public Uri? ServerUrl { get; }
|
||||||
|
public int ServerPort { get; }
|
||||||
public IReadOnlyList<ModFileEntry> Files { get; }
|
public IReadOnlyList<ModFileEntry> Files { get; }
|
||||||
|
|
||||||
public ManifestModel(
|
public ManifestModel(
|
||||||
Version alayaVersion,
|
Version alayaVersion,
|
||||||
string requiredJavaVersion,
|
|
||||||
Uri requiredJavaUrl,
|
|
||||||
string minecraftVersion,
|
string minecraftVersion,
|
||||||
Uri minecraftUrl,
|
|
||||||
string neoforgedVersion,
|
string neoforgedVersion,
|
||||||
Uri neoforgedUrl,
|
Uri serverUrl,
|
||||||
|
int serverPort,
|
||||||
IEnumerable<ModFileEntry> files)
|
IEnumerable<ModFileEntry> files)
|
||||||
{
|
{
|
||||||
AlayaVersion = alayaVersion ?? throw new ArgumentNullException(nameof(alayaVersion));
|
AlayaVersion = alayaVersion ?? throw new ArgumentNullException(nameof(alayaVersion));
|
||||||
RequiredJavaVersion = RequireNonEmpty(requiredJavaVersion, nameof(requiredJavaVersion));
|
|
||||||
RequiredJavaUrl = requiredJavaUrl ?? throw new ArgumentNullException(nameof(requiredJavaUrl));
|
|
||||||
|
|
||||||
MinecraftVersion = RequireNonEmpty(minecraftVersion, nameof(minecraftVersion));
|
MinecraftVersion = RequireNonEmpty(minecraftVersion, nameof(minecraftVersion));
|
||||||
MinecraftUrl = minecraftUrl ?? throw new ArgumentNullException(nameof(minecraftUrl));
|
|
||||||
|
|
||||||
NeoforgedVersion = RequireNonEmpty(neoforgedVersion, nameof(neoforgedVersion));
|
NeoforgedVersion = RequireNonEmpty(neoforgedVersion, nameof(neoforgedVersion));
|
||||||
NeoforgedUrl = neoforgedUrl ?? throw new ArgumentNullException(nameof(neoforgedUrl));
|
|
||||||
|
|
||||||
if (files == null)
|
if (files == null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,22 +8,19 @@ namespace AlayaCore.Models.Manifests
|
|||||||
{
|
{
|
||||||
public string FileName { get; }
|
public string FileName { get; }
|
||||||
public FileType Type { get; }
|
public FileType Type { get; }
|
||||||
public string? ModrinthId { get; }
|
|
||||||
public string? ModrinthVersionId { get; }
|
|
||||||
public string Sha512Hash { get; }
|
public string Sha512Hash { get; }
|
||||||
|
public long Size { get; }
|
||||||
|
|
||||||
public ModFileEntry(
|
public ModFileEntry(
|
||||||
string name,
|
string fileName,
|
||||||
FileType type,
|
FileType type,
|
||||||
string sha512Hash,
|
string sha512Hash,
|
||||||
string? modrinthId = null,
|
long size)
|
||||||
string? modrinthVersionId = null)
|
|
||||||
{
|
{
|
||||||
FileName = RequireNonEmpty(name, nameof(name));
|
FileName = RequireNonEmpty(fileName, nameof(fileName));
|
||||||
Type = type;
|
Type = type;
|
||||||
Sha512Hash = RequireNonEmpty(sha512Hash, nameof(sha512Hash));
|
Sha512Hash = RequireSha512Hash(sha512Hash, nameof(sha512Hash));
|
||||||
ModrinthId = NormalizeOptional(modrinthId);
|
Size = RequirePositiveSize(size, nameof(size));
|
||||||
ModrinthVersionId = NormalizeOptional(modrinthVersionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string RequireNonEmpty(string value, string paramName)
|
private static string RequireNonEmpty(string value, string paramName)
|
||||||
@@ -36,9 +33,29 @@ namespace AlayaCore.Models.Manifests
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? NormalizeOptional(string? value)
|
private static string RequireSha512Hash(string value, string paramName)
|
||||||
{
|
{
|
||||||
return string.IsNullOrWhiteSpace(value) ? null : value;
|
string normalized = RequireNonEmpty(value, paramName)
|
||||||
|
.Trim()
|
||||||
|
.Replace("-", string.Empty)
|
||||||
|
.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (normalized.Length != 128)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("SHA-512 hash must be 128 hexadecimal characters long.", paramName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long RequirePositiveSize(long value, string paramName)
|
||||||
|
{
|
||||||
|
if (value <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(paramName, "Size must be greater than zero.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using System;
|
|
||||||
using AlayaCore.Models.Manifests;
|
|
||||||
|
|
||||||
namespace AlayaCore.Models
|
|
||||||
{
|
|
||||||
public class ModrinthModInfoModel
|
|
||||||
{
|
|
||||||
public ModrinthModInfoModel(Uri modUrl, long size)
|
|
||||||
{
|
|
||||||
ModUrl = modUrl;
|
|
||||||
Size = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uri ModUrl { get; set; }
|
|
||||||
public long Size { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
8
AlayaCore/Models/Results/InstallOrUpdateResult.cs
Normal file
8
AlayaCore/Models/Results/InstallOrUpdateResult.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace AlayaCore.Models.Results
|
||||||
|
{
|
||||||
|
public enum InstallOrUpdateResult
|
||||||
|
{
|
||||||
|
Ready,
|
||||||
|
UpdaterLaunched
|
||||||
|
}
|
||||||
|
}
|
||||||
216
AlayaCore/Policies/RetryPolicy.cs
Normal file
216
AlayaCore/Policies/RetryPolicy.cs
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Abstractions.Interfaces.Policies;
|
||||||
|
using AlayaCore.Models.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace AlayaCore.Services
|
||||||
|
{
|
||||||
|
public sealed class RetryPolicy : IRetryPolicy
|
||||||
|
{
|
||||||
|
private readonly RetryPolicyOptions _options;
|
||||||
|
private readonly ILogger<RetryPolicy> _logger;
|
||||||
|
|
||||||
|
private static readonly Random _random = new Random();
|
||||||
|
|
||||||
|
public RetryPolicy(
|
||||||
|
RetryPolicyOptions options,
|
||||||
|
ILogger<RetryPolicy> logger)
|
||||||
|
{
|
||||||
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecuteAsync(
|
||||||
|
Func<CancellationToken, Task> operation,
|
||||||
|
string operationName,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (operation == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(operation));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(operationName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Operation name cannot be null, empty, or whitespace.", nameof(operationName));
|
||||||
|
}
|
||||||
|
|
||||||
|
await ExecuteAsync<object?>(
|
||||||
|
async token =>
|
||||||
|
{
|
||||||
|
await operation(token).ConfigureAwait(false);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
operationName,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> ExecuteAsync<T>(
|
||||||
|
Func<CancellationToken, Task<T>> operation,
|
||||||
|
string operationName,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (operation == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(operation));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(operationName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Operation name cannot be null, empty, or whitespace.", nameof(operationName));
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateOptions();
|
||||||
|
|
||||||
|
Exception? lastException = null;
|
||||||
|
|
||||||
|
for (int attempt = 1; attempt <= _options.MaxAttempts; attempt++)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (attempt > 1)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Retrying operation {OperationName}. Attempt {Attempt} of {MaxAttempts}.",
|
||||||
|
operationName,
|
||||||
|
attempt,
|
||||||
|
_options.MaxAttempts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await operation(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested && IsRetryable(ex))
|
||||||
|
{
|
||||||
|
lastException = ex;
|
||||||
|
|
||||||
|
if (attempt == _options.MaxAttempts)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
ex,
|
||||||
|
"Operation {OperationName} failed after {MaxAttempts} attempts due to repeated timeout or cancellation-like transient failures.",
|
||||||
|
operationName,
|
||||||
|
_options.MaxAttempts);
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeSpan delay = CalculateDelay(attempt);
|
||||||
|
|
||||||
|
_logger.LogWarning(
|
||||||
|
ex,
|
||||||
|
"Operation {OperationName} timed out or was transiently cancelled on attempt {Attempt} of {MaxAttempts}. Retrying after {DelayMs}ms.",
|
||||||
|
operationName,
|
||||||
|
attempt,
|
||||||
|
_options.MaxAttempts,
|
||||||
|
(int)delay.TotalMilliseconds);
|
||||||
|
|
||||||
|
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Operation {OperationName} was cancelled by the caller.",
|
||||||
|
operationName);
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (IsRetryable(ex))
|
||||||
|
{
|
||||||
|
lastException = ex;
|
||||||
|
|
||||||
|
if (attempt == _options.MaxAttempts)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
ex,
|
||||||
|
"Operation {OperationName} failed after {MaxAttempts} attempts.",
|
||||||
|
operationName,
|
||||||
|
_options.MaxAttempts);
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeSpan delay = CalculateDelay(attempt);
|
||||||
|
|
||||||
|
_logger.LogWarning(
|
||||||
|
ex,
|
||||||
|
"Operation {OperationName} failed with a transient error on attempt {Attempt} of {MaxAttempts}. Retrying after {DelayMs}ms.",
|
||||||
|
operationName,
|
||||||
|
attempt,
|
||||||
|
_options.MaxAttempts,
|
||||||
|
(int)delay.TotalMilliseconds);
|
||||||
|
|
||||||
|
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
ex,
|
||||||
|
"Operation {OperationName} failed with a non-retryable error on attempt {Attempt}.",
|
||||||
|
operationName,
|
||||||
|
attempt);
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Retry policy exited unexpectedly for operation '{operationName}'.",
|
||||||
|
lastException);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateOptions()
|
||||||
|
{
|
||||||
|
if (_options.MaxAttempts <= 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("RetryPolicyOptions.MaxAttempts must be greater than zero.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_options.BaseDelayMilliseconds < 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("RetryPolicyOptions.BaseDelayMilliseconds cannot be negative.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_options.BackoffMultiplier < 1d)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("RetryPolicyOptions.BackoffMultiplier must be greater than or equal to 1.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_options.MaxDelayMilliseconds < 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("RetryPolicyOptions.MaxDelayMilliseconds cannot be negative.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsRetryable(Exception exception)
|
||||||
|
{
|
||||||
|
return exception switch
|
||||||
|
{
|
||||||
|
HttpRequestException => true,
|
||||||
|
IOException => true,
|
||||||
|
TaskCanceledException => true,
|
||||||
|
InvalidDataException => false,
|
||||||
|
ArgumentException => false,
|
||||||
|
InvalidOperationException => false,
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private TimeSpan CalculateDelay(int attempt)
|
||||||
|
{
|
||||||
|
double exponentialDelay = _options.BaseDelayMilliseconds *
|
||||||
|
Math.Pow(_options.BackoffMultiplier, attempt - 1);
|
||||||
|
|
||||||
|
double cappedDelay = Math.Min(exponentialDelay, _options.MaxDelayMilliseconds);
|
||||||
|
|
||||||
|
int jitterMilliseconds = _random.Next(0, 150);
|
||||||
|
|
||||||
|
return TimeSpan.FromMilliseconds(cappedDelay + jitterMilliseconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
200
AlayaCore/Services/AuthService.cs
Normal file
200
AlayaCore/Services/AuthService.cs
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Abstractions.Interfaces;
|
||||||
|
using AlayaCore.Abstractions.Interfaces.Services;
|
||||||
|
using AlayaCore.Utilities.Enums;
|
||||||
|
using CmlLib.Core.Auth;
|
||||||
|
using CmlLib.Core.Auth.Microsoft;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Identity.Client;
|
||||||
|
using XboxAuthNet.Game.Msal;
|
||||||
|
using XboxAuthNet.Game.Msal.OAuth;
|
||||||
|
|
||||||
|
namespace AlayaCore.Services
|
||||||
|
{
|
||||||
|
public sealed class AuthService : IAuthService
|
||||||
|
{
|
||||||
|
private readonly IFileStore _fileStore;
|
||||||
|
private readonly ILogger<AuthService> _logger;
|
||||||
|
|
||||||
|
private MSession? _session;
|
||||||
|
private IPublicClientApplication? _clientApp;
|
||||||
|
private JELoginHandler? _loginHandler;
|
||||||
|
|
||||||
|
public AuthService(
|
||||||
|
IFileStore fileStore,
|
||||||
|
ILogger<AuthService> logger)
|
||||||
|
{
|
||||||
|
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsAuthenticatedAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (_session != null && _session.CheckIsValid())
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Authentication check succeeded using the cached in-memory session.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("No valid cached in-memory session was found. Attempting silent authentication check.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JELoginHandler loginHandler = await BuildHandlerAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
_session = await loginHandler
|
||||||
|
.AuthenticateSilently(cancellationToken: cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Authentication check was cancelled.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_session = null;
|
||||||
|
_logger.LogInformation(ex, "Silent authentication check failed. The user is not currently authenticated.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AuthenticateAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogInformation("Starting authentication flow.");
|
||||||
|
|
||||||
|
JELoginHandler loginHandler = await BuildHandlerAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Attempting silent authentication.");
|
||||||
|
|
||||||
|
_session = await loginHandler
|
||||||
|
.AuthenticateSilently(cancellationToken: cancellationToken)
|
||||||
|
.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)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Authentication was cancelled during silent authentication.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(ex, "Silent authentication failed. Falling back to interactive authentication.");
|
||||||
|
|
||||||
|
_session = await loginHandler
|
||||||
|
.AuthenticateInteractively(cancellationToken: cancellationToken)
|
||||||
|
.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())
|
||||||
|
{
|
||||||
|
_logger.LogError("Authentication failed because no valid Minecraft session was produced.");
|
||||||
|
throw new InvalidOperationException("Authentication did not produce a valid Minecraft session.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SignOutAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogInformation("Signing out the current user.");
|
||||||
|
|
||||||
|
JELoginHandler loginHandler = await BuildHandlerAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
await loginHandler.Signout(cancellationToken).ConfigureAwait(false);
|
||||||
|
_session = null;
|
||||||
|
|
||||||
|
_logger.LogInformation("Sign-out completed and cached session was cleared.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MSession> GetSessionAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (_session != null && _session.CheckIsValid())
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Returning a valid cached Minecraft session.");
|
||||||
|
return _session;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("No valid cached session was available. Attempting authentication before returning a session.");
|
||||||
|
|
||||||
|
await AuthenticateAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Returning Minecraft session obtained from authentication flow.");
|
||||||
|
return _session;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<JELoginHandler> BuildHandlerAsync()
|
||||||
|
{
|
||||||
|
if (_loginHandler != null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Reusing existing JELoginHandler instance.");
|
||||||
|
return _loginHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
string accountDirectory = _fileStore.GetOrCreate(FolderLocation.Data);
|
||||||
|
string accountFilePath = Path.Combine(accountDirectory, "accounts.json");
|
||||||
|
|
||||||
|
_logger.LogInformation("Building MSAL client and login handler. Account cache path: {AccountFilePath}", accountFilePath);
|
||||||
|
|
||||||
|
_clientApp = await MsalClientHelper
|
||||||
|
.BuildApplicationWithCache("d91042d4-3eb5-43e4-b3ed-600e1d0760ff")
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_loginHandler = new JELoginHandlerBuilder()
|
||||||
|
.WithOAuthProvider(new MsalCodeFlowProvider(_clientApp))
|
||||||
|
.WithAccountManager(accountFilePath)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_logger.LogInformation("MSAL client and JELoginHandler were created successfully.");
|
||||||
|
|
||||||
|
return _loginHandler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using AlayaCore.Abstractions.Interfaces.Clients;
|
|
||||||
using AlayaCore.Abstractions.Interfaces.Services;
|
|
||||||
using AlayaCore.Models.Progress;
|
|
||||||
using AlayaCore.Models.Results;
|
|
||||||
|
|
||||||
namespace AlayaCore.Services
|
|
||||||
{
|
|
||||||
public sealed class DownloadService : IDownloadService
|
|
||||||
{
|
|
||||||
private const int BUFFER_SIZE = 81920;
|
|
||||||
|
|
||||||
private readonly IHttpClient _httpClient;
|
|
||||||
|
|
||||||
public DownloadService(IHttpClient httpClient)
|
|
||||||
{
|
|
||||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<DownloadResult> DownloadFileAsync(
|
|
||||||
Uri sourceUri,
|
|
||||||
string destinationPath,
|
|
||||||
string sha512Hash,
|
|
||||||
IProgress<DownloadProgress>? progress = null,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (sourceUri == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(sourceUri));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sourceUri.IsAbsoluteUri)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Source URI must be absolute.", nameof(sourceUri));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(destinationPath))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Destination path cannot be null, empty, or whitespace.", nameof(destinationPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(sha512Hash))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("SHA-512 hash cannot be null, empty, or whitespace.", nameof(sha512Hash));
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
string normalizedExpectedHash = NormalizeHash(sha512Hash);
|
|
||||||
string fileName = Path.GetFileName(destinationPath);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(fileName))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Destination path must include a file name.", nameof(destinationPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
EnsureDestinationDirectoryExists(destinationPath);
|
|
||||||
|
|
||||||
if (File.Exists(destinationPath) && VerifyFileHash(destinationPath, normalizedExpectedHash))
|
|
||||||
{
|
|
||||||
long existingLength = new FileInfo(destinationPath).Length;
|
|
||||||
|
|
||||||
progress?.Report(new DownloadProgress(
|
|
||||||
fileName: fileName,
|
|
||||||
destinationPath: destinationPath,
|
|
||||||
bytesDownloaded: existingLength,
|
|
||||||
totalBytes: existingLength,
|
|
||||||
bytesPerSecond: null,
|
|
||||||
statusMessage: "File already present and valid."));
|
|
||||||
|
|
||||||
return new DownloadResult(
|
|
||||||
destinationPath,
|
|
||||||
DownloadOutcome.SkippedAlreadyValid,
|
|
||||||
hashVerified: true,
|
|
||||||
bytesDownloaded: 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool destinationExisted = File.Exists(destinationPath);
|
|
||||||
string tempFilePath = destinationPath + ".download";
|
|
||||||
|
|
||||||
DeleteFileIfExists(tempFilePath);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using HttpResponseMessage response = await _httpClient.GetAsync(
|
|
||||||
sourceUri,
|
|
||||||
HttpCompletionOption.ResponseHeadersRead,
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
long? totalBytes = response.Content.Headers.ContentLength;
|
|
||||||
long bytesDownloaded = 0;
|
|
||||||
|
|
||||||
progress?.Report(new DownloadProgress(
|
|
||||||
fileName: fileName,
|
|
||||||
destinationPath: destinationPath,
|
|
||||||
bytesDownloaded: 0,
|
|
||||||
totalBytes: totalBytes,
|
|
||||||
bytesPerSecond: null,
|
|
||||||
statusMessage: "Starting download..."));
|
|
||||||
|
|
||||||
using Stream responseStream = await response.Content.ReadAsStreamAsync();
|
|
||||||
using FileStream fileStream = new FileStream(
|
|
||||||
tempFilePath,
|
|
||||||
FileMode.Create,
|
|
||||||
FileAccess.Write,
|
|
||||||
FileShare.None,
|
|
||||||
BUFFER_SIZE,
|
|
||||||
useAsync: true);
|
|
||||||
using SHA512 sha512 = SHA512.Create();
|
|
||||||
|
|
||||||
byte[] buffer = new byte[BUFFER_SIZE];
|
|
||||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
int bytesRead = await responseStream.ReadAsync(
|
|
||||||
buffer,
|
|
||||||
0,
|
|
||||||
buffer.Length,
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
if (bytesRead == 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
await fileStream.WriteAsync(
|
|
||||||
buffer,
|
|
||||||
0,
|
|
||||||
bytesRead,
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
sha512.TransformBlock(buffer, 0, bytesRead, null, 0);
|
|
||||||
|
|
||||||
bytesDownloaded += bytesRead;
|
|
||||||
|
|
||||||
double? bytesPerSecond = null;
|
|
||||||
if (stopwatch.Elapsed.TotalSeconds > 0d)
|
|
||||||
{
|
|
||||||
bytesPerSecond = bytesDownloaded / stopwatch.Elapsed.TotalSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
progress?.Report(new DownloadProgress(
|
|
||||||
fileName: fileName,
|
|
||||||
destinationPath: destinationPath,
|
|
||||||
bytesDownloaded: bytesDownloaded,
|
|
||||||
totalBytes: totalBytes,
|
|
||||||
bytesPerSecond: bytesPerSecond,
|
|
||||||
statusMessage: "Downloading file..."));
|
|
||||||
}
|
|
||||||
|
|
||||||
sha512.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
|
|
||||||
await fileStream.FlushAsync(cancellationToken);
|
|
||||||
|
|
||||||
string actualHash = ConvertToLowerHex(sha512.Hash);
|
|
||||||
if (!string.Equals(actualHash, normalizedExpectedHash, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
throw new InvalidDataException(
|
|
||||||
$"Downloaded file hash mismatch. Expected '{normalizedExpectedHash}', got '{actualHash}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ReplaceDestinationFile(tempFilePath, destinationPath);
|
|
||||||
|
|
||||||
double? finalBytesPerSecond = null;
|
|
||||||
if (stopwatch.Elapsed.TotalSeconds > 0d)
|
|
||||||
{
|
|
||||||
finalBytesPerSecond = bytesDownloaded / stopwatch.Elapsed.TotalSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
progress?.Report(new DownloadProgress(
|
|
||||||
fileName: fileName,
|
|
||||||
destinationPath: destinationPath,
|
|
||||||
bytesDownloaded: bytesDownloaded,
|
|
||||||
totalBytes: totalBytes ?? bytesDownloaded,
|
|
||||||
bytesPerSecond: finalBytesPerSecond,
|
|
||||||
statusMessage: "Download complete."));
|
|
||||||
|
|
||||||
return new DownloadResult(
|
|
||||||
destinationPath,
|
|
||||||
destinationExisted ? DownloadOutcome.ReplacedInvalid : DownloadOutcome.Downloaded,
|
|
||||||
hashVerified: true,
|
|
||||||
bytesDownloaded: bytesDownloaded);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
DeleteFileIfExists(tempFilePath);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool VerifyFileHash(string filePath, string sha512Hash)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(filePath))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("File path cannot be null, empty, or whitespace.", nameof(filePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(sha512Hash))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("SHA-512 hash cannot be null, empty, or whitespace.", nameof(sha512Hash));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!File.Exists(filePath))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
string normalizedExpectedHash = NormalizeHash(sha512Hash);
|
|
||||||
|
|
||||||
using FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
||||||
using SHA512 sha512 = SHA512.Create();
|
|
||||||
|
|
||||||
byte[] hashBytes = sha512.ComputeHash(fileStream);
|
|
||||||
string actualHash = ConvertToLowerHex(hashBytes);
|
|
||||||
|
|
||||||
return string.Equals(actualHash, normalizedExpectedHash, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnsureDestinationDirectoryExists(string destinationPath)
|
|
||||||
{
|
|
||||||
string? directoryPath = Path.GetDirectoryName(destinationPath);
|
|
||||||
if (!string.IsNullOrWhiteSpace(directoryPath))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(directoryPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReplaceDestinationFile(string sourcePath, string destinationPath)
|
|
||||||
{
|
|
||||||
DeleteFileIfExists(destinationPath);
|
|
||||||
File.Move(sourcePath, destinationPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DeleteFileIfExists(string path)
|
|
||||||
{
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
File.Delete(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeHash(string hash)
|
|
||||||
{
|
|
||||||
return hash.Trim().Replace("-", string.Empty).ToLowerInvariant();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ConvertToLowerHex(byte[]? hashBytes)
|
|
||||||
{
|
|
||||||
if (hashBytes == null || hashBytes.Length == 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Hash computation returned no data.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return BitConverter.ToString(hashBytes).Replace("-", string.Empty).ToLowerInvariant();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
372
AlayaCore/Services/GameInstallService.cs
Normal file
372
AlayaCore/Services/GameInstallService.cs
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Abstractions.Interfaces;
|
||||||
|
using AlayaCore.Abstractions.Interfaces.Services;
|
||||||
|
using AlayaCore.Installation;
|
||||||
|
using AlayaCore.Models;
|
||||||
|
using AlayaCore.Models.Manifests;
|
||||||
|
using AlayaCore.Utilities.Enums;
|
||||||
|
using CmlLib.Core;
|
||||||
|
using CmlLib.Core.Installer.NeoForge;
|
||||||
|
using CmlLib.Core.Installer.NeoForge.Installers;
|
||||||
|
using CmlLib.Core.Installers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace AlayaCore.Services
|
||||||
|
{
|
||||||
|
public sealed class GameInstallService : IGameInstallService
|
||||||
|
{
|
||||||
|
private const string INSTALLED_MODS_MANIFEST_FILE_NAME = "InstalledModsManifest.json";
|
||||||
|
|
||||||
|
private readonly IFileStore _fileStore;
|
||||||
|
private readonly ISettingsService _settingsService;
|
||||||
|
private readonly ILogger<GameInstallService> _logger;
|
||||||
|
|
||||||
|
private AlayaPath? _gamePath;
|
||||||
|
private MinecraftLauncher? _minecraftLauncher;
|
||||||
|
|
||||||
|
public GameInstallService(
|
||||||
|
IFileStore fileStore,
|
||||||
|
ISettingsService settingsService,
|
||||||
|
ILogger<GameInstallService> logger)
|
||||||
|
{
|
||||||
|
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
||||||
|
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EnsureMinecraftInstalledAsync(
|
||||||
|
ManifestModel manifest,
|
||||||
|
InstallEnvironment environment,
|
||||||
|
CancellationToken cancellationToken = default,
|
||||||
|
EventHandler<InstallerProgressChangedEventArgs>? minecraftProgress = null,
|
||||||
|
EventHandler<ByteProgress>? byteProgress = null)
|
||||||
|
{
|
||||||
|
if (manifest == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(manifest));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (environment == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(environment));
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Ensuring Minecraft is installed. Required version: {RequiredVersion}, Installed: {Installed}, Current version: {CurrentVersion}",
|
||||||
|
manifest.MinecraftVersion,
|
||||||
|
environment.MinecraftInstalled,
|
||||||
|
environment.MinecraftVersion);
|
||||||
|
|
||||||
|
bool alreadyInstalled =
|
||||||
|
environment.MinecraftInstalled &&
|
||||||
|
string.Equals(environment.MinecraftVersion, manifest.MinecraftVersion, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (alreadyInstalled)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Minecraft version {MinecraftVersion} is already installed and matches the manifest.", manifest.MinecraftVersion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool versionMismatch =
|
||||||
|
environment.MinecraftInstalled &&
|
||||||
|
!string.Equals(environment.MinecraftVersion, manifest.MinecraftVersion, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftLauncher launcher = GetOrCreateLauncher(minecraftProgress, byteProgress);
|
||||||
|
|
||||||
|
_logger.LogInformation("Starting Minecraft installation for version {MinecraftVersion}.", manifest.MinecraftVersion);
|
||||||
|
|
||||||
|
await launcher
|
||||||
|
.InstallAsync(manifest.MinecraftVersion, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Minecraft installation completed for version {MinecraftVersion}.", manifest.MinecraftVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EnsureNeoForgeInstalledAsync(
|
||||||
|
ManifestModel manifest,
|
||||||
|
InstallEnvironment environment,
|
||||||
|
CancellationToken cancellationToken = default,
|
||||||
|
IProgress<InstallerProgressChangedEventArgs>? progress = null,
|
||||||
|
IProgress<ByteProgress>? byteProgress = null)
|
||||||
|
{
|
||||||
|
if (manifest == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(manifest));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (environment == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(environment));
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Ensuring NeoForge is installed. Required version: {RequiredVersion}, Installed: {Installed}, Current version: {CurrentVersion}",
|
||||||
|
manifest.NeoforgedVersion,
|
||||||
|
environment.NeoforgedInstalled,
|
||||||
|
environment.NeoforgedVersion);
|
||||||
|
|
||||||
|
bool alreadyInstalled =
|
||||||
|
environment.NeoforgedInstalled &&
|
||||||
|
string.Equals(environment.NeoforgedVersion, manifest.NeoforgedVersion, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (alreadyInstalled)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("NeoForge version {NeoForgeVersion} is already installed and matches the manifest.", manifest.NeoforgedVersion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool neoForgeMismatch =
|
||||||
|
environment.NeoforgedInstalled &&
|
||||||
|
!string.Equals(environment.NeoforgedVersion, manifest.NeoforgedVersion, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!environment.MinecraftInstalled ||
|
||||||
|
!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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
launcher,
|
||||||
|
manifest,
|
||||||
|
environment,
|
||||||
|
cancellationToken,
|
||||||
|
progress,
|
||||||
|
byteProgress).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"NeoForge installation completed. Verifying Minecraft files for version {MinecraftVersion}.",
|
||||||
|
manifest.MinecraftVersion);
|
||||||
|
|
||||||
|
await launcher
|
||||||
|
.InstallAsync(manifest.MinecraftVersion, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Minecraft file verification completed after NeoForge installation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task VerifyFilesAsync(
|
||||||
|
ManifestModel manifest,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (manifest == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(manifest));
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftLauncher launcher = GetOrCreateLauncher();
|
||||||
|
|
||||||
|
_logger.LogInformation("Verifying Minecraft files for version {MinecraftVersion}.", manifest.MinecraftVersion);
|
||||||
|
|
||||||
|
await launcher
|
||||||
|
.InstallAsync(manifest.MinecraftVersion, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Minecraft file verification completed for version {MinecraftVersion}.", manifest.MinecraftVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CleanOldInstallAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
string gamePath = GetMinecraftPath();
|
||||||
|
|
||||||
|
_logger.LogInformation("Cleaning old game installation at path {GamePath}.", gamePath);
|
||||||
|
|
||||||
|
if (Directory.Exists(gamePath))
|
||||||
|
{
|
||||||
|
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(
|
||||||
|
_fileStore.Get(FolderLocation.Manifests),
|
||||||
|
INSTALLED_MODS_MANIFEST_FILE_NAME);
|
||||||
|
|
||||||
|
if (File.Exists(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;
|
||||||
|
_minecraftLauncher = null;
|
||||||
|
|
||||||
|
_logger.LogDebug("Cleared cached Minecraft launcher state.");
|
||||||
|
|
||||||
|
await _settingsService
|
||||||
|
.UpdateLaunchVersionAsync(string.Empty, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Cleared stored launch version after cleaning the old install.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private MinecraftLauncher GetOrCreateLauncher(
|
||||||
|
EventHandler<InstallerProgressChangedEventArgs>? minecraftProgress = null,
|
||||||
|
EventHandler<ByteProgress>? byteProgress = null)
|
||||||
|
{
|
||||||
|
if (_minecraftLauncher != null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Reusing existing MinecraftLauncher instance.");
|
||||||
|
return _minecraftLauncher;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Creating a new MinecraftLauncher instance.");
|
||||||
|
|
||||||
|
_gamePath = new AlayaPath(_fileStore);
|
||||||
|
_minecraftLauncher = new MinecraftLauncher(_gamePath);
|
||||||
|
|
||||||
|
if (byteProgress != null)
|
||||||
|
{
|
||||||
|
_minecraftLauncher.ByteProgressChanged += byteProgress;
|
||||||
|
_logger.LogDebug("Attached Minecraft byte progress handler.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minecraftProgress != null)
|
||||||
|
{
|
||||||
|
_minecraftLauncher.FileProgressChanged += minecraftProgress;
|
||||||
|
_logger.LogDebug("Attached Minecraft file progress handler.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return _minecraftLauncher;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InstallNeoForgeAsync(
|
||||||
|
MinecraftLauncher launcher,
|
||||||
|
ManifestModel manifest,
|
||||||
|
InstallEnvironment environment,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
IProgress<InstallerProgressChangedEventArgs>? progress = null,
|
||||||
|
IProgress<ByteProgress>? byteProgress = null)
|
||||||
|
{
|
||||||
|
if (launcher == null)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
NeoForgeInstallOptions options = new NeoForgeInstallOptions
|
||||||
|
{
|
||||||
|
CancellationToken = cancellationToken,
|
||||||
|
JavaPath = environment.JavaPath,
|
||||||
|
SkipIfAlreadyInstalled = true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (progress != null)
|
||||||
|
{
|
||||||
|
options.FileProgress = progress;
|
||||||
|
_logger.LogDebug("Attached NeoForge file progress reporter.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byteProgress != null)
|
||||||
|
{
|
||||||
|
options.ByteProgress = byteProgress;
|
||||||
|
_logger.LogDebug("Attached NeoForge byte progress reporter.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string version = await installer
|
||||||
|
.Install(manifest.MinecraftVersion, manifest.NeoforgedVersion, options)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("NeoForge installer returned launch version {LaunchVersion}.", version);
|
||||||
|
|
||||||
|
await _settingsService
|
||||||
|
.UpdateLaunchVersionAsync(version, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Persisted launch version {LaunchVersion} to settings.", version);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetMinecraftPath()
|
||||||
|
{
|
||||||
|
string gamePath = _fileStore.GetOrCreate(FolderLocation.Game);
|
||||||
|
_logger.LogDebug("Resolved Minecraft game path to {GamePath}.", gamePath);
|
||||||
|
return gamePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
176
AlayaCore/Services/GameLaunchService.cs
Normal file
176
AlayaCore/Services/GameLaunchService.cs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Abstractions.Interfaces;
|
||||||
|
using AlayaCore.Abstractions.Interfaces.Services;
|
||||||
|
using AlayaCore.Installation;
|
||||||
|
using AlayaCore.Models;
|
||||||
|
using AlayaCore.Models.Configuration;
|
||||||
|
using AlayaCore.Models.Manifests;
|
||||||
|
using CmlLib.Core;
|
||||||
|
using CmlLib.Core.ProcessBuilder;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace AlayaCore.Services
|
||||||
|
{
|
||||||
|
public sealed class GameLaunchService : IGameLaunchService
|
||||||
|
{
|
||||||
|
private readonly IAuthService _authService;
|
||||||
|
private readonly IFileStore _fileStore;
|
||||||
|
private readonly ILaunchDirector _director;
|
||||||
|
private readonly GameOptions _gameOptions;
|
||||||
|
private readonly ILogger<GameLaunchService> _logger;
|
||||||
|
|
||||||
|
private MinecraftLauncher? _minecraftLauncher;
|
||||||
|
|
||||||
|
public GameLaunchService(
|
||||||
|
IAuthService authService,
|
||||||
|
IFileStore fileStore,
|
||||||
|
ILaunchDirector director,
|
||||||
|
GameOptions gameOptions,
|
||||||
|
ILogger<GameLaunchService> logger)
|
||||||
|
{
|
||||||
|
_authService = authService ?? throw new ArgumentNullException(nameof(authService));
|
||||||
|
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
||||||
|
_director = director ?? throw new ArgumentNullException(nameof(director));
|
||||||
|
_gameOptions = gameOptions ?? throw new ArgumentNullException(nameof(gameOptions));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LaunchAsync(
|
||||||
|
ManifestModel manifest,
|
||||||
|
InstallEnvironment environment,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(manifest));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (environment == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(environment));
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
manifest,
|
||||||
|
environment,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
MinecraftLauncher launcher = GetOrCreateLauncher();
|
||||||
|
|
||||||
|
_logger.LogInformation("Creating Minecraft process for launch version {LaunchVersion}.", _gameOptions.LaunchVersion);
|
||||||
|
|
||||||
|
var process = await launcher
|
||||||
|
.CreateProcessAsync(_gameOptions.LaunchVersion, option)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Minecraft process was created successfully. ProcessId: {ProcessId}",
|
||||||
|
process.Id);
|
||||||
|
|
||||||
|
var processWrapper = new ProcessWrapper(process);
|
||||||
|
|
||||||
|
_logger.LogInformation("Starting Minecraft process.");
|
||||||
|
processWrapper.StartWithEvents();
|
||||||
|
|
||||||
|
_logger.LogInformation("Waiting for Minecraft process to exit.");
|
||||||
|
await processWrapper.WaitForExitTaskAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Minecraft process exited. ProcessId: {ProcessId}, ExitCode: {ExitCode}",
|
||||||
|
process.Id,
|
||||||
|
process.ExitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MinecraftLauncher GetOrCreateLauncher()
|
||||||
|
{
|
||||||
|
if (_minecraftLauncher != null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Reusing existing MinecraftLauncher instance for game launch.");
|
||||||
|
return _minecraftLauncher;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Creating a new MinecraftLauncher instance for game launch.");
|
||||||
|
_minecraftLauncher = new MinecraftLauncher(new AlayaPath(_fileStore));
|
||||||
|
return _minecraftLauncher;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<MLaunchOption> BuildLaunchOptionsAsync(
|
||||||
|
ManifestModel manifest,
|
||||||
|
InstallEnvironment environment,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (manifest == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(manifest));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (environment == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(environment));
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_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
|
||||||
|
{
|
||||||
|
Session = session,
|
||||||
|
JavaPath = environment.JavaPath,
|
||||||
|
MinimumRamMb = _gameOptions.MinimumRamMb,
|
||||||
|
MaximumRamMb = _gameOptions.MaximumRamMb,
|
||||||
|
ScreenWidth = _gameOptions.ScreenWidth,
|
||||||
|
ScreenHeight = _gameOptions.ScreenHeight,
|
||||||
|
ServerIp = manifest.ServerUrl.Host,
|
||||||
|
ServerPort = manifest.ServerPort,
|
||||||
|
DockName = "AlayaCraft"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
381
AlayaCore/Services/HttpDownloadService.cs
Normal file
381
AlayaCore/Services/HttpDownloadService.cs
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Abstractions.Interfaces.Clients;
|
||||||
|
using AlayaCore.Abstractions.Interfaces.Policies;
|
||||||
|
using AlayaCore.Abstractions.Interfaces.Services;
|
||||||
|
using AlayaCore.Models.Progress;
|
||||||
|
using AlayaCore.Models.Results;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace AlayaCore.Services
|
||||||
|
{
|
||||||
|
public sealed class HttpDownloadService : IDownloadService
|
||||||
|
{
|
||||||
|
private const int BUFFER_SIZE = 81920;
|
||||||
|
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
|
private readonly IRetryPolicy _retryPolicy;
|
||||||
|
private readonly ILogger<HttpDownloadService> _logger;
|
||||||
|
|
||||||
|
public HttpDownloadService(
|
||||||
|
IHttpClient httpClient,
|
||||||
|
IRetryPolicy retryPolicy,
|
||||||
|
ILogger<HttpDownloadService> logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||||
|
_retryPolicy = retryPolicy ?? throw new ArgumentNullException(nameof(retryPolicy));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DownloadResult> DownloadFileAsync(
|
||||||
|
Uri sourceUri,
|
||||||
|
string destinationPath,
|
||||||
|
string sha512Hash,
|
||||||
|
IProgress<DownloadProgress>? progress = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (sourceUri == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(sourceUri));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sourceUri.IsAbsoluteUri)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Source URI must be absolute.", nameof(sourceUri));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(destinationPath))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Destination path cannot be null, empty, or whitespace.", nameof(destinationPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
string normalizedExpectedHash = NormalizeHash(sha512Hash);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
string fileName = Path.GetFileName(destinationPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(fileName))
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (File.Exists(destinationPath) && VerifyFileHash(destinationPath, normalizedExpectedHash))
|
||||||
|
{
|
||||||
|
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(
|
||||||
|
fileName: fileName,
|
||||||
|
destinationPath: destinationPath,
|
||||||
|
bytesDownloaded: 0,
|
||||||
|
totalBytes: existingLength,
|
||||||
|
bytesPerSecond: null,
|
||||||
|
statusMessage: "File already present and valid."));
|
||||||
|
|
||||||
|
return new DownloadResult(
|
||||||
|
destinationPath,
|
||||||
|
DownloadOutcome.SkippedAlreadyValid,
|
||||||
|
hashVerified: true,
|
||||||
|
bytesDownloaded: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool destinationExisted = File.Exists(destinationPath);
|
||||||
|
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);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DownloadResult result = await _retryPolicy.ExecuteAsync(
|
||||||
|
async token =>
|
||||||
|
{
|
||||||
|
DeleteFileIfExists(tempFilePath);
|
||||||
|
|
||||||
|
using HttpResponseMessage response = await _httpClient.GetAsync(
|
||||||
|
sourceUri,
|
||||||
|
HttpCompletionOption.ResponseHeadersRead,
|
||||||
|
token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
long? totalBytes = response.Content.Headers.ContentLength;
|
||||||
|
long bytesDownloaded = 0;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Download response received for {FileName}. Content-Length: {TotalBytes}.",
|
||||||
|
fileName,
|
||||||
|
totalBytes);
|
||||||
|
|
||||||
|
progress?.Report(new DownloadProgress(
|
||||||
|
fileName: fileName,
|
||||||
|
destinationPath: destinationPath,
|
||||||
|
bytesDownloaded: 0,
|
||||||
|
totalBytes: totalBytes,
|
||||||
|
bytesPerSecond: null,
|
||||||
|
statusMessage: "Starting download..."));
|
||||||
|
|
||||||
|
await using Stream responseStream = await response.Content
|
||||||
|
.ReadAsStreamAsync()
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
await using FileStream fileStream = new FileStream(
|
||||||
|
tempFilePath,
|
||||||
|
FileMode.Create,
|
||||||
|
FileAccess.Write,
|
||||||
|
FileShare.None,
|
||||||
|
BUFFER_SIZE,
|
||||||
|
useAsync: true);
|
||||||
|
|
||||||
|
using SHA512 sha512 = SHA512.Create();
|
||||||
|
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Streaming download content for {FileName} into temporary file {TempFilePath}.",
|
||||||
|
fileName,
|
||||||
|
tempFilePath);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int bytesRead = await responseStream.ReadAsync(
|
||||||
|
buffer,
|
||||||
|
0,
|
||||||
|
buffer.Length,
|
||||||
|
token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fileStream.WriteAsync(
|
||||||
|
buffer,
|
||||||
|
0,
|
||||||
|
bytesRead,
|
||||||
|
token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
sha512.TransformBlock(buffer, 0, bytesRead, null, 0);
|
||||||
|
|
||||||
|
bytesDownloaded += bytesRead;
|
||||||
|
|
||||||
|
double? bytesPerSecond = null;
|
||||||
|
if (stopwatch.Elapsed.TotalSeconds > 0d)
|
||||||
|
{
|
||||||
|
bytesPerSecond = bytesDownloaded / stopwatch.Elapsed.TotalSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress?.Report(new DownloadProgress(
|
||||||
|
fileName: fileName,
|
||||||
|
destinationPath: destinationPath,
|
||||||
|
bytesDownloaded: bytesDownloaded,
|
||||||
|
totalBytes: totalBytes,
|
||||||
|
bytesPerSecond: bytesPerSecond,
|
||||||
|
statusMessage: "Downloading file..."));
|
||||||
|
}
|
||||||
|
|
||||||
|
sha512.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
|
||||||
|
await fileStream.FlushAsync(token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
string actualHash = ConvertToLowerHex(sha512.Hash);
|
||||||
|
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(
|
||||||
|
$"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);
|
||||||
|
|
||||||
|
double? finalBytesPerSecond = null;
|
||||||
|
if (stopwatch.Elapsed.TotalSeconds > 0d)
|
||||||
|
{
|
||||||
|
finalBytesPerSecond = bytesDownloaded / stopwatch.Elapsed.TotalSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress?.Report(new DownloadProgress(
|
||||||
|
fileName: fileName,
|
||||||
|
destinationPath: destinationPath,
|
||||||
|
bytesDownloaded: bytesDownloaded,
|
||||||
|
totalBytes: totalBytes ?? bytesDownloaded,
|
||||||
|
bytesPerSecond: finalBytesPerSecond,
|
||||||
|
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(
|
||||||
|
destinationPath,
|
||||||
|
outcome,
|
||||||
|
hashVerified: true,
|
||||||
|
bytesDownloaded: bytesDownloaded);
|
||||||
|
},
|
||||||
|
$"download:{fileName}",
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool VerifyFileHash(string filePath, string sha512Hash)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("File path cannot be null, empty, or whitespace.", nameof(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
string normalizedExpectedHash = NormalizeHash(sha512Hash);
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Hash verification skipped because file does not exist at {FilePath}.", filePath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
using SHA512 sha512 = SHA512.Create();
|
||||||
|
|
||||||
|
byte[] hashBytes = sha512.ComputeHash(fileStream);
|
||||||
|
string actualHash = ConvertToLowerHex(hashBytes);
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
string? directoryPath = Path.GetDirectoryName(destinationPath);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(directoryPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directoryPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReplaceDestinationFile(string sourcePath, string destinationPath)
|
||||||
|
{
|
||||||
|
DeleteFileIfExists(destinationPath);
|
||||||
|
File.Move(sourcePath, destinationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DeleteFileIfExists(string path)
|
||||||
|
{
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeHash(string hash)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(hash))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("SHA-512 hash cannot be null, empty, or whitespace.", nameof(hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
string normalized = hash.Trim().Replace("-", string.Empty).ToLowerInvariant();
|
||||||
|
|
||||||
|
if (normalized.Length != 128)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("SHA-512 hash must be 128 hexadecimal characters long.", nameof(hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (char c in normalized)
|
||||||
|
{
|
||||||
|
bool isHex =
|
||||||
|
(c >= '0' && c <= '9') ||
|
||||||
|
(c >= 'a' && c <= 'f');
|
||||||
|
|
||||||
|
if (!isHex)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("SHA-512 hash contains invalid characters.", nameof(hash));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ConvertToLowerHex(byte[]? hashBytes)
|
||||||
|
{
|
||||||
|
if (hashBytes == null || hashBytes.Length == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Hash computation returned no data.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return BitConverter.ToString(hashBytes).Replace("-", string.Empty).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,45 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Abstractions.Interfaces;
|
||||||
using AlayaCore.Abstractions.Interfaces.Services;
|
using AlayaCore.Abstractions.Interfaces.Services;
|
||||||
using AlayaCore.Installation;
|
using AlayaCore.Installation;
|
||||||
using AlayaCore.Models.Manifests;
|
using AlayaCore.Models.Manifests;
|
||||||
|
using AlayaCore.Utilities.Enums;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace AlayaCore.Services
|
namespace AlayaCore.Services
|
||||||
{
|
{
|
||||||
public sealed class InstallationStateService : IInstallStateService
|
public sealed class InstallationStateService : IInstallStateService
|
||||||
{
|
{
|
||||||
private const string JavaRuntimeFolderName = "java-runtime-epsilon";
|
private const string VERSIONS_FOLDER_NAME = "versions";
|
||||||
|
|
||||||
|
private readonly IFileStore _fileStore;
|
||||||
private readonly IManifestService _manifestService;
|
private readonly IManifestService _manifestService;
|
||||||
|
private readonly ILogger<InstallationStateService> _logger;
|
||||||
|
|
||||||
public InstallationStateService(IManifestService manifestService)
|
public InstallationStateService(
|
||||||
|
IFileStore fileStore,
|
||||||
|
IManifestService manifestService,
|
||||||
|
ILogger<InstallationStateService> logger)
|
||||||
{
|
{
|
||||||
|
_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);
|
||||||
@@ -33,30 +47,42 @@ 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.");
|
||||||
|
}
|
||||||
|
|
||||||
bool minecraftInstalled = IsMinecraftInstalled();
|
InstalledVersionState versionState = GetInstalledVersionState();
|
||||||
string? minecraftVersion = null;
|
|
||||||
|
|
||||||
bool neoforgeInstalled = IsNeoforgeInstalled();
|
|
||||||
string? neoforgeVersion = null;
|
|
||||||
|
|
||||||
|
|
||||||
InstalledModsManifestModel installedModsManifest =
|
InstalledModsManifestModel installedModsManifest =
|
||||||
await _manifestService.GetInstalledModsManifestAsync(cancellationToken).ConfigureAwait(false)
|
await _manifestService.GetInstalledModsManifestAsync(cancellationToken).ConfigureAwait(false);
|
||||||
?? new InstalledModsManifestModel();
|
|
||||||
|
|
||||||
return new InstallEnvironment(
|
InstallEnvironment environment = new InstallEnvironment(
|
||||||
osPlatform: platform,
|
osPlatform: platform,
|
||||||
javaInstalled: javaInstalled,
|
javaInstalled: javaInstalled,
|
||||||
javaPath: javaPath,
|
javaPath: javaPath,
|
||||||
javaVersion: javaVersion,
|
javaVersion: javaVersion,
|
||||||
minecraftInstalled: minecraftInstalled,
|
minecraftInstalled: !string.IsNullOrWhiteSpace(versionState.MinecraftVersion),
|
||||||
minecraftVersion: minecraftVersion,
|
minecraftVersion: versionState.MinecraftVersion,
|
||||||
neoforgedInstalled: neoforgeInstalled,
|
neoforgedInstalled: !string.IsNullOrWhiteSpace(versionState.NeoForgeVersion),
|
||||||
neoforgedVersion: 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()
|
||||||
@@ -79,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))
|
||||||
{
|
{
|
||||||
@@ -91,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
|
||||||
@@ -111,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)
|
||||||
@@ -131,37 +175,188 @@ namespace AlayaCore.Services
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryGetJavaPath(out string? javaPath)
|
private bool TryGetJavaPath(out string? javaPath)
|
||||||
{
|
{
|
||||||
string executableName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
string executableName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||||
? "java.exe"
|
? "javaw.exe"
|
||||||
: "java";
|
: "java";
|
||||||
|
|
||||||
string fullPath = Path.Combine(
|
string runtimePath = _fileStore.Get(FolderLocation.JavaRuntime);
|
||||||
AppContext.BaseDirectory,
|
string fullPath = Path.Combine(runtimePath, "bin", executableName);
|
||||||
"Java",
|
|
||||||
JavaRuntimeFolderName,
|
|
||||||
"bin",
|
|
||||||
executableName);
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsMinecraftInstalled()
|
private InstalledVersionState GetInstalledVersionState()
|
||||||
{
|
{
|
||||||
return true;
|
string versionsPath = GetVersionsPath();
|
||||||
|
|
||||||
|
_logger.LogDebug("Inspecting installed version metadata under {VersionsPath}.", 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] versionDirectories = Directory
|
||||||
|
.GetDirectories(versionsPath)
|
||||||
|
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (versionDirectories.Length == 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Versions directory at {VersionsPath} was empty.", versionsPath);
|
||||||
|
return InstalledVersionState.Empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
string? minecraftVersion = null;
|
||||||
|
string? neoForgeVersion = null;
|
||||||
|
|
||||||
|
foreach (string versionDirectory in versionDirectories)
|
||||||
|
{
|
||||||
|
string? versionFolderName = Path.GetFileName(versionDirectory);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(versionFolderName))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Skipping version directory with an invalid folder name: {VersionDirectory}.", versionDirectory);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string versionJsonPath = Path.Combine(versionDirectory, $"{versionFolderName}.json");
|
||||||
|
|
||||||
|
if (!File.Exists(versionJsonPath))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Skipping version directory {VersionDirectory} because version metadata file {VersionJsonPath} was not found.", versionDirectory, versionJsonPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryLoadJson(versionJsonPath, out JObject? versionJson))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Skipping version metadata file {VersionJsonPath} because it could not be read or parsed.", versionJsonPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? id = versionJson.Value<string>("id");
|
||||||
|
string? inheritsFrom = versionJson.Value<string>("inheritsFrom");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Skipping version metadata file {VersionJsonPath} because it did not contain a valid id.", versionJsonPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsNeoForgeVersion(id, inheritsFrom))
|
||||||
|
{
|
||||||
|
neoForgeVersion ??= id;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(minecraftVersion) && !string.IsNullOrWhiteSpace(inheritsFrom))
|
||||||
|
{
|
||||||
|
minecraftVersion = inheritsFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Detected NeoForge version metadata. Id: {NeoForgeVersion}, InheritsFrom: {MinecraftVersion}",
|
||||||
|
id,
|
||||||
|
inheritsFrom);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsNeoforgeInstalled()
|
private string GetVersionsPath()
|
||||||
{
|
{
|
||||||
return true;
|
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)
|
||||||
|
{
|
||||||
|
return ContainsNeoForgeMarker(id) || ContainsNeoForgeMarker(inheritsFrom);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ContainsNeoForgeMarker(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Contains("neoforge", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
value.Contains("neoforged", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryLoadJson(string path, out JObject? jsonObject)
|
||||||
|
{
|
||||||
|
jsonObject = null;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to load or parse JSON file at {Path}.", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class InstalledVersionState
|
||||||
|
{
|
||||||
|
public string? MinecraftVersion { get; }
|
||||||
|
public string? NeoForgeVersion { get; }
|
||||||
|
|
||||||
|
public InstalledVersionState(string? minecraftVersion, string? neoForgeVersion)
|
||||||
|
{
|
||||||
|
MinecraftVersion = Normalize(minecraftVersion);
|
||||||
|
NeoForgeVersion = Normalize(neoForgeVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InstalledVersionState Empty()
|
||||||
|
{
|
||||||
|
return new InstalledVersionState(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Normalize(string? value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? null : value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using AlayaCore.Abstractions.Interfaces.Services;
|
|
||||||
using AlayaCore.Installation;
|
|
||||||
using AlayaCore.Models.Manifests;
|
|
||||||
|
|
||||||
namespace AlayaCore.Services
|
|
||||||
{
|
|
||||||
public sealed class JavaService : IJavaService
|
|
||||||
{
|
|
||||||
private const string DownloadFileName = "java-runtime.download";
|
|
||||||
private const string JavaInstallFolderName = "Java";
|
|
||||||
private const string JavaArchiveHashPlaceholder = "REPLACE_WITH_MANIFEST_HASH_SUPPORT";
|
|
||||||
|
|
||||||
private readonly IDownloadService _downloadService;
|
|
||||||
|
|
||||||
public JavaService(IDownloadService downloadService)
|
|
||||||
{
|
|
||||||
_downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task EnsureValidJavaInstalledAsync(
|
|
||||||
ManifestModel manifest,
|
|
||||||
InstallEnvironment environment,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (manifest == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(manifest));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (environment == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(environment));
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
if (environment.JavaInstalled &&
|
|
||||||
string.Equals(environment.JavaVersion, manifest.RequiredJavaVersion, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (environment.JavaInstalled && !string.IsNullOrWhiteSpace(environment.JavaPath))
|
|
||||||
{
|
|
||||||
string javaRootFolder = ResolveJavaRootFolder(environment.JavaPath);
|
|
||||||
await RemoveJavaAsync(javaRootFolder, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
string downloadPath = GetJavaDownloadPath();
|
|
||||||
string installDirectory = GetJavaInstallDirectory();
|
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(downloadPath) ?? AppContext.BaseDirectory);
|
|
||||||
Directory.CreateDirectory(installDirectory);
|
|
||||||
|
|
||||||
await DownloadJavaAsync(
|
|
||||||
manifest.RequiredJavaUrl,
|
|
||||||
downloadPath,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
await InstallJavaAsync(
|
|
||||||
downloadPath,
|
|
||||||
installDirectory,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetJavaInstallDirectory()
|
|
||||||
{
|
|
||||||
return Path.Combine(AppContext.BaseDirectory, JavaInstallFolderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetJavaDownloadPath()
|
|
||||||
{
|
|
||||||
return Path.Combine(AppContext.BaseDirectory, "Temp", DownloadFileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ResolveJavaRootFolder(string javaExecutablePath)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(javaExecutablePath))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Java executable path cannot be null, empty, or whitespace.", nameof(javaExecutablePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
string? binFolder = Path.GetDirectoryName(javaExecutablePath);
|
|
||||||
if (string.IsNullOrWhiteSpace(binFolder))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Could not resolve the Java bin directory.");
|
|
||||||
}
|
|
||||||
|
|
||||||
string? javaRootFolder = Path.GetDirectoryName(binFolder);
|
|
||||||
if (string.IsNullOrWhiteSpace(javaRootFolder))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Could not resolve the Java installation directory.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return javaRootFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task RemoveJavaAsync(
|
|
||||||
string oldJavaFolder,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(oldJavaFolder))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Old Java folder cannot be null, empty, or whitespace.", nameof(oldJavaFolder));
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
if (Directory.Exists(oldJavaFolder))
|
|
||||||
{
|
|
||||||
Directory.Delete(oldJavaFolder, recursive: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DownloadJavaAsync(
|
|
||||||
Uri javaUri,
|
|
||||||
string destinationPath,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (javaUri == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(javaUri));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!javaUri.IsAbsoluteUri)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Java download URI must be absolute.", nameof(javaUri));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(destinationPath))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Destination path cannot be null, empty, or whitespace.", nameof(destinationPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
// Replace this when your manifest includes a Java archive/runtime hash.
|
|
||||||
await _downloadService.DownloadFileAsync(
|
|
||||||
javaUri,
|
|
||||||
destinationPath,
|
|
||||||
JavaArchiveHashPlaceholder,
|
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task InstallJavaAsync(
|
|
||||||
string installerPath,
|
|
||||||
string installDirectory,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(installerPath))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Installer path cannot be null, empty, or whitespace.", nameof(installerPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(installDirectory))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Install directory cannot be null, empty, or whitespace.", nameof(installDirectory));
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
if (!File.Exists(installerPath))
|
|
||||||
{
|
|
||||||
throw new FileNotFoundException("Java installer or archive was not found.", installerPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.CreateDirectory(installDirectory);
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// Implement this based on the actual Java package format.
|
|
||||||
//
|
|
||||||
// Examples:
|
|
||||||
// - If the file is a .zip, extract it into installDirectory
|
|
||||||
// - If it is a .tar.gz, unpack it appropriately
|
|
||||||
// - If it is an installer executable, launch it silently
|
|
||||||
//
|
|
||||||
// For now this is intentionally left explicit rather than guessing the wrong install strategy.
|
|
||||||
throw new NotImplementedException("InstallJavaAsync must be implemented for the actual Java package format.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,104 +6,179 @@ 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
|
||||||
{
|
{
|
||||||
public sealed class LauncherUpdateService : IUpdateService
|
public sealed class LauncherUpdateService : IUpdateService
|
||||||
{
|
{
|
||||||
private readonly IManifestService _manifestService;
|
private readonly IManifestService _manifestService;
|
||||||
private readonly UpdateServiceOptions _options;
|
private readonly LauncherUpdateServiceOptions _options;
|
||||||
|
private readonly ILogger<LauncherUpdateService> _logger;
|
||||||
|
|
||||||
public LauncherUpdateService(
|
public LauncherUpdateService(
|
||||||
IManifestService manifestService,
|
IManifestService manifestService,
|
||||||
UpdateServiceOptions 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
string remoteHash = await _manifestService
|
LauncherManifestModel remoteManifest = await _manifestService
|
||||||
.GetRemoteLauncherManifestHashAsync(cancellationToken)
|
.GetRemoteLauncherManifestAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(remoteHash))
|
if (remoteManifest == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Remote launcher manifest could not be loaded.");
|
||||||
|
throw new InvalidOperationException("Remote launcher manifest could not be loaded.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return !string.Equals(
|
bool versionMismatch = localManifest.Version != remoteManifest.Version;
|
||||||
|
bool hashMismatch = !string.Equals(
|
||||||
localManifest.Sha512Hash.Trim(),
|
localManifest.Sha512Hash.Trim(),
|
||||||
remoteHash.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task LaunchUpdaterAsync(LauncherManifestModel newManifest, CancellationToken cancellationToken = default)
|
public Task LaunchUpdaterAsync(
|
||||||
|
LauncherManifestModel newManifest,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (newManifest == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(newManifest));
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
string updaterPath = _options.AlayaUpdaterPath;
|
string updaterPath = _options.AlayaUpdaterPath;
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
string workingDirectory = Path.GetDirectoryName(updaterPath)
|
string workingDirectory = Path.GetDirectoryName(updaterPath)
|
||||||
?? throw new InvalidOperationException("Updater working directory could not be resolved.");
|
?? throw new InvalidOperationException("Updater working directory could not be resolved.");
|
||||||
|
|
||||||
|
string arguments = $"-url \"{newManifest.DownloadUri.AbsoluteUri}\"";
|
||||||
|
|
||||||
var startInfo = new ProcessStartInfo
|
var startInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = updaterPath,
|
FileName = updaterPath,
|
||||||
WorkingDirectory = workingDirectory,
|
WorkingDirectory = workingDirectory,
|
||||||
Arguments = $@"-url {newManifest.DownloadUri}",
|
Arguments = arguments,
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
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 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,54 +3,76 @@ using System.IO;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Abstractions.Interfaces;
|
||||||
using AlayaCore.Abstractions.Interfaces.Clients;
|
using AlayaCore.Abstractions.Interfaces.Clients;
|
||||||
|
using AlayaCore.Abstractions.Interfaces.Policies;
|
||||||
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 AlayaCore.Models.Manifests.DTO;
|
using AlayaCore.Models.Manifests.DTO;
|
||||||
|
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 ManifestServiceOptions _options;
|
private readonly ManifestServiceOptions _options;
|
||||||
|
private readonly IRetryPolicy _retryPolicy;
|
||||||
|
private readonly ILogger<ManifestService> _logger;
|
||||||
|
|
||||||
public ManifestService(
|
public ManifestService(
|
||||||
IDownloadService downloadService,
|
IDownloadService downloadService,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
ManifestServiceOptions options)
|
IFileStore fileStore,
|
||||||
|
ManifestServiceOptions options,
|
||||||
|
IRetryPolicy retryPolicy,
|
||||||
|
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));
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
_retryPolicy = retryPolicy ?? throw new ArgumentNullException(nameof(retryPolicy));
|
||||||
|
_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.AlayaManifestUri,
|
||||||
|
destinationPath);
|
||||||
|
|
||||||
return DownloadAndLoadManifestAsync<ManifestDto, ManifestModel>(
|
return DownloadAndLoadManifestAsync<ManifestDto, ManifestModel>(
|
||||||
_options.CoreManifestUri,
|
_options.AlayaManifestUri,
|
||||||
destinationPath,
|
destinationPath,
|
||||||
_options.CoreManifestSha512Hash,
|
_options.AlayaManifestSha512Hash,
|
||||||
static dto => dto.ToModel(),
|
static dto => dto.ToModel(),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<InstalledModsManifestModel> GetInstalledModsManifestAsync(CancellationToken cancellationToken = default)
|
public async Task<InstalledModsManifestModel> GetInstalledModsManifestAsync(
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,22 +80,40 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
InstalledModsManifestModel? manifest = DeserializeAndMapManifest<InstalledModsManifestDto, InstalledModsManifestModel>(
|
InstalledModsManifestModel? manifest =
|
||||||
json,
|
DeserializeAndMapManifest<InstalledModsManifestDto, InstalledModsManifestModel>(
|
||||||
path,
|
json,
|
||||||
static dto => dto.ToModel(),
|
path,
|
||||||
swallowDeserializationErrors: true);
|
static dto => dto.ToModel(),
|
||||||
|
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,
|
||||||
@@ -82,103 +122,105 @@ 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)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
_logger.LogDebug("Fetching remote Alaya manifest version from {ManifestUri}.", _options.AlayaManifestUri);
|
||||||
|
|
||||||
using HttpResponseMessage response = await _httpClient.GetAsync(
|
ManifestModel remoteManifest = await GetRemoteManifestAsync<ManifestDto, ManifestModel>(
|
||||||
_options.CoreManifestUri,
|
_options.AlayaManifestUri,
|
||||||
HttpCompletionOption.ResponseContentRead,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
|
||||||
{
|
|
||||||
throw new InvalidDataException(
|
|
||||||
$"Remote core manifest response from '{_options.CoreManifestUri}' was empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ManifestModel remoteManifest = DeserializeAndMapManifest<ManifestDto, ManifestModel>(
|
|
||||||
json,
|
|
||||||
_options.CoreManifestUri.ToString(),
|
|
||||||
static dto => dto.ToModel(),
|
static dto => dto.ToModel(),
|
||||||
swallowDeserializationErrors: false)!;
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (remoteManifest.AlayaVersion == null)
|
if (remoteManifest.AlayaVersion == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Remote Alaya manifest from {ManifestUri} did not contain a valid version.", _options.AlayaManifestUri);
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"Remote core manifest from '{_options.CoreManifestUri}' does not contain a valid version.");
|
$"Remote core manifest from '{_options.AlayaManifestUri}' does not contain a valid version.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Fetched remote Alaya manifest version {RemoteVersion} from {ManifestUri}.",
|
||||||
|
remoteManifest.AlayaVersion,
|
||||||
|
_options.AlayaManifestUri);
|
||||||
|
|
||||||
return remoteManifest.AlayaVersion;
|
return remoteManifest.AlayaVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GetRemoteLauncherManifestHashAsync(CancellationToken cancellationToken = default)
|
public async Task<LauncherManifestModel> GetRemoteLauncherManifestAsync(
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
_logger.LogDebug("Fetching remote launcher manifest from {ManifestUri}.", _options.LauncherManifestUri);
|
||||||
|
|
||||||
using HttpResponseMessage response = await _httpClient.GetAsync(
|
LauncherManifestModel remoteManifest = await GetRemoteManifestAsync<LauncherManifestDto, LauncherManifestModel>(
|
||||||
_options.LauncherManifestUri,
|
_options.LauncherManifestUri,
|
||||||
HttpCompletionOption.ResponseContentRead,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
|
||||||
{
|
|
||||||
throw new InvalidDataException(
|
|
||||||
$"Remote launcher manifest response from '{_options.LauncherManifestUri}' was empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
LauncherManifestModel remoteManifest = DeserializeAndMapManifest<LauncherManifestDto, LauncherManifestModel>(
|
|
||||||
json,
|
|
||||||
_options.LauncherManifestUri.ToString(),
|
|
||||||
static dto => dto.ToModel(),
|
static dto => dto.ToModel(),
|
||||||
swallowDeserializationErrors: false)!;
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return remoteManifest.Sha512Hash.Trim();
|
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(
|
||||||
|
$"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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetLauncherManifestPath()
|
public string GetLauncherManifestPath()
|
||||||
{
|
{
|
||||||
return Path.Combine(_options.ManifestDirectoryPath, 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(_options.ManifestDirectoryPath, 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(_options.ManifestDirectoryPath, 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>(
|
||||||
@@ -199,6 +241,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,14 +249,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>(
|
||||||
@@ -253,14 +308,26 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
EnsureDirectoryExists(destinationPath);
|
EnsureDirectoryExists(destinationPath);
|
||||||
|
|
||||||
await _downloadService.DownloadFileAsync(
|
_logger.LogInformation(
|
||||||
|
"Downloading manifest from {ManifestUri} to {DestinationPath}.",
|
||||||
manifestUri,
|
manifestUri,
|
||||||
destinationPath,
|
destinationPath);
|
||||||
sha512Hash,
|
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
await _retryPolicy.ExecuteAsync(
|
||||||
|
async token =>
|
||||||
|
{
|
||||||
|
await _downloadService.DownloadFileAsync(
|
||||||
|
manifestUri,
|
||||||
|
destinationPath,
|
||||||
|
sha512Hash,
|
||||||
|
cancellationToken: token).ConfigureAwait(false);
|
||||||
|
},
|
||||||
|
$"manifest-download:{Path.GetFileName(destinationPath)}",
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,17 +335,83 @@ 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 static TModel? DeserializeAndMapManifest<TDto, TModel>(
|
private async Task<TModel> GetRemoteManifestAsync<TDto, TModel>(
|
||||||
|
Uri manifestUri,
|
||||||
|
Func<TDto, TModel> map,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
where TDto : class
|
||||||
|
{
|
||||||
|
if (manifestUri == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(manifestUri));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manifestUri.IsAbsoluteUri)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Manifest URI must be absolute.", nameof(manifestUri));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(map));
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogDebug("Fetching remote manifest from {ManifestUri}.", manifestUri);
|
||||||
|
|
||||||
|
return await _retryPolicy.ExecuteAsync(
|
||||||
|
async token =>
|
||||||
|
{
|
||||||
|
using HttpResponseMessage response = await _httpClient.GetAsync(
|
||||||
|
manifestUri,
|
||||||
|
HttpCompletionOption.ResponseContentRead,
|
||||||
|
token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
_logger.LogError("Remote manifest response from {ManifestUri} was empty.", manifestUri);
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"Remote manifest response from '{manifestUri}' was empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
TModel model = DeserializeAndMapManifest(
|
||||||
|
json,
|
||||||
|
manifestUri.ToString(),
|
||||||
|
map,
|
||||||
|
swallowDeserializationErrors: false)!;
|
||||||
|
|
||||||
|
_logger.LogDebug("Successfully fetched and mapped remote manifest from {ManifestUri}.", manifestUri);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
},
|
||||||
|
$"manifest-fetch:{manifestUri.AbsolutePath}",
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TModel? DeserializeAndMapManifest<TDto, TModel>(
|
||||||
string json,
|
string json,
|
||||||
string sourceName,
|
string sourceName,
|
||||||
Func<TDto, TModel> map,
|
Func<TDto, TModel> map,
|
||||||
@@ -289,9 +422,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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,9 +449,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);
|
||||||
@@ -326,24 +463,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 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);
|
||||||
|
|||||||
@@ -5,13 +5,19 @@ using System.Linq;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Abstractions.Interfaces;
|
||||||
using AlayaCore.Abstractions.Interfaces.Clients;
|
using AlayaCore.Abstractions.Interfaces.Clients;
|
||||||
|
using AlayaCore.Abstractions.Interfaces.Policies;
|
||||||
using AlayaCore.Abstractions.Interfaces.Services;
|
using AlayaCore.Abstractions.Interfaces.Services;
|
||||||
using AlayaCore.Installation;
|
using AlayaCore.Installation;
|
||||||
using AlayaCore.Models;
|
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.Progress;
|
||||||
using AlayaCore.Utilities.Enums;
|
using AlayaCore.Utilities.Enums;
|
||||||
|
using AlayaCore.Utilities.Extensions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
@@ -19,25 +25,35 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
public sealed class ModService : IModService
|
public sealed class ModService : IModService
|
||||||
{
|
{
|
||||||
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 ModrinthConnectionOptions _options;
|
private readonly ModrinthConnectionOptions _options;
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClient _httpClient;
|
||||||
|
private readonly IFileStore _fileStore;
|
||||||
|
private readonly IRetryPolicy _retryPolicy;
|
||||||
|
private readonly ILogger<ModService> _logger;
|
||||||
|
|
||||||
public ModService(
|
public ModService(
|
||||||
IDownloadService downloadService,
|
IDownloadService downloadService,
|
||||||
ModrinthConnectionOptions options,
|
ModrinthConnectionOptions options,
|
||||||
IHttpClient httpClient)
|
IHttpClient httpClient,
|
||||||
|
IFileStore fileStore,
|
||||||
|
IRetryPolicy retryPolicy,
|
||||||
|
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));
|
||||||
|
_retryPolicy = retryPolicy ?? throw new ArgumentNullException(nameof(retryPolicy));
|
||||||
|
_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)
|
||||||
@@ -52,79 +68,105 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
IReadOnlyList<InstalledModEntry> installedMods = environment.InstalledModsManifest.Mods;
|
IReadOnlyList<ModFileEntry> installedMods = environment.InstalledModsManifest.Mods;
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
await RemoveStaleModsAsync(requiredMods, cancellationToken).ConfigureAwait(false);
|
_logger.LogInformation(
|
||||||
|
"Starting mod sync. RequiredMods: {RequiredModCount}, InstalledModsManifestEntries: {InstalledModCount}",
|
||||||
|
requiredMods.Count,
|
||||||
|
installedMods.Count);
|
||||||
|
|
||||||
List<ModFileEntry> missingOrOutdatedMods = new List<ModFileEntry>();
|
RemoveStaleMods(requiredMods, cancellationToken);
|
||||||
List<InstalledModEntry> finalInstalledMods = new List<InstalledModEntry>();
|
|
||||||
|
List<ModFileEntry> finalInstalledMods = new List<ModFileEntry>();
|
||||||
|
|
||||||
foreach (ModFileEntry requiredMod in requiredMods)
|
foreach (ModFileEntry requiredMod in requiredMods)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
InstalledModEntry? installedMod = installedMods.FirstOrDefault(
|
_logger.LogDebug(
|
||||||
|
"Processing required mod {FileName}. Expected SHA-512: {Sha512Hash}, Expected Size: {Size}",
|
||||||
|
requiredMod.FileName,
|
||||||
|
requiredMod.Sha512Hash,
|
||||||
|
requiredMod.Size);
|
||||||
|
|
||||||
|
ModFileEntry? installedMod = installedMods.FirstOrDefault(
|
||||||
mod => string.Equals(mod.FileName, requiredMod.FileName, StringComparison.OrdinalIgnoreCase));
|
mod => string.Equals(mod.FileName, requiredMod.FileName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
string destinationPath = GetModDestinationPath(requiredMod);
|
string destinationPath = GetModDestinationPath(requiredMod);
|
||||||
|
|
||||||
|
bool isValidInstalledMod =
|
||||||
|
installedMod != null &&
|
||||||
|
IsInstalledModUpToDate(installedMod, requiredMod) &&
|
||||||
|
File.Exists(destinationPath) &&
|
||||||
|
_downloadService.VerifyFileHash(destinationPath, requiredMod.Sha512Hash);
|
||||||
|
|
||||||
|
if (isValidInstalledMod)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Mod {FileName} is already installed and valid. Skipping download.",
|
||||||
|
requiredMod.FileName);
|
||||||
|
|
||||||
|
finalInstalledMods.Add(installedMod!);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (installedMod == null)
|
if (installedMod == null)
|
||||||
{
|
{
|
||||||
missingOrOutdatedMods.Add(requiredMod);
|
_logger.LogInformation(
|
||||||
continue;
|
"Mod {FileName} is missing locally and will be downloaded.",
|
||||||
|
requiredMod.FileName);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (!IsInstalledModUpToDate(installedMod, requiredMod))
|
|
||||||
{
|
{
|
||||||
missingOrOutdatedMods.Add(requiredMod);
|
_logger.LogWarning(
|
||||||
continue;
|
"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);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!File.Exists(destinationPath))
|
Uri modUri = await ResolveModUrlAsync(requiredMod, cancellationToken).ConfigureAwait(false);
|
||||||
{
|
|
||||||
missingOrOutdatedMods.Add(requiredMod);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hashMatches = _downloadService.VerifyFileHash(destinationPath, requiredMod.Sha512Hash);
|
_logger.LogInformation(
|
||||||
if (!hashMatches)
|
"Downloading mod {FileName} from {ModUri} to {DestinationPath}.",
|
||||||
{
|
requiredMod.FileName,
|
||||||
missingOrOutdatedMods.Add(requiredMod);
|
modUri,
|
||||||
continue;
|
destinationPath);
|
||||||
}
|
|
||||||
|
|
||||||
finalInstalledMods.Add(installedMod);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (ModFileEntry mod in missingOrOutdatedMods)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
ModrinthModInfoModel model = await ResolveModUrlAsync(mod, cancellationToken).ConfigureAwait(false);
|
|
||||||
string destinationPath = GetModDestinationPath(mod);
|
|
||||||
|
|
||||||
await _downloadService.DownloadFileAsync(
|
await _downloadService.DownloadFileAsync(
|
||||||
model.ModUrl,
|
modUri,
|
||||||
destinationPath,
|
destinationPath,
|
||||||
mod.Sha512Hash,
|
requiredMod.Sha512Hash,
|
||||||
|
progress,
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
finalInstalledMods.Add(new InstalledModEntry(
|
_logger.LogInformation(
|
||||||
mod.FileName,
|
"Download completed successfully for mod {FileName}.",
|
||||||
mod.ModrinthVersionId ?? string.Empty,
|
requiredMod.FileName);
|
||||||
mod.Sha512Hash,
|
|
||||||
model.Size));
|
finalInstalledMods.Add(new ModFileEntry(
|
||||||
|
requiredMod.FileName,
|
||||||
|
requiredMod.Type,
|
||||||
|
requiredMod.Sha512Hash,
|
||||||
|
requiredMod.Size));
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
||||||
InstalledModEntry installedMod,
|
ModFileEntry installedMod,
|
||||||
ModFileEntry requiredMod)
|
ModFileEntry requiredMod)
|
||||||
{
|
{
|
||||||
if (installedMod == null)
|
if (installedMod == null)
|
||||||
@@ -137,27 +179,20 @@ namespace AlayaCore.Services
|
|||||||
throw new ArgumentNullException(nameof(requiredMod));
|
throw new ArgumentNullException(nameof(requiredMod));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(requiredMod.ModrinthVersionId))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(requiredMod.Sha512Hash))
|
if (string.IsNullOrWhiteSpace(requiredMod.Sha512Hash))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Equals(
|
return string.Equals(
|
||||||
installedMod.Version,
|
|
||||||
requiredMod.ModrinthVersionId,
|
|
||||||
StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& string.Equals(
|
|
||||||
installedMod.Sha512Hash,
|
installedMod.Sha512Hash,
|
||||||
requiredMod.Sha512Hash,
|
requiredMod.Sha512Hash,
|
||||||
StringComparison.OrdinalIgnoreCase);
|
StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& installedMod.Size == requiredMod.Size
|
||||||
|
&& installedMod.Type == requiredMod.Type;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ModrinthModInfoModel> ResolveModUrlAsync(
|
private async Task<Uri> ResolveModUrlAsync(
|
||||||
ModFileEntry fileEntry,
|
ModFileEntry fileEntry,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -166,79 +201,169 @@ namespace AlayaCore.Services
|
|||||||
throw new ArgumentNullException(nameof(fileEntry));
|
throw new ArgumentNullException(nameof(fileEntry));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(fileEntry.ModrinthId))
|
if (string.IsNullOrWhiteSpace(fileEntry.Sha512Hash))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Mod '{fileEntry.FileName}' does not contain a Modrinth ID.");
|
_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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(fileEntry.ModrinthVersionId))
|
string versionEndpoint = BuildVersionEndpoint(fileEntry.Sha512Hash);
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Mod '{fileEntry.FileName}' does not contain a Modrinth version ID.");
|
|
||||||
}
|
|
||||||
|
|
||||||
string versionEndpoint = BuildVersionEndpoint(fileEntry.ModrinthId, fileEntry.ModrinthVersionId);
|
_logger.LogDebug(
|
||||||
|
"Resolving mod URL for {FileName} using Modrinth endpoint {VersionEndpoint}.",
|
||||||
|
fileEntry.FileName,
|
||||||
|
versionEndpoint);
|
||||||
|
|
||||||
using HttpResponseMessage response = await _httpClient.GetAsync(
|
return await _retryPolicy.ExecuteAsync(
|
||||||
new Uri(versionEndpoint, UriKind.Absolute),
|
async token =>
|
||||||
HttpCompletionOption.ResponseContentRead,
|
{
|
||||||
|
using HttpResponseMessage response = await _httpClient.GetAsync(
|
||||||
|
new Uri(versionEndpoint, UriKind.Absolute),
|
||||||
|
HttpCompletionOption.ResponseContentRead,
|
||||||
|
token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
JObject jsonObject = JObject.Parse(json);
|
||||||
|
|
||||||
|
JArray? filesArray = jsonObject["files"] as JArray;
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
JObject? selectedFile = filesArray
|
||||||
|
.OfType<JObject>()
|
||||||
|
.FirstOrDefault(file => file.Value<bool?>("primary") == true)
|
||||||
|
?? filesArray.OfType<JObject>().FirstOrDefault();
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
JObject? hashesObject = selectedFile["hashes"] as JObject;
|
||||||
|
if (hashesObject == null)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Mod metadata response for {FileName} did not contain a hashes object.",
|
||||||
|
fileEntry.FileName);
|
||||||
|
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"The mod metadata response for '{fileEntry.FileName}' did not contain a hashes object.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string? remoteSha512Hash = hashesObject.Value<string>("sha512");
|
||||||
|
if (string.IsNullOrWhiteSpace(remoteSha512Hash))
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Mod metadata response for {FileName} did not contain a valid SHA-512 hash.",
|
||||||
|
fileEntry.FileName);
|
||||||
|
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid SHA-512 hash.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
$"The mod metadata SHA-512 hash for '{fileEntry.FileName}' did not match the required manifest hash.");
|
||||||
|
}
|
||||||
|
|
||||||
|
long? size = selectedFile.Value<long?>("size");
|
||||||
|
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(
|
||||||
|
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file 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(
|
||||||
|
$"The mod metadata size for '{fileEntry.FileName}' did not match the required manifest size.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string? modUrl = selectedFile.Value<string>("url");
|
||||||
|
if (string.IsNullOrWhiteSpace(modUrl))
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Mod metadata response for {FileName} did not contain a valid file URL.",
|
||||||
|
fileEntry.FileName);
|
||||||
|
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file URL.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
$"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;
|
||||||
|
},
|
||||||
|
$"mod-metadata:{fileEntry.FileName}",
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
|
||||||
{
|
|
||||||
throw new InvalidDataException($"The Modrinth version response for '{fileEntry.FileName}' was empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
JObject jsonObject = JObject.Parse(json);
|
|
||||||
|
|
||||||
JArray? filesArray = jsonObject["files"] as JArray;
|
|
||||||
if (filesArray == null || filesArray.Count == 0)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException($"The Modrinth version response for '{fileEntry.FileName}' did not contain any files.");
|
|
||||||
}
|
|
||||||
|
|
||||||
JObject? selectedFile = filesArray
|
|
||||||
.OfType<JObject>()
|
|
||||||
.FirstOrDefault(file => file.Value<bool?>("primary") == true)
|
|
||||||
?? filesArray.OfType<JObject>().FirstOrDefault();
|
|
||||||
|
|
||||||
if (selectedFile == null)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException($"The Modrinth version response for '{fileEntry.FileName}' did not contain a usable file entry.");
|
|
||||||
}
|
|
||||||
|
|
||||||
string? modUrl = selectedFile.Value<string>("url");
|
|
||||||
if (string.IsNullOrWhiteSpace(modUrl))
|
|
||||||
{
|
|
||||||
throw new InvalidDataException($"The Modrinth version response for '{fileEntry.FileName}' did not contain a valid file URL.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Uri.TryCreate(modUrl, UriKind.Absolute, out Uri? result))
|
|
||||||
{
|
|
||||||
throw new InvalidDataException($"The Modrinth version response for '{fileEntry.FileName}' contained an invalid file URL.");
|
|
||||||
}
|
|
||||||
|
|
||||||
long? size = selectedFile.Value<long?>("size");
|
|
||||||
if (!size.HasValue || size.Value <= 0)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException($"The Modrinth version response for '{fileEntry.FileName}' did not contain a valid file size.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ModrinthModInfoModel(result, size.Value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildVersionEndpoint(string modrinthId, string modrinthVersionId)
|
private string BuildVersionEndpoint(string sha512Hash)
|
||||||
{
|
{
|
||||||
string baseUrl = _options.BaseApiUrl?.TrimEnd('/')
|
string baseUrl = _options.BaseApiUrl?.TrimEnd('/')
|
||||||
?? throw new InvalidOperationException("Modrinth base API URL is not configured.");
|
?? throw new InvalidOperationException("Modrinth base API URL is not configured.");
|
||||||
|
|
||||||
return $"{baseUrl}/project/{modrinthId}/version/{modrinthVersionId}";
|
return $"{baseUrl}/version_file/{sha512Hash}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetModDestinationPath(ModFileEntry fileEntry)
|
private string GetModDestinationPath(ModFileEntry fileEntry)
|
||||||
{
|
{
|
||||||
if (fileEntry == null)
|
if (fileEntry == null)
|
||||||
{
|
{
|
||||||
@@ -251,30 +376,37 @@ namespace AlayaCore.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
string modsDirectory = GetModsDirectoryPath();
|
string modsDirectory = GetModsDirectoryPath();
|
||||||
Directory.CreateDirectory(modsDirectory);
|
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 static string GetModsDirectoryPath()
|
private string GetModsDirectoryPath()
|
||||||
{
|
{
|
||||||
return Path.Combine(AppContext.BaseDirectory, "Game", "mods");
|
string modsDirectory = _fileStore.GetOrCreate(FolderLocation.Mods);
|
||||||
|
_logger.LogDebug("Resolved mods directory to {ModsDirectory}.", modsDirectory);
|
||||||
|
return modsDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task RemoveStaleModsAsync(
|
private void RemoveStaleMods(
|
||||||
IEnumerable<ModFileEntry> requiredMods,
|
IEnumerable<ModFileEntry> requiredMods,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (requiredMods == null)
|
if (requiredMods == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(requiredMods));
|
throw new ArgumentNullException(nameof(requiredMods));
|
||||||
}
|
}
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
string modsDirectory = _fileStore.Get(FolderLocation.Mods);
|
||||||
|
|
||||||
string modsDirectory = GetModsDirectoryPath();
|
|
||||||
if (!Directory.Exists(modsDirectory))
|
if (!Directory.Exists(modsDirectory))
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Mods directory {ModsDirectory} does not exist. No stale mods need removal.", modsDirectory);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,15 +423,18 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task FlushInstalledModsManifestAsync(
|
private async Task FlushInstalledModsManifestAsync(
|
||||||
IEnumerable<InstalledModEntry> installedMods,
|
IEnumerable<ModFileEntry> installedMods,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (installedMods == null)
|
if (installedMods == null)
|
||||||
@@ -307,33 +442,37 @@ namespace AlayaCore.Services
|
|||||||
throw new ArgumentNullException(nameof(installedMods));
|
throw new ArgumentNullException(nameof(installedMods));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<InstalledModEntry> entries = installedMods.ToList();
|
List<ModFileEntry> entries = installedMods.ToList();
|
||||||
|
|
||||||
InstalledModsManifestModel manifest = new InstalledModsManifestModel(entries);
|
InstalledModsManifestModel manifest = new InstalledModsManifestModel(entries);
|
||||||
|
|
||||||
string manifestsDirectory = Path.Combine(AppContext.BaseDirectory, "Manifests");
|
string manifestsDirectory = _fileStore.GetOrCreate(FolderLocation.Manifests);
|
||||||
Directory.CreateDirectory(manifestsDirectory);
|
|
||||||
|
|
||||||
string manifestPath = Path.Combine(manifestsDirectory, InstalledModsManifestFileName);
|
string manifestPath = Path.Combine(manifestsDirectory, INSTALLED_MODS_MANIFEST_FILE_NAME);
|
||||||
string temporaryManifestPath = manifestPath + ".tmp";
|
string temporaryManifestPath = manifestPath + ".tmp";
|
||||||
|
|
||||||
string json = JsonConvert.SerializeObject(
|
InstalledModsManifestDto dto = manifest.ToDto();
|
||||||
new
|
|
||||||
{
|
string json = JsonConvert.SerializeObject(dto, Formatting.Indented);
|
||||||
mods = manifest.Mods.Select(mod => new
|
|
||||||
{
|
_logger.LogDebug(
|
||||||
fileName = mod.FileName,
|
"Writing installed mods manifest to temporary path {TemporaryManifestPath}. EntryCount: {EntryCount}",
|
||||||
version = mod.Version,
|
temporaryManifestPath,
|
||||||
sha512Hash = mod.Sha512Hash,
|
entries.Count);
|
||||||
size = mod.Size
|
|
||||||
})
|
|
||||||
},
|
|
||||||
Formatting.Indented);
|
|
||||||
|
|
||||||
await File.WriteAllTextAsync(temporaryManifestPath, json, cancellationToken).ConfigureAwait(false);
|
await File.WriteAllTextAsync(temporaryManifestPath, json, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
File.Copy(temporaryManifestPath, manifestPath, overwrite: true);
|
if (File.Exists(manifestPath))
|
||||||
File.Delete(temporaryManifestPath);
|
{
|
||||||
|
_logger.LogDebug("Deleting previous installed mods manifest at {ManifestPath}.", manifestPath);
|
||||||
|
File.Delete(manifestPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Move(temporaryManifestPath, manifestPath);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Installed mods manifest updated successfully at {ManifestPath}. EntryCount: {EntryCount}",
|
||||||
|
manifestPath,
|
||||||
|
entries.Count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
458
AlayaCore/Services/SettingsService.cs
Normal file
458
AlayaCore/Services/SettingsService.cs
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Abstractions.Configuration;
|
||||||
|
using AlayaCore.Abstractions.Interfaces;
|
||||||
|
using AlayaCore.Abstractions.Interfaces.Services;
|
||||||
|
using AlayaCore.Models.Configuration;
|
||||||
|
using AlayaCore.Utilities.Enums;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace AlayaCore.Services
|
||||||
|
{
|
||||||
|
public sealed class SettingsService : ISettingsService
|
||||||
|
{
|
||||||
|
private readonly IFileStore _fileStore;
|
||||||
|
private readonly ILogger<SettingsService> _logger;
|
||||||
|
|
||||||
|
public LauncherOptions LauncherOptions { get; }
|
||||||
|
public GameOptions GameOptions { get; }
|
||||||
|
|
||||||
|
public SettingsService(
|
||||||
|
LauncherOptions launcherOptions,
|
||||||
|
GameOptions gameOptions,
|
||||||
|
IFileStore fileStore,
|
||||||
|
ILogger<SettingsService> logger)
|
||||||
|
{
|
||||||
|
LauncherOptions = launcherOptions ?? throw new ArgumentNullException(nameof(launcherOptions));
|
||||||
|
GameOptions = gameOptions ?? throw new ArgumentNullException(nameof(gameOptions));
|
||||||
|
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetForceReinstallAsync(bool value, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Updating launcher option ForceReinstall to {ForceReinstall}.", value);
|
||||||
|
|
||||||
|
LauncherOptions.ForceReinstall = value;
|
||||||
|
|
||||||
|
await SaveLauncherOptionsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Launcher option ForceReinstall was updated and saved successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateLaunchVersionAsync(string newVersion, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
_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)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Saving launcher options to disk.");
|
||||||
|
|
||||||
|
await SaveAsync(
|
||||||
|
LauncherOptions.FileName,
|
||||||
|
LauncherOptions,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Launcher options were saved successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadLauncherOptionsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Loading launcher options from disk.");
|
||||||
|
|
||||||
|
LauncherOptions? loadedOptions = await LoadAsync<LauncherOptions>(
|
||||||
|
LauncherOptions.FileName,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (loadedOptions == null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No launcher options file was found or it was empty. Existing in-memory launcher options will be kept.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LauncherOptions.ForceReinstall = loadedOptions.ForceReinstall;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Launcher options were loaded successfully. ForceReinstall: {ForceReinstall}",
|
||||||
|
LauncherOptions.ForceReinstall);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
GameOptions.FileName,
|
||||||
|
GameOptions,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Game options were saved successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadGameOptionsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Loading game options from disk.");
|
||||||
|
|
||||||
|
GameOptions? loadedOptions = await LoadAsync<GameOptions>(
|
||||||
|
GameOptions.FileName,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (loadedOptions == null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No game options file was found or it was empty. Applying default game options.");
|
||||||
|
ApplyGameOptions(GameOptions.Default);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyGameOptions(loadedOptions);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Game options were loaded successfully. LaunchVersion: {LaunchVersion}, MinimumRamMb: {MinimumRamMb}, MaximumRamMb: {MaximumRamMb}, Resolution: {ScreenWidth}x{ScreenHeight}, Fullscreen: {Fullscreen}",
|
||||||
|
GameOptions.LaunchVersion,
|
||||||
|
GameOptions.MinimumRamMb,
|
||||||
|
GameOptions.MaximumRamMb,
|
||||||
|
GameOptions.ScreenWidth,
|
||||||
|
GameOptions.ScreenHeight,
|
||||||
|
GameOptions.Fullscreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadAllAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Loading all settings from disk.");
|
||||||
|
|
||||||
|
await LoadLauncherOptionsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
await LoadGameOptionsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("All settings were loaded successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAllAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Saving all settings to disk.");
|
||||||
|
|
||||||
|
await SaveLauncherOptionsAsync(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>(
|
||||||
|
string fileName,
|
||||||
|
T value,
|
||||||
|
CancellationToken cancellationToken) where T : BaseConfig
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(fileName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("File name cannot be null, empty, or whitespace.", nameof(fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
string fullPath = GetFullPath(fileName);
|
||||||
|
string? directoryPath = Path.GetDirectoryName(fullPath);
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(directoryPath);
|
||||||
|
|
||||||
|
string temporaryPath = fullPath + ".tmp";
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Deleting existing settings file at {FullPath}.", fullPath);
|
||||||
|
File.Delete(fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Move(temporaryPath, fullPath);
|
||||||
|
|
||||||
|
_logger.LogInformation("Settings file {FileName} was saved successfully to {FullPath}.", fileName, fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<T?> LoadAsync<T>(
|
||||||
|
string fileName,
|
||||||
|
CancellationToken cancellationToken) where T : BaseConfig
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(fileName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("File name cannot be null, empty, or whitespace.", nameof(fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
string fullPath = GetFullPath(fileName);
|
||||||
|
|
||||||
|
if (!File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Settings file {FileName} was not found at {FullPath}.", fileName, fullPath);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Loading settings file {FileName} from {FullPath}.", fileName, fullPath);
|
||||||
|
|
||||||
|
string json = await File.ReadAllTextAsync(fullPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Settings file {FileName} at {FullPath} was empty.", fileName, fullPath);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to deserialize settings file {FileName} at {FullPath} to {TypeName}.", fileName, fullPath, typeof(T).Name);
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"Failed to deserialize settings file '{fullPath}' to {typeof(T).Name}.",
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetFullPath(string fileName)
|
||||||
|
{
|
||||||
|
string fullPath = Path.Combine(_fileStore.GetOrCreate(FolderLocation.Config), fileName);
|
||||||
|
_logger.LogDebug("Resolved settings file path for {FileName} to {FullPath}.", fileName, fullPath);
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
AlayaCore/States/LaunchPlan.cs
Normal file
79
AlayaCore/States/LaunchPlan.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
namespace AlayaCore.States
|
||||||
|
{
|
||||||
|
public enum LaunchState
|
||||||
|
{
|
||||||
|
Checking,
|
||||||
|
Ready,
|
||||||
|
LauncherNeedsUpdate,
|
||||||
|
NeedAuthenticating,
|
||||||
|
InstallMinecraft,
|
||||||
|
InstallNeoforge,
|
||||||
|
SyncMods
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LaunchPlan
|
||||||
|
{
|
||||||
|
public bool LauncherNeedsUpdate { get; }
|
||||||
|
public bool MinecraftNeedsInstallOrUpdate { get; }
|
||||||
|
public bool NeoforgeNeedsInstallOrUpdate { get; }
|
||||||
|
public bool ModsNeedSync { get; }
|
||||||
|
public bool NeedAuthenticating { get; }
|
||||||
|
|
||||||
|
public LaunchState State => ComputeState();
|
||||||
|
|
||||||
|
public bool CanRun => State == LaunchState.Ready;
|
||||||
|
|
||||||
|
public bool NeedsUpdating =>
|
||||||
|
LauncherNeedsUpdate ||
|
||||||
|
MinecraftNeedsInstallOrUpdate ||
|
||||||
|
NeoforgeNeedsInstallOrUpdate ||
|
||||||
|
ModsNeedSync;
|
||||||
|
|
||||||
|
public bool NeedsAttention =>
|
||||||
|
NeedsUpdating || NeedAuthenticating;
|
||||||
|
|
||||||
|
public LaunchPlan(
|
||||||
|
bool launcherNeedsUpdate,
|
||||||
|
bool minecraftNeedsInstallOrUpdate,
|
||||||
|
bool neoforgeNeedsInstallOrUpdate,
|
||||||
|
bool modsNeedSync,
|
||||||
|
bool needAuthenticating)
|
||||||
|
{
|
||||||
|
LauncherNeedsUpdate = launcherNeedsUpdate;
|
||||||
|
MinecraftNeedsInstallOrUpdate = minecraftNeedsInstallOrUpdate;
|
||||||
|
NeoforgeNeedsInstallOrUpdate = neoforgeNeedsInstallOrUpdate;
|
||||||
|
ModsNeedSync = modsNeedSync;
|
||||||
|
NeedAuthenticating = needAuthenticating;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LaunchState ComputeState()
|
||||||
|
{
|
||||||
|
if (LauncherNeedsUpdate)
|
||||||
|
return LaunchState.LauncherNeedsUpdate;
|
||||||
|
|
||||||
|
if (NeedAuthenticating)
|
||||||
|
return LaunchState.NeedAuthenticating;
|
||||||
|
|
||||||
|
if (MinecraftNeedsInstallOrUpdate)
|
||||||
|
return LaunchState.InstallMinecraft;
|
||||||
|
|
||||||
|
if (NeoforgeNeedsInstallOrUpdate)
|
||||||
|
return LaunchState.InstallNeoforge;
|
||||||
|
|
||||||
|
if (ModsNeedSync)
|
||||||
|
return LaunchState.SyncMods;
|
||||||
|
|
||||||
|
return LaunchState.Ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LaunchPlan EmptyReady()
|
||||||
|
{
|
||||||
|
return new LaunchPlan(
|
||||||
|
launcherNeedsUpdate: false,
|
||||||
|
minecraftNeedsInstallOrUpdate: false,
|
||||||
|
neoforgeNeedsInstallOrUpdate: false,
|
||||||
|
modsNeedSync: false,
|
||||||
|
needAuthenticating: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace AlayaCore.Utilities.Converters
|
namespace AlayaCore.Utilities.Converters
|
||||||
@@ -12,9 +11,9 @@ namespace AlayaCore.Utilities.Converters
|
|||||||
{
|
{
|
||||||
writer.WriteNull();
|
writer.WriteNull();
|
||||||
}
|
}
|
||||||
else if (value is Uri)
|
else if (value is Uri uri)
|
||||||
{
|
{
|
||||||
writer.WriteValue(((Uri)value).AbsoluteUri);
|
writer.WriteValue(uri.AbsoluteUri);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -33,8 +32,7 @@ namespace AlayaCore.Utilities.Converters
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Uri uri = new Uri((string)reader.Value!);
|
return new Uri((string)reader.Value!, UriKind.Absolute);
|
||||||
return uri;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -47,7 +45,7 @@ namespace AlayaCore.Utilities.Converters
|
|||||||
|
|
||||||
public override bool CanConvert(Type objectType)
|
public override bool CanConvert(Type objectType)
|
||||||
{
|
{
|
||||||
return objectType == typeof(Uri);
|
return typeof(Uri).IsAssignableFrom(objectType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
17
AlayaCore/Utilities/Enums/FolderLocation.cs
Normal file
17
AlayaCore/Utilities/Enums/FolderLocation.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace AlayaCore.Utilities.Enums
|
||||||
|
{
|
||||||
|
public enum FolderLocation
|
||||||
|
{
|
||||||
|
BaseDirectory,
|
||||||
|
Java,
|
||||||
|
JavaRuntime,
|
||||||
|
Game,
|
||||||
|
Mods,
|
||||||
|
ResourcePacks,
|
||||||
|
Config,
|
||||||
|
Downloads,
|
||||||
|
Manifests,
|
||||||
|
Plugins,
|
||||||
|
Data
|
||||||
|
}
|
||||||
|
}
|
||||||
16
AlayaCore/Utilities/Enums/LauncherErrorType.cs
Normal file
16
AlayaCore/Utilities/Enums/LauncherErrorType.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace AlayaCore.Utilities.Enums
|
||||||
|
{
|
||||||
|
public enum LauncherErrorType
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
Network,
|
||||||
|
Manifest,
|
||||||
|
Authentication,
|
||||||
|
Download,
|
||||||
|
Installation,
|
||||||
|
Update,
|
||||||
|
Launch,
|
||||||
|
Configuration,
|
||||||
|
Cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,17 +15,17 @@ namespace AlayaCore.Utilities.Extensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new InstalledModsManifestModel(
|
return new InstalledModsManifestModel(
|
||||||
dto.InstalledMods?.Select(mod => mod.ToModel()) ?? Array.Empty<InstalledModEntry>());
|
dto.InstalledMods?.Select(mod => mod.ToModel()) ?? Array.Empty<ModFileEntry>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InstalledModEntry ToModel(this InstalledModEntryDto dto)
|
public static InstalledModsManifestDto ToDto(this InstalledModsManifestModel model)
|
||||||
{
|
{
|
||||||
if (dto == null)
|
if (model == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(dto));
|
throw new ArgumentNullException(nameof(model));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new InstalledModEntry(dto.FileName, dto.Version, dto.Sha512Hash, dto.Size);
|
return new InstalledModsManifestDto(model.Mods.Select(mod => mod.ToDto()).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LauncherManifestModel ToModel(this LauncherManifestDto dto)
|
public static LauncherManifestModel ToModel(this LauncherManifestDto dto)
|
||||||
@@ -47,12 +47,10 @@ namespace AlayaCore.Utilities.Extensions
|
|||||||
|
|
||||||
return new ManifestModel(
|
return new ManifestModel(
|
||||||
dto.AlayaVersion,
|
dto.AlayaVersion,
|
||||||
dto.RequiredJavaVersion,
|
|
||||||
dto.RequiredJavaUrl,
|
|
||||||
dto.MinecraftVersion,
|
dto.MinecraftVersion,
|
||||||
dto.MinecraftUrl,
|
|
||||||
dto.NeoforgedVersion,
|
dto.NeoforgedVersion,
|
||||||
dto.NeoforgedUrl,
|
dto.ServerUrl,
|
||||||
|
dto.ServerPort,
|
||||||
dto.Files?.Select(file => file.ToModel()) ?? Array.Empty<ModFileEntry>());
|
dto.Files?.Select(file => file.ToModel()) ?? Array.Empty<ModFileEntry>());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,8 +65,7 @@ namespace AlayaCore.Utilities.Extensions
|
|||||||
dto.Name,
|
dto.Name,
|
||||||
dto.Type,
|
dto.Type,
|
||||||
dto.Sha512Hash,
|
dto.Sha512Hash,
|
||||||
dto.ModrinthId,
|
dto.Size);
|
||||||
dto.ModrinthVersionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ManifestDto ToDto(this ManifestModel model)
|
public static ManifestDto ToDto(this ManifestModel model)
|
||||||
@@ -81,12 +78,8 @@ namespace AlayaCore.Utilities.Extensions
|
|||||||
return new ManifestDto
|
return new ManifestDto
|
||||||
{
|
{
|
||||||
AlayaVersion = model.AlayaVersion,
|
AlayaVersion = model.AlayaVersion,
|
||||||
RequiredJavaVersion = model.RequiredJavaVersion,
|
|
||||||
RequiredJavaUrl = model.RequiredJavaUrl,
|
|
||||||
MinecraftVersion = model.MinecraftVersion,
|
MinecraftVersion = model.MinecraftVersion,
|
||||||
MinecraftUrl = model.MinecraftUrl,
|
|
||||||
NeoforgedVersion = model.NeoforgedVersion,
|
NeoforgedVersion = model.NeoforgedVersion,
|
||||||
NeoforgedUrl = model.NeoforgedUrl,
|
|
||||||
Files = model.Files.Select(file => file.ToDto()).ToList()
|
Files = model.Files.Select(file => file.ToDto()).ToList()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -103,8 +96,7 @@ namespace AlayaCore.Utilities.Extensions
|
|||||||
Name = model.FileName,
|
Name = model.FileName,
|
||||||
Type = model.Type,
|
Type = model.Type,
|
||||||
Sha512Hash = model.Sha512Hash,
|
Sha512Hash = model.Sha512Hash,
|
||||||
ModrinthId = model.ModrinthId,
|
Size = model.Size
|
||||||
ModrinthVersionId = model.ModrinthVersionId
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
AlayaCore/Utilities/Helpers/ErrorHelper.cs
Normal file
56
AlayaCore/Utilities/Helpers/ErrorHelper.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using AlayaCore.Utilities.Enums;
|
||||||
|
|
||||||
|
namespace AlayaCore.Errors
|
||||||
|
{
|
||||||
|
public static class ErrorHelper
|
||||||
|
{
|
||||||
|
public static LauncherError Map(Exception exception)
|
||||||
|
{
|
||||||
|
if (exception == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
return exception switch
|
||||||
|
{
|
||||||
|
OperationCanceledException => new LauncherError(
|
||||||
|
LauncherErrorType.Cancelled,
|
||||||
|
"The operation was cancelled.",
|
||||||
|
exception),
|
||||||
|
|
||||||
|
HttpRequestException => new LauncherError(
|
||||||
|
LauncherErrorType.Network,
|
||||||
|
"A network error occurred.",
|
||||||
|
exception),
|
||||||
|
|
||||||
|
IOException => new LauncherError(
|
||||||
|
LauncherErrorType.Network,
|
||||||
|
"A file or network I/O error occurred.",
|
||||||
|
exception),
|
||||||
|
|
||||||
|
InvalidDataException => new LauncherError(
|
||||||
|
LauncherErrorType.Manifest,
|
||||||
|
"Invalid or corrupt data was encountered.",
|
||||||
|
exception),
|
||||||
|
|
||||||
|
ArgumentException => new LauncherError(
|
||||||
|
LauncherErrorType.Configuration,
|
||||||
|
"Invalid configuration or input.",
|
||||||
|
exception),
|
||||||
|
|
||||||
|
InvalidOperationException => new LauncherError(
|
||||||
|
LauncherErrorType.Launch,
|
||||||
|
"The operation could not be completed due to an invalid state.",
|
||||||
|
exception),
|
||||||
|
|
||||||
|
_ => new LauncherError(
|
||||||
|
LauncherErrorType.Unknown,
|
||||||
|
"An unexpected error occurred.",
|
||||||
|
exception)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
AlayaCore/Utilities/Helpers/OptionsHelper.cs
Normal file
21
AlayaCore/Utilities/Helpers/OptionsHelper.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using AlayaCore.Models.Configuration;
|
||||||
|
|
||||||
|
namespace AlayaCore.Utilities.Helpers
|
||||||
|
{
|
||||||
|
public static class OptionsHelper
|
||||||
|
{
|
||||||
|
public static (LauncherUpdateServiceOptions LaunchUpdater, LauncherOptions Launcher,
|
||||||
|
GameOptions Game, ManifestServiceOptions Manifest, ModrinthConnectionOptions Modrinth,
|
||||||
|
RetryPolicyOptions RetryPolicy) GetDefaultOptions()
|
||||||
|
{
|
||||||
|
LauncherUpdateServiceOptions lso = LauncherUpdateServiceOptions.Default;
|
||||||
|
LauncherOptions lo = LauncherOptions.Default;
|
||||||
|
GameOptions go = GameOptions.Default;
|
||||||
|
ManifestServiceOptions mso = ManifestServiceOptions.Default;
|
||||||
|
ModrinthConnectionOptions mco = ModrinthConnectionOptions.Default;
|
||||||
|
RetryPolicyOptions rpo = RetryPolicyOptions.Default;
|
||||||
|
|
||||||
|
return (lso, lo, go, mso, mco, rpo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
AlayaCore/Utilities/Stores/LocalFileStore.cs
Normal file
111
AlayaCore/Utilities/Stores/LocalFileStore.cs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using AlayaCore.Abstractions.Interfaces;
|
||||||
|
using AlayaCore.Utilities.Enums;
|
||||||
|
|
||||||
|
namespace AlayaCore.Utilities.Stores
|
||||||
|
{
|
||||||
|
public sealed class LocalFileStore : IFileStore
|
||||||
|
{
|
||||||
|
private static readonly string BaseDirectoryPath = AppContext.BaseDirectory;
|
||||||
|
private static readonly string JavaDirectoryPath = Path.Combine(BaseDirectoryPath, "Java");
|
||||||
|
private static readonly string JavaRuntimeDirectoryPath = Path.Combine(JavaDirectoryPath, "runtime");
|
||||||
|
private static readonly string GameDirectoryPath = Path.Combine(BaseDirectoryPath, "Game");
|
||||||
|
private static readonly string ModsDirectoryPath = Path.Combine(GameDirectoryPath, "mods");
|
||||||
|
private static readonly string ResourcePacksDirectoryPath = Path.Combine(GameDirectoryPath, "resourcepacks");
|
||||||
|
private static readonly string ConfigDirectoryPath = Path.Combine(BaseDirectoryPath, "Config");
|
||||||
|
private static readonly string DownloadsDirectoryPath = Path.Combine(BaseDirectoryPath, "Temp");
|
||||||
|
private static readonly string ManifestsDirectoryPath = Path.Combine(BaseDirectoryPath, "Manifests");
|
||||||
|
private static readonly string PluginsDirectoryPath = Path.Combine(GameDirectoryPath, "plugins");
|
||||||
|
private static readonly string DataDirectoryPath = Path.Combine(BaseDirectoryPath, "Data");
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<FolderLocation, string> Folders =
|
||||||
|
new Dictionary<FolderLocation, string>
|
||||||
|
{
|
||||||
|
{ FolderLocation.BaseDirectory, BaseDirectoryPath },
|
||||||
|
{ FolderLocation.Java, JavaDirectoryPath },
|
||||||
|
{ FolderLocation.JavaRuntime, JavaRuntimeDirectoryPath },
|
||||||
|
{ FolderLocation.Game, GameDirectoryPath },
|
||||||
|
{ FolderLocation.Mods, ModsDirectoryPath },
|
||||||
|
{ FolderLocation.ResourcePacks, ResourcePacksDirectoryPath },
|
||||||
|
{ FolderLocation.Config, ConfigDirectoryPath },
|
||||||
|
{ FolderLocation.Downloads, DownloadsDirectoryPath },
|
||||||
|
{ FolderLocation.Manifests, ManifestsDirectoryPath},
|
||||||
|
{ FolderLocation.Plugins, PluginsDirectoryPath },
|
||||||
|
{ FolderLocation.Data, DataDirectoryPath}
|
||||||
|
};
|
||||||
|
|
||||||
|
public LocalFileStore()
|
||||||
|
{
|
||||||
|
CreateDirectories();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CreateDirectories()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(BaseDirectoryPath);
|
||||||
|
Directory.CreateDirectory(JavaDirectoryPath);
|
||||||
|
Directory.CreateDirectory(JavaRuntimeDirectoryPath);
|
||||||
|
Directory.CreateDirectory(GameDirectoryPath);
|
||||||
|
Directory.CreateDirectory(ModsDirectoryPath);
|
||||||
|
Directory.CreateDirectory(ResourcePacksDirectoryPath);
|
||||||
|
Directory.CreateDirectory(ConfigDirectoryPath);
|
||||||
|
Directory.CreateDirectory(DownloadsDirectoryPath);
|
||||||
|
Directory.CreateDirectory(ManifestsDirectoryPath);
|
||||||
|
Directory.CreateDirectory(PluginsDirectoryPath);
|
||||||
|
Directory.CreateDirectory(DataDirectoryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Get(FolderLocation location)
|
||||||
|
{
|
||||||
|
if (!Folders.TryGetValue(location, out string? path))
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(location), location, "Unknown folder location.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetOrCreate(FolderLocation location)
|
||||||
|
{
|
||||||
|
string path = Get(location);
|
||||||
|
Directory.CreateDirectory(path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Combine(FolderLocation location, params string[] paths)
|
||||||
|
{
|
||||||
|
if (paths == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(paths));
|
||||||
|
}
|
||||||
|
|
||||||
|
string rootPath = Get(location);
|
||||||
|
|
||||||
|
if (paths.Length == 0)
|
||||||
|
{
|
||||||
|
return rootPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] combinedPaths = new string[paths.Length + 1];
|
||||||
|
combinedPaths[0] = rootPath;
|
||||||
|
|
||||||
|
for (int i = 0; i < paths.Length; i++)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(paths[i]))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Path segments cannot be null, empty, or whitespace.", nameof(paths));
|
||||||
|
}
|
||||||
|
|
||||||
|
combinedPaths[i + 1] = paths[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(combinedPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Exists(FolderLocation location)
|
||||||
|
{
|
||||||
|
return Directory.Exists(Get(location));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,9 +44,25 @@
|
|||||||
"netstandard2.1": {
|
"netstandard2.1": {
|
||||||
"targetAlias": "netstandard2.1",
|
"targetAlias": "netstandard2.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"CmlLib.Core": {
|
||||||
|
"target": "Package",
|
||||||
|
"version": "[4.0.6, )"
|
||||||
|
},
|
||||||
|
"CmlLib.Core.Auth.Microsoft": {
|
||||||
|
"target": "Package",
|
||||||
|
"version": "[3.3.1, )"
|
||||||
|
},
|
||||||
|
"CmlLib.Core.Installer.NeoForge": {
|
||||||
|
"target": "Package",
|
||||||
|
"version": "[4.0.0, )"
|
||||||
|
},
|
||||||
"Newtonsoft.Json": {
|
"Newtonsoft.Json": {
|
||||||
"target": "Package",
|
"target": "Package",
|
||||||
"version": "[13.0.4, )"
|
"version": "[13.0.4, )"
|
||||||
|
},
|
||||||
|
"XboxAuthNet.Game.Msal": {
|
||||||
|
"target": "Package",
|
||||||
|
"version": "[0.1.3, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"imports": [
|
"imports": [
|
||||||
|
|||||||
@@ -1,2 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
|
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||||
|
<Import Project="$(NuGetPackageRoot)system.text.json/8.0.5/buildTransitive/netstandard2.0/System.Text.Json.targets" Condition="Exists('$(NuGetPackageRoot)system.text.json/8.0.5/buildTransitive/netstandard2.0/System.Text.Json.targets')" />
|
||||||
|
<Import Project="$(NuGetPackageRoot)microsoft.extensions.logging.abstractions/8.0.1/buildTransitive/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.logging.abstractions/8.0.1/buildTransitive/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.targets')" />
|
||||||
|
</ImportGroup>
|
||||||
|
</Project>
|
||||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("AlayaCore")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("AlayaCore")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+823ccf4b877ddf1fc1293f72c6b704d6a449ddaa")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("AlayaCore")]
|
[assembly: System.Reflection.AssemblyProductAttribute("AlayaCore")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("AlayaCore")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("AlayaCore")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
02d72db7e7301cd87568f68f39a003b2484df3c699c3ceed1d9bd04f25f6e3dd
|
c63b3bf6f0fd998ece1d911c58e61eb584840e2e19d535b62f67f54b1510e49e
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,40 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dgSpecHash": "iqHG7z//q/I=",
|
"dgSpecHash": "pXFuC0zr6Zg=",
|
||||||
"success": true,
|
"success": true,
|
||||||
"projectFilePath": "/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/AlayaCore.csproj",
|
"projectFilePath": "/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/AlayaCore.csproj",
|
||||||
"expectedPackageFiles": [
|
"expectedPackageFiles": [
|
||||||
|
"/Users/ryanmacham/.nuget/packages/cmllib.core/4.0.6/cmllib.core.4.0.6.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/cmllib.core.auth.microsoft/3.3.1/cmllib.core.auth.microsoft.3.3.1.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/cmllib.core.commons/4.0.0/cmllib.core.commons.4.0.0.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/cmllib.core.installer.neoforge/4.0.0/cmllib.core.installer.neoforge.4.0.0.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/htmlagilitypack/1.11.48/htmlagilitypack.1.11.48.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/lzma-sdk/19.0.0/lzma-sdk.19.0.0.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/microsoft.bcl.asyncinterfaces/8.0.0/microsoft.bcl.asyncinterfaces.8.0.0.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/microsoft.extensions.dependencyinjection.abstractions/8.0.1/microsoft.extensions.dependencyinjection.abstractions.8.0.1.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/microsoft.extensions.logging.abstractions/8.0.1/microsoft.extensions.logging.abstractions.8.0.1.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/microsoft.identity.client/4.61.3/microsoft.identity.client.4.61.3.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/microsoft.identity.client.extensions.msal/4.61.3/microsoft.identity.client.extensions.msal.4.61.3.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/microsoft.identitymodel.abstractions/6.35.0/microsoft.identitymodel.abstractions.6.35.0.nupkg.sha512",
|
||||||
"/Users/ryanmacham/.nuget/packages/newtonsoft.json/13.0.4/newtonsoft.json.13.0.4.nupkg.sha512",
|
"/Users/ryanmacham/.nuget/packages/newtonsoft.json/13.0.4/newtonsoft.json.13.0.4.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/sharpziplib/1.4.2/sharpziplib.1.4.2.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/system.buffers/4.5.1/system.buffers.4.5.1.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/system.diagnostics.diagnosticsource/6.0.1/system.diagnostics.diagnosticsource.6.0.1.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/system.io.filesystem.accesscontrol/5.0.0/system.io.filesystem.accesscontrol.5.0.0.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/system.memory/4.5.5/system.memory.4.5.5.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/system.net.http.json/8.0.0/system.net.http.json.8.0.0.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/system.numerics.vectors/4.4.0/system.numerics.vectors.4.4.0.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/system.runtime.compilerservices.unsafe/6.0.0/system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/system.security.accesscontrol/5.0.0/system.security.accesscontrol.5.0.0.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/system.security.cryptography.protecteddata/4.5.0/system.security.cryptography.protecteddata.4.5.0.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/system.security.principal.windows/5.0.0/system.security.principal.windows.5.0.0.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/system.text.encodings.web/8.0.0/system.text.encodings.web.8.0.0.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/system.text.json/8.0.5/system.text.json.8.0.5.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/system.threading.tasks.dataflow/8.0.1/system.threading.tasks.dataflow.8.0.1.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/system.threading.tasks.extensions/4.5.4/system.threading.tasks.extensions.4.5.4.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/xboxauthnet/3.0.4/xboxauthnet.3.0.4.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/xboxauthnet.game/1.4.1/xboxauthnet.game.1.4.1.nupkg.sha512",
|
||||||
|
"/Users/ryanmacham/.nuget/packages/xboxauthnet.game.msal/0.1.3/xboxauthnet.game.msal.0.1.3.nupkg.sha512",
|
||||||
"/Users/ryanmacham/.nuget/packages/netstandard.library.ref/2.1.0/netstandard.library.ref.2.1.0.nupkg.sha512"
|
"/Users/ryanmacham/.nuget/packages/netstandard.library.ref/2.1.0/netstandard.library.ref.2.1.0.nupkg.sha512"
|
||||||
],
|
],
|
||||||
"logs": []
|
"logs": []
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
"restore":{"projectUniqueName":"/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/AlayaCore.csproj","projectName":"AlayaCore","projectPath":"/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/AlayaCore.csproj","packagesPath":"","outputPath":"/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/obj/","projectStyle":"PackageReference","originalTargetFrameworks":["netstandard2.1"],"sources":{"https://api.nuget.org/v3/index.json":{}},"frameworks":{"netstandard2.1":{"targetAlias":"netstandard2.1","projectReferences":{}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"},"SdkAnalysisLevel":"10.0.200"}"frameworks":{"netstandard2.1":{"targetAlias":"netstandard2.1","dependencies":{"Newtonsoft.Json":{"target":"Package","version":"[13.0.4, )"}},"imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"downloadDependencies":[{"name":"NETStandard.Library.Ref","version":"[2.1.0, 2.1.0]"}],"frameworkReferences":{"NETStandard.Library":{"privateAssets":"all"}},"runtimeIdentifierGraphPath":"/usr/local/share/dotnet/sdk/10.0.201/RuntimeIdentifierGraph.json"}}
|
"restore":{"projectUniqueName":"/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/AlayaCore.csproj","projectName":"AlayaCore","projectPath":"/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/AlayaCore.csproj","packagesPath":"","outputPath":"/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/obj/","projectStyle":"PackageReference","originalTargetFrameworks":["netstandard2.1"],"sources":{"https://api.nuget.org/v3/index.json":{}},"frameworks":{"netstandard2.1":{"targetAlias":"netstandard2.1","projectReferences":{}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"},"SdkAnalysisLevel":"10.0.200"}"frameworks":{"netstandard2.1":{"targetAlias":"netstandard2.1","dependencies":{"CmlLib.Core":{"target":"Package","version":"[4.0.6, )"},"CmlLib.Core.Auth.Microsoft":{"target":"Package","version":"[3.3.1, )"},"CmlLib.Core.Installer.NeoForge":{"target":"Package","version":"[4.0.0, )"},"Newtonsoft.Json":{"target":"Package","version":"[13.0.4, )"},"XboxAuthNet.Game.Msal":{"target":"Package","version":"[0.1.3, )"}},"imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"downloadDependencies":[{"name":"NETStandard.Library.Ref","version":"[2.1.0, 2.1.0]"}],"frameworkReferences":{"NETStandard.Library":{"privateAssets":"all"}},"runtimeIdentifierGraphPath":"/usr/local/share/dotnet/sdk/10.0.201/RuntimeIdentifierGraph.json"}}
|
||||||
@@ -1 +1 @@
|
|||||||
17752277078247393
|
17754069520908862
|
||||||
@@ -1 +1 @@
|
|||||||
17752277078247393
|
17754069520908862
|
||||||
Reference in New Issue
Block a user