Switched to Modrinth Version_File with Hash rather than ID and Version.
Implemented SettingsService
This commit is contained in:
@@ -1,10 +1,21 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Models;
|
||||||
|
using AlayaCore.States;
|
||||||
|
|
||||||
namespace AlayaCore.Abstractions.Interfaces
|
namespace AlayaCore.Abstractions.Interfaces
|
||||||
{
|
{
|
||||||
public interface ILaunchDirector
|
public interface ILaunchDirector
|
||||||
{
|
{
|
||||||
Task RunAsync(CancellationToken cancellationToken = default);
|
Task<LaunchPlan> EvaluateAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task InstallOrUpdateAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task LaunchAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
bool CanRun { get; }
|
||||||
|
bool NeedsUpdating { get; }
|
||||||
|
|
||||||
|
LaunchPlan? CurrentPlan { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Installation;
|
||||||
|
using AlayaCore.Models.Manifests;
|
||||||
|
|
||||||
namespace AlayaCore.Abstractions.Interfaces.Services
|
namespace AlayaCore.Abstractions.Interfaces.Services
|
||||||
{
|
{
|
||||||
public interface IGameLaunchServer
|
public interface IGameLaunchService
|
||||||
{
|
{
|
||||||
|
Task LaunchAsync(
|
||||||
|
ManifestModel manifest,
|
||||||
|
InstallEnvironment environment,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,17 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Models.Configuration;
|
||||||
|
|
||||||
namespace AlayaCore.Abstractions.Interfaces.Services
|
namespace AlayaCore.Abstractions.Interfaces.Services
|
||||||
{
|
{
|
||||||
public interface ISettingsService
|
public interface ISettingsService
|
||||||
{
|
{
|
||||||
|
LauncherOptions LauncherOptions { get; }
|
||||||
|
|
||||||
|
Task SetForceReinstallAsync(bool value, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task SaveLauncherOptionsAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task LoadLauncherOptionsAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,14 @@
|
|||||||
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.States;
|
||||||
|
|
||||||
namespace AlayaCore
|
namespace AlayaCore
|
||||||
{
|
{
|
||||||
@@ -16,22 +19,34 @@ namespace AlayaCore
|
|||||||
private readonly IInstallStateService _installStateService;
|
private readonly IInstallStateService _installStateService;
|
||||||
private readonly IJavaService _javaService;
|
private readonly IJavaService _javaService;
|
||||||
private readonly IModService _modService;
|
private readonly IModService _modService;
|
||||||
|
private readonly IGameLaunchService _gameLaunchService;
|
||||||
|
private readonly LauncherOptions _options;
|
||||||
|
|
||||||
|
public bool CanRun { get; private set; }
|
||||||
|
|
||||||
|
public bool NeedsUpdating { get; private set; }
|
||||||
|
|
||||||
|
public LaunchPlan? CurrentPlan { get; private set; }
|
||||||
|
|
||||||
public LaunchDirector(
|
public LaunchDirector(
|
||||||
IManifestService manifestService,
|
IManifestService manifestService,
|
||||||
IUpdateService updateService,
|
IUpdateService updateService,
|
||||||
IInstallStateService installStateService,
|
IInstallStateService installStateService,
|
||||||
IJavaService javaService,
|
IJavaService javaService,
|
||||||
IModService modService)
|
IModService modService,
|
||||||
|
IGameLaunchService gameLaunchService,
|
||||||
|
LauncherOptions options)
|
||||||
{
|
{
|
||||||
_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));
|
_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));
|
||||||
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RunAsync(CancellationToken cancellationToken = default)
|
public async Task<LaunchPlan> EvaluateAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
@@ -41,17 +56,164 @@ namespace AlayaCore
|
|||||||
|
|
||||||
if (launcherNeedsUpdate)
|
if (launcherNeedsUpdate)
|
||||||
{
|
{
|
||||||
LauncherManifestModel launcherManifest = await _manifestService
|
LaunchPlan launcherUpdatePlan = new LaunchPlan(
|
||||||
.GetLauncherManifestAsync(cancellationToken)
|
launcherNeedsUpdate: true,
|
||||||
.ConfigureAwait(false);
|
javaNeedsInstallOrUpdate: false,
|
||||||
|
minecraftNeedsInstallOrUpdate: false,
|
||||||
|
neoforgeNeedsInstallOrUpdate: false,
|
||||||
|
modsNeedSync: false);
|
||||||
|
|
||||||
await _updateService
|
ApplyPlan(launcherUpdatePlan);
|
||||||
.LaunchUpdaterAsync(launcherManifest, cancellationToken)
|
return launcherUpdatePlan;
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ManifestModel manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
InstallEnvironment environment = await _installStateService
|
||||||
|
.GetCurrentEnvironmentAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
bool javaNeedsInstallOrUpdate =
|
||||||
|
_options.ForceReinstall ||
|
||||||
|
!environment.JavaInstalled ||
|
||||||
|
!string.Equals(environment.JavaVersion, manifest.RequiredJavaVersion, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
bool minecraftNeedsInstallOrUpdate =
|
||||||
|
_options.ForceReinstall ||
|
||||||
|
!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,
|
||||||
|
javaNeedsInstallOrUpdate: javaNeedsInstallOrUpdate,
|
||||||
|
minecraftNeedsInstallOrUpdate: minecraftNeedsInstallOrUpdate,
|
||||||
|
neoforgeNeedsInstallOrUpdate: neoforgeNeedsInstallOrUpdate,
|
||||||
|
modsNeedSync: modsNeedSync);
|
||||||
|
|
||||||
|
ApplyPlan(plan);
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InstallOrUpdateAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
LaunchPlan plan = await EvaluateAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
while (!plan.CanRun)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
ManifestModel? manifest = null;
|
||||||
|
InstallEnvironment? environment = null;
|
||||||
|
|
||||||
|
switch (plan.State)
|
||||||
|
{
|
||||||
|
case LaunchState.LauncherNeedsUpdate:
|
||||||
|
{
|
||||||
|
LauncherManifestModel launcherManifest = await _manifestService
|
||||||
|
.GetLauncherManifestAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _updateService
|
||||||
|
.LaunchUpdaterAsync(launcherManifest, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
ApplyPlan(plan);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LaunchState.InstallJava:
|
||||||
|
{
|
||||||
|
manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
environment = await _installStateService
|
||||||
|
.GetCurrentEnvironmentAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _javaService
|
||||||
|
.EnsureValidJavaInstalledAsync(manifest, environment, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LaunchState.InstallMinecraft:
|
||||||
|
{
|
||||||
|
throw new NotImplementedException("Minecraft install/update flow has not been implemented yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
case LaunchState.InstallNeoforge:
|
||||||
|
{
|
||||||
|
throw new NotImplementedException("NeoForge install/update flow has not been implemented yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
case LaunchState.SyncMods:
|
||||||
|
{
|
||||||
|
manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
environment = await _installStateService
|
||||||
|
.GetCurrentEnvironmentAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _modService
|
||||||
|
.ProcessModsAsync(manifest, environment, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LaunchState.Ready:
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Unsupported launch state '{plan.State}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plan = await EvaluateAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LaunchAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (CurrentPlan == null)
|
||||||
|
{
|
||||||
|
await EvaluateAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CanRun)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
await _gameLaunchService
|
||||||
|
.LaunchAsync(manifest, environment, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ManifestModel> EnsureCurrentManifestAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
ManifestModel? localManifest = await _manifestService
|
ManifestModel? localManifest = await _manifestService
|
||||||
.GetLocalCoreManifestAsync(cancellationToken)
|
.GetLocalCoreManifestAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
@@ -72,48 +234,61 @@ namespace AlayaCore
|
|||||||
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);
|
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
|
|
||||||
|
|
||||||
if (!environment.MinecraftInstalled || environment.MinecraftVersion != manifest.MinecraftVersion)
|
var requiredMods = manifest.Files
|
||||||
{
|
.Where(file => file.Type == AlayaCore.Utilities.Enums.FileType.Mod)
|
||||||
|
.ToList();
|
||||||
}
|
|
||||||
|
|
||||||
// Process Neoforge
|
|
||||||
|
|
||||||
if (!environment.NeoforgedInstalled || environment.NeoforgedVersion != manifest.NeoforgedVersion)
|
var installedMods = environment.InstalledModsManifest.Mods;
|
||||||
|
|
||||||
|
if (requiredMods.Count != installedMods.Count)
|
||||||
{
|
{
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process Mods
|
foreach (ModFileEntry requiredMod in requiredMods)
|
||||||
|
{
|
||||||
await _modService.ProcessModsAsync(manifest, environment, cancellationToken).ConfigureAwait(false);
|
InstalledModEntry? 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyPlan(LaunchPlan plan)
|
||||||
|
{
|
||||||
|
CurrentPlan = plan ?? throw new ArgumentNullException(nameof(plan));
|
||||||
|
NeedsUpdating = plan.NeedsUpdating;
|
||||||
|
CanRun = plan.CanRun;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
namespace AlayaCore.Models.Configuration
|
namespace AlayaCore.Models.Configuration
|
||||||
{
|
{
|
||||||
public class LauncherOptions
|
public sealed class LauncherOptions
|
||||||
{
|
{
|
||||||
|
public bool ForceReinstall { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
namespace AlayaCore.Models.Configuration
|
namespace AlayaCore.Models.Configuration
|
||||||
{
|
{
|
||||||
public class UpdateServiceOptions
|
public sealed class LauncherUpdateServiceOptions
|
||||||
{
|
{
|
||||||
|
public LauncherUpdateServiceOptions(string alayaUpdaterPath, bool forceUpdate)
|
||||||
|
{
|
||||||
|
AlayaUpdaterPath = alayaUpdaterPath;
|
||||||
|
ForceUpdate = forceUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
public string AlayaUpdaterPath { get; set; }
|
public string AlayaUpdaterPath { get; set; }
|
||||||
public bool ForceUpdate { get; set; }
|
public bool ForceUpdate { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,36 +6,27 @@ namespace AlayaCore.Models.Manifests
|
|||||||
public sealed class InstalledModEntry
|
public sealed class InstalledModEntry
|
||||||
{
|
{
|
||||||
public string FileName { get; }
|
public string FileName { get; }
|
||||||
public string Version { get; }
|
|
||||||
|
|
||||||
public string Sha512Hash { get; }
|
public string Sha512Hash { get; }
|
||||||
|
public long Size { get; }
|
||||||
public long Size { get; set; }
|
|
||||||
|
|
||||||
public InstalledModEntry(string fileName, string version, string sha512Hash, long size)
|
public InstalledModEntry(string fileName, string sha512Hash, long size)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(fileName))
|
if (string.IsNullOrWhiteSpace(fileName))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Name cannot be null, empty, or whitespace.", nameof(fileName));
|
throw new ArgumentException("File 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))
|
if (string.IsNullOrWhiteSpace(sha512Hash))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("SHA512Hash cannot be null, empty, or whitespace.", nameof(sha512Hash));
|
throw new ArgumentException("SHA-512 hash cannot be null, empty, or whitespace.", nameof(sha512Hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size == 0)
|
if (size <= 0)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Size cannot be 0, empty, or whitespace.", nameof(size));
|
throw new ArgumentOutOfRangeException(nameof(size), "Size must be greater than zero.");
|
||||||
}
|
}
|
||||||
|
|
||||||
FileName = fileName;
|
FileName = fileName;
|
||||||
Version = version;
|
|
||||||
Sha512Hash = sha512Hash;
|
Sha512Hash = sha512Hash;
|
||||||
Size = size;
|
Size = size;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,22 +8,20 @@ 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 name,
|
||||||
FileType type,
|
FileType type,
|
||||||
string sha512Hash,
|
string sha512Hash,
|
||||||
string? modrinthId = null,
|
long size)
|
||||||
string? modrinthVersionId = null)
|
|
||||||
{
|
{
|
||||||
FileName = RequireNonEmpty(name, nameof(name));
|
FileName = RequireNonEmpty(name, nameof(name));
|
||||||
Type = type;
|
Type = type;
|
||||||
Sha512Hash = RequireNonEmpty(sha512Hash, nameof(sha512Hash));
|
Sha512Hash = RequireNonEmpty(sha512Hash, nameof(sha512Hash));
|
||||||
ModrinthId = NormalizeOptional(modrinthId);
|
Size = size;
|
||||||
ModrinthVersionId = NormalizeOptional(modrinthVersionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string RequireNonEmpty(string value, string paramName)
|
private static string RequireNonEmpty(string value, string paramName)
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ namespace AlayaCore.Services
|
|||||||
await fileStream.FlushAsync(cancellationToken);
|
await fileStream.FlushAsync(cancellationToken);
|
||||||
|
|
||||||
string actualHash = ConvertToLowerHex(sha512.Hash);
|
string actualHash = ConvertToLowerHex(sha512.Hash);
|
||||||
|
|
||||||
if (!string.Equals(actualHash, normalizedExpectedHash, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(actualHash, normalizedExpectedHash, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
public sealed class InstallationStateService : IInstallStateService
|
public sealed class InstallationStateService : IInstallStateService
|
||||||
{
|
{
|
||||||
private const string JavaRuntimeFolderName = "java-runtime-epsilon";
|
private const string JAVA_RUNTIME_FOLDER_NAME = "java-runtime-epsilon";
|
||||||
|
|
||||||
private readonly IManifestService _manifestService;
|
private readonly IManifestService _manifestService;
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ namespace AlayaCore.Services
|
|||||||
string fullPath = Path.Combine(
|
string fullPath = Path.Combine(
|
||||||
AppContext.BaseDirectory,
|
AppContext.BaseDirectory,
|
||||||
"Java",
|
"Java",
|
||||||
JavaRuntimeFolderName,
|
JAVA_RUNTIME_FOLDER_NAME,
|
||||||
"bin",
|
"bin",
|
||||||
executableName);
|
executableName);
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
public sealed class JavaService : IJavaService
|
public sealed class JavaService : IJavaService
|
||||||
{
|
{
|
||||||
private const string DownloadFileName = "java-runtime.download";
|
private const string DOWNLOAD_FILE_NAME = "java-runtime.download";
|
||||||
private const string JavaInstallFolderName = "Java";
|
private const string JAVA_INSTALL_FOLDER_NAME = "Java";
|
||||||
private const string JavaArchiveHashPlaceholder = "REPLACE_WITH_MANIFEST_HASH_SUPPORT";
|
private const string JAVA_ARCHIVE_HASH_PLACEHOLDER = "REPLACE_WITH_MANIFEST_HASH_SUPPORT";
|
||||||
|
|
||||||
private readonly IDownloadService _downloadService;
|
private readonly IDownloadService _downloadService;
|
||||||
|
|
||||||
@@ -69,12 +69,12 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
private static string GetJavaInstallDirectory()
|
private static string GetJavaInstallDirectory()
|
||||||
{
|
{
|
||||||
return Path.Combine(AppContext.BaseDirectory, JavaInstallFolderName);
|
return Path.Combine(AppContext.BaseDirectory, JAVA_INSTALL_FOLDER_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetJavaDownloadPath()
|
private static string GetJavaDownloadPath()
|
||||||
{
|
{
|
||||||
return Path.Combine(AppContext.BaseDirectory, "Temp", DownloadFileName);
|
return Path.Combine(AppContext.BaseDirectory, "Temp", DOWNLOAD_FILE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolveJavaRootFolder(string javaExecutablePath)
|
private static string ResolveJavaRootFolder(string javaExecutablePath)
|
||||||
@@ -144,7 +144,7 @@ namespace AlayaCore.Services
|
|||||||
await _downloadService.DownloadFileAsync(
|
await _downloadService.DownloadFileAsync(
|
||||||
javaUri,
|
javaUri,
|
||||||
destinationPath,
|
destinationPath,
|
||||||
JavaArchiveHashPlaceholder,
|
JAVA_ARCHIVE_HASH_PLACEHOLDER,
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ 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;
|
||||||
|
|
||||||
public LauncherUpdateService(
|
public LauncherUpdateService(
|
||||||
IManifestService manifestService,
|
IManifestService manifestService,
|
||||||
UpdateServiceOptions options)
|
LauncherUpdateServiceOptions options)
|
||||||
{
|
{
|
||||||
_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));
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ namespace AlayaCore.Services
|
|||||||
{
|
{
|
||||||
public sealed class ManifestService : IManifestService
|
public sealed class ManifestService : IManifestService
|
||||||
{
|
{
|
||||||
private const string CoreManifestFileName = "CoreManifest.json";
|
private const string CORE_MANIFEST_FILE_NAME = "CoreManifest.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;
|
||||||
@@ -168,17 +168,17 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
public string GetLauncherManifestPath()
|
public string GetLauncherManifestPath()
|
||||||
{
|
{
|
||||||
return Path.Combine(_options.ManifestDirectoryPath, LauncherManifestFileName);
|
return Path.Combine(_options.ManifestDirectoryPath, LAUNCHER_MANIFEST_FILE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetCoreManifestPath()
|
public string GetCoreManifestPath()
|
||||||
{
|
{
|
||||||
return Path.Combine(_options.ManifestDirectoryPath, CoreManifestFileName);
|
return Path.Combine(_options.ManifestDirectoryPath, CORE_MANIFEST_FILE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetInstalledModsManifestPath()
|
public string GetInstalledModsManifestPath()
|
||||||
{
|
{
|
||||||
return Path.Combine(_options.ManifestDirectoryPath, InstalledModsManifestFileName);
|
return Path.Combine(_options.ManifestDirectoryPath, INSTALLED_MODS_MANIFEST_FILE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TModel?> LoadLocalManifestAsync<TDto, TModel>(
|
private async Task<TModel?> LoadLocalManifestAsync<TDto, TModel>(
|
||||||
|
|||||||
@@ -19,19 +19,22 @@ 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 ManifestServiceOptions _manifestOptions;
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClient _httpClient;
|
||||||
|
|
||||||
public ModService(
|
public ModService(
|
||||||
IDownloadService downloadService,
|
IDownloadService downloadService,
|
||||||
ModrinthConnectionOptions options,
|
ModrinthConnectionOptions options,
|
||||||
|
ManifestServiceOptions manifestOptions,
|
||||||
IHttpClient httpClient)
|
IHttpClient httpClient)
|
||||||
{
|
{
|
||||||
_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));
|
||||||
|
_manifestOptions = manifestOptions ?? throw new ArgumentNullException(nameof(manifestOptions));
|
||||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,9 +61,8 @@ namespace AlayaCore.Services
|
|||||||
.Where(file => file.Type == FileType.Mod)
|
.Where(file => file.Type == FileType.Mod)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
await RemoveStaleModsAsync(requiredMods, cancellationToken).ConfigureAwait(false);
|
RemoveStaleMods(requiredMods);
|
||||||
|
|
||||||
List<ModFileEntry> missingOrOutdatedMods = new List<ModFileEntry>();
|
|
||||||
List<InstalledModEntry> finalInstalledMods = new List<InstalledModEntry>();
|
List<InstalledModEntry> finalInstalledMods = new List<InstalledModEntry>();
|
||||||
|
|
||||||
foreach (ModFileEntry requiredMod in requiredMods)
|
foreach (ModFileEntry requiredMod in requiredMods)
|
||||||
@@ -72,52 +74,30 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
string destinationPath = GetModDestinationPath(requiredMod);
|
string destinationPath = GetModDestinationPath(requiredMod);
|
||||||
|
|
||||||
if (installedMod == null)
|
bool isValidInstalledMod =
|
||||||
|
installedMod != null &&
|
||||||
|
IsInstalledModUpToDate(installedMod, requiredMod) &&
|
||||||
|
File.Exists(destinationPath) &&
|
||||||
|
_downloadService.VerifyFileHash(destinationPath, requiredMod.Sha512Hash);
|
||||||
|
|
||||||
|
if (isValidInstalledMod)
|
||||||
{
|
{
|
||||||
missingOrOutdatedMods.Add(requiredMod);
|
finalInstalledMods.Add(installedMod!);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsInstalledModUpToDate(installedMod, requiredMod))
|
ModrinthModInfoModel modInfo = await ResolveModUrlAsync(requiredMod, cancellationToken).ConfigureAwait(false);
|
||||||
{
|
|
||||||
missingOrOutdatedMods.Add(requiredMod);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!File.Exists(destinationPath))
|
|
||||||
{
|
|
||||||
missingOrOutdatedMods.Add(requiredMod);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hashMatches = _downloadService.VerifyFileHash(destinationPath, requiredMod.Sha512Hash);
|
|
||||||
if (!hashMatches)
|
|
||||||
{
|
|
||||||
missingOrOutdatedMods.Add(requiredMod);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
modInfo.ModUrl,
|
||||||
destinationPath,
|
destinationPath,
|
||||||
mod.Sha512Hash,
|
requiredMod.Sha512Hash,
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
finalInstalledMods.Add(new InstalledModEntry(
|
finalInstalledMods.Add(new InstalledModEntry(
|
||||||
mod.FileName,
|
requiredMod.FileName,
|
||||||
mod.ModrinthVersionId ?? string.Empty,
|
requiredMod.Sha512Hash,
|
||||||
mod.Sha512Hash,
|
modInfo.Size));
|
||||||
model.Size));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await FlushInstalledModsManifestAsync(finalInstalledMods, cancellationToken).ConfigureAwait(false);
|
await FlushInstalledModsManifestAsync(finalInstalledMods, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -137,24 +117,16 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ModrinthModInfoModel> ResolveModUrlAsync(
|
private async Task<ModrinthModInfoModel> ResolveModUrlAsync(
|
||||||
@@ -166,17 +138,12 @@ 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.");
|
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);
|
|
||||||
|
|
||||||
using HttpResponseMessage response = await _httpClient.GetAsync(
|
using HttpResponseMessage response = await _httpClient.GetAsync(
|
||||||
new Uri(versionEndpoint, UriKind.Absolute),
|
new Uri(versionEndpoint, UriKind.Absolute),
|
||||||
@@ -189,7 +156,7 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"The Modrinth version response for '{fileEntry.FileName}' was empty.");
|
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' was empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
JObject jsonObject = JObject.Parse(json);
|
JObject jsonObject = JObject.Parse(json);
|
||||||
@@ -197,7 +164,7 @@ namespace AlayaCore.Services
|
|||||||
JArray? filesArray = jsonObject["files"] as JArray;
|
JArray? filesArray = jsonObject["files"] as JArray;
|
||||||
if (filesArray == null || filesArray.Count == 0)
|
if (filesArray == null || filesArray.Count == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"The Modrinth version response for '{fileEntry.FileName}' did not contain any files.");
|
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain any files.");
|
||||||
}
|
}
|
||||||
|
|
||||||
JObject? selectedFile = filesArray
|
JObject? selectedFile = filesArray
|
||||||
@@ -207,35 +174,57 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
if (selectedFile == null)
|
if (selectedFile == null)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"The Modrinth version response for '{fileEntry.FileName}' did not contain a usable file entry.");
|
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain a usable file entry.");
|
||||||
|
}
|
||||||
|
|
||||||
|
JObject? hashesObject = selectedFile["hashes"] as JObject;
|
||||||
|
|
||||||
|
if (hashesObject == null)
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"The mod metadata SHA-512 hash for '{fileEntry.FileName}' did not match the required manifest hash.");
|
||||||
}
|
}
|
||||||
|
|
||||||
string? modUrl = selectedFile.Value<string>("url");
|
string? modUrl = selectedFile.Value<string>("url");
|
||||||
if (string.IsNullOrWhiteSpace(modUrl))
|
if (string.IsNullOrWhiteSpace(modUrl))
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"The Modrinth version response for '{fileEntry.FileName}' did not contain a valid file URL.");
|
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))
|
if (!Uri.TryCreate(modUrl, UriKind.Absolute, out Uri? result))
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"The Modrinth version response for '{fileEntry.FileName}' contained an invalid file URL.");
|
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' contained an invalid file URL.");
|
||||||
}
|
}
|
||||||
|
|
||||||
long? size = selectedFile.Value<long?>("size");
|
long? size = selectedFile.Value<long?>("size");
|
||||||
if (!size.HasValue || size.Value <= 0)
|
if (!size.HasValue || size.Value <= 0)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"The Modrinth version response for '{fileEntry.FileName}' did not contain a valid file size.");
|
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file size.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ModrinthModInfoModel(result, size.Value);
|
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 static string GetModDestinationPath(ModFileEntry fileEntry)
|
||||||
@@ -261,17 +250,13 @@ namespace AlayaCore.Services
|
|||||||
return Path.Combine(AppContext.BaseDirectory, "Game", "mods");
|
return Path.Combine(AppContext.BaseDirectory, "Game", "mods");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task RemoveStaleModsAsync(
|
private static void RemoveStaleMods(IEnumerable<ModFileEntry> requiredMods)
|
||||||
IEnumerable<ModFileEntry> requiredMods,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
{
|
||||||
if (requiredMods == null)
|
if (requiredMods == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(requiredMods));
|
throw new ArgumentNullException(nameof(requiredMods));
|
||||||
}
|
}
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
string modsDirectory = GetModsDirectoryPath();
|
string modsDirectory = GetModsDirectoryPath();
|
||||||
if (!Directory.Exists(modsDirectory))
|
if (!Directory.Exists(modsDirectory))
|
||||||
{
|
{
|
||||||
@@ -285,8 +270,6 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
foreach (string filePath in Directory.GetFiles(modsDirectory))
|
foreach (string filePath in Directory.GetFiles(modsDirectory))
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
string fileName = Path.GetFileName(filePath);
|
string fileName = Path.GetFileName(filePath);
|
||||||
|
|
||||||
if (!requiredFileNames.Contains(fileName))
|
if (!requiredFileNames.Contains(fileName))
|
||||||
@@ -294,11 +277,9 @@ namespace AlayaCore.Services
|
|||||||
File.Delete(filePath);
|
File.Delete(filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task FlushInstalledModsManifestAsync(
|
private async Task FlushInstalledModsManifestAsync(
|
||||||
IEnumerable<InstalledModEntry> installedMods,
|
IEnumerable<InstalledModEntry> installedMods,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -308,13 +289,12 @@ namespace AlayaCore.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<InstalledModEntry> entries = installedMods.ToList();
|
List<InstalledModEntry> entries = installedMods.ToList();
|
||||||
|
|
||||||
InstalledModsManifestModel manifest = new InstalledModsManifestModel(entries);
|
InstalledModsManifestModel manifest = new InstalledModsManifestModel(entries);
|
||||||
|
|
||||||
string manifestsDirectory = Path.Combine(AppContext.BaseDirectory, "Manifests");
|
string manifestsDirectory = _manifestOptions.ManifestDirectoryPath;
|
||||||
Directory.CreateDirectory(manifestsDirectory);
|
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(
|
string json = JsonConvert.SerializeObject(
|
||||||
@@ -323,7 +303,6 @@ namespace AlayaCore.Services
|
|||||||
mods = manifest.Mods.Select(mod => new
|
mods = manifest.Mods.Select(mod => new
|
||||||
{
|
{
|
||||||
fileName = mod.FileName,
|
fileName = mod.FileName,
|
||||||
version = mod.Version,
|
|
||||||
sha512Hash = mod.Sha512Hash,
|
sha512Hash = mod.Sha512Hash,
|
||||||
size = mod.Size
|
size = mod.Size
|
||||||
})
|
})
|
||||||
@@ -332,8 +311,12 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
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);
|
{
|
||||||
|
File.Delete(manifestPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Move(temporaryManifestPath, manifestPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,132 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AlayaCore.Abstractions.Interfaces.Services;
|
||||||
|
using AlayaCore.Models.Configuration;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace AlayaCore.Services
|
namespace AlayaCore.Services
|
||||||
{
|
{
|
||||||
public class SettingsService
|
public sealed class SettingsService : ISettingsService
|
||||||
{
|
{
|
||||||
|
private const string LauncherSettingsFileName = "Launcher.json";
|
||||||
|
|
||||||
|
public LauncherOptions LauncherOptions { get; }
|
||||||
|
|
||||||
|
public SettingsService(LauncherOptions launcherOptions)
|
||||||
|
{
|
||||||
|
LauncherOptions = launcherOptions ?? throw new ArgumentNullException(nameof(launcherOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetForceReinstallAsync(bool value, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
LauncherOptions.ForceReinstall = value;
|
||||||
|
await SaveLauncherOptionsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveLauncherOptionsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await SaveAsync(
|
||||||
|
LauncherSettingsFileName,
|
||||||
|
LauncherOptions,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadLauncherOptionsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
LauncherOptions? loadedOptions = await LoadAsync<LauncherOptions>(
|
||||||
|
LauncherSettingsFileName,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (loadedOptions == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LauncherOptions.ForceReinstall = loadedOptions.ForceReinstall;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveAsync<T>(
|
||||||
|
string fileName,
|
||||||
|
T value,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Could not resolve the settings directory path.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(directoryPath);
|
||||||
|
|
||||||
|
string temporaryPath = fullPath + ".tmp";
|
||||||
|
string json = JsonConvert.SerializeObject(value, Formatting.Indented);
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(temporaryPath, json, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
File.Delete(fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Move(temporaryPath, fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<T?> LoadAsync<T>(
|
||||||
|
string fileName,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
string json = await File.ReadAllTextAsync(fullPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<T>(json);
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"Failed to deserialize settings file '{fullPath}' to {typeof(T).Name}.",
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetFullPath(string fileName)
|
||||||
|
{
|
||||||
|
return Path.Combine(AppContext.BaseDirectory, "Config", fileName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,74 @@
|
|||||||
namespace AlayaCore.States
|
namespace AlayaCore.States
|
||||||
{
|
{
|
||||||
public class LaunchPlan
|
public enum LaunchState
|
||||||
{
|
{
|
||||||
|
Ready,
|
||||||
|
LauncherNeedsUpdate,
|
||||||
|
InstallJava,
|
||||||
|
InstallMinecraft,
|
||||||
|
InstallNeoforge,
|
||||||
|
SyncMods
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LaunchPlan
|
||||||
|
{
|
||||||
|
public bool LauncherNeedsUpdate { get; }
|
||||||
|
public bool JavaNeedsInstallOrUpdate { get; }
|
||||||
|
public bool MinecraftNeedsInstallOrUpdate { get; }
|
||||||
|
public bool NeoforgeNeedsInstallOrUpdate { get; }
|
||||||
|
public bool ModsNeedSync { get; }
|
||||||
|
|
||||||
|
public LaunchState State => ComputeState();
|
||||||
|
|
||||||
|
public bool CanRun =>
|
||||||
|
State == LaunchState.Ready;
|
||||||
|
|
||||||
|
public bool NeedsUpdating =>
|
||||||
|
State != LaunchState.Ready;
|
||||||
|
|
||||||
|
public LaunchPlan(
|
||||||
|
bool launcherNeedsUpdate,
|
||||||
|
bool javaNeedsInstallOrUpdate,
|
||||||
|
bool minecraftNeedsInstallOrUpdate,
|
||||||
|
bool neoforgeNeedsInstallOrUpdate,
|
||||||
|
bool modsNeedSync)
|
||||||
|
{
|
||||||
|
LauncherNeedsUpdate = launcherNeedsUpdate;
|
||||||
|
JavaNeedsInstallOrUpdate = javaNeedsInstallOrUpdate;
|
||||||
|
MinecraftNeedsInstallOrUpdate = minecraftNeedsInstallOrUpdate;
|
||||||
|
NeoforgeNeedsInstallOrUpdate = neoforgeNeedsInstallOrUpdate;
|
||||||
|
ModsNeedSync = modsNeedSync;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LaunchState ComputeState()
|
||||||
|
{
|
||||||
|
// Priority order matters a LOT here
|
||||||
|
if (LauncherNeedsUpdate)
|
||||||
|
return LaunchState.LauncherNeedsUpdate;
|
||||||
|
|
||||||
|
if (JavaNeedsInstallOrUpdate)
|
||||||
|
return LaunchState.InstallJava;
|
||||||
|
|
||||||
|
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,
|
||||||
|
javaNeedsInstallOrUpdate: false,
|
||||||
|
minecraftNeedsInstallOrUpdate: false,
|
||||||
|
neoforgeNeedsInstallOrUpdate: false,
|
||||||
|
modsNeedSync: false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ namespace AlayaCore.Utilities.Extensions
|
|||||||
throw new ArgumentNullException(nameof(dto));
|
throw new ArgumentNullException(nameof(dto));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new InstalledModEntry(dto.FileName, dto.Version, dto.Sha512Hash, dto.Size);
|
return new InstalledModEntry(dto.FileName, dto.Sha512Hash, dto.Size);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LauncherManifestModel ToModel(this LauncherManifestDto dto)
|
public static LauncherManifestModel ToModel(this LauncherManifestDto dto)
|
||||||
@@ -67,8 +67,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)
|
||||||
@@ -102,9 +101,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,
|
|
||||||
ModrinthVersionId = model.ModrinthVersionId
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user