Files
AlayaCore/AlayaCore/LaunchDirector.cs
2026-04-04 20:51:53 +01:00

294 lines
11 KiB
C#

using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AlayaCore.Abstractions.Interfaces;
using AlayaCore.Abstractions.Interfaces.Services;
using AlayaCore.Installation;
using AlayaCore.Models.Configuration;
using AlayaCore.Models.Manifests;
using AlayaCore.States;
namespace AlayaCore
{
public sealed class LaunchDirector : ILaunchDirector
{
private readonly IManifestService _manifestService;
private readonly IUpdateService _updateService;
private readonly IInstallStateService _installStateService;
private readonly IJavaService _javaService;
private readonly IModService _modService;
private readonly IGameLaunchService _gameLaunchService;
private readonly LauncherOptions _options;
public bool CanRun { get; private set; }
public bool NeedsUpdating { get; private set; }
public LaunchPlan? CurrentPlan { get; private set; }
public LaunchDirector(
IManifestService manifestService,
IUpdateService updateService,
IInstallStateService installStateService,
IJavaService javaService,
IModService modService,
IGameLaunchService gameLaunchService,
LauncherOptions options)
{
_manifestService = manifestService ?? throw new ArgumentNullException(nameof(manifestService));
_updateService = updateService ?? throw new ArgumentNullException(nameof(updateService));
_installStateService = installStateService ?? throw new ArgumentNullException(nameof(installStateService));
_javaService = javaService ?? throw new ArgumentNullException(nameof(javaService));
_modService = modService ?? throw new ArgumentNullException(nameof(modService));
_gameLaunchService = gameLaunchService ?? throw new ArgumentNullException(nameof(gameLaunchService));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
public async Task<LaunchPlan> EvaluateAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
bool launcherNeedsUpdate = await _updateService
.DoesLauncherNeedUpdating(cancellationToken)
.ConfigureAwait(false);
if (launcherNeedsUpdate)
{
LaunchPlan launcherUpdatePlan = new LaunchPlan(
launcherNeedsUpdate: true,
javaNeedsInstallOrUpdate: false,
minecraftNeedsInstallOrUpdate: false,
neoforgeNeedsInstallOrUpdate: false,
modsNeedSync: false);
ApplyPlan(launcherUpdatePlan);
return launcherUpdatePlan;
}
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
.GetLocalCoreManifestAsync(cancellationToken)
.ConfigureAwait(false);
Version remoteVersion = await _manifestService
.GetRemoteCoreManifestVersionAsync(cancellationToken)
.ConfigureAwait(false);
if (localManifest == null || localManifest.AlayaVersion != remoteVersion)
{
localManifest = await _manifestService
.GetCoreManifestAsync(cancellationToken)
.ConfigureAwait(false);
}
if (localManifest == null)
{
throw new FileNotFoundException("Local core manifest was not found after refresh.");
}
return localManifest;
}
private static bool DoModsNeedSync(ManifestModel manifest, InstallEnvironment environment)
{
if (manifest == null)
{
throw new ArgumentNullException(nameof(manifest));
}
if (environment == null)
{
throw new ArgumentNullException(nameof(environment));
}
var requiredMods = manifest.Files
.Where(file => file.Type == AlayaCore.Utilities.Enums.FileType.Mod)
.ToList();
var installedMods = environment.InstalledModsManifest.Mods;
if (requiredMods.Count != installedMods.Count)
{
return true;
}
foreach (ModFileEntry requiredMod in requiredMods)
{
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;
}
}
}