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 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 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) { 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; } } return false; } private void ApplyPlan(LaunchPlan plan) { CurrentPlan = plan ?? throw new ArgumentNullException(nameof(plan)); NeedsUpdating = plan.NeedsUpdating; CanRun = plan.CanRun; } } }