feat(launcher): add verified first-run installer
This commit is contained in:
parent
60f627998c
commit
ff6ebb6a6a
28 changed files with 3259 additions and 125 deletions
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.Launcher.Core.Orchestration;
|
||||
using AcDream.Launcher.Core.Profiles;
|
||||
using AcDream.Launcher.Core.Launching;
|
||||
using AcDream.Launcher.Core.Installation;
|
||||
using AcDream.Launcher.ViewModels;
|
||||
|
||||
namespace AcDream.Launcher.Tests;
|
||||
|
|
@ -32,7 +33,7 @@ public sealed class LauncherWindowViewModelTests
|
|||
Assert.True(session.IsActive);
|
||||
|
||||
Assert.True(viewModel.IsFirstRunRequired);
|
||||
Assert.Contains("LA9", viewModel.FirstRunWizardShell.Body, StringComparison.Ordinal);
|
||||
Assert.Contains("SHA-256", viewModel.FirstRunWizardShell.Body, StringComparison.Ordinal);
|
||||
Assert.Contains("LA10", viewModel.UpdatePromptShell.Body, StringComparison.Ordinal);
|
||||
viewModel.FirstRunWizardShell.OpenCommand.Execute(null);
|
||||
Assert.True(viewModel.FirstRunWizardShell.IsOpen);
|
||||
|
|
@ -325,6 +326,92 @@ public sealed class LauncherWindowViewModelTests
|
|||
Assert.False(viewModel.EditorDialog.IsOpen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FirstRunWizardAutoDetectsValidatesAndPublishesVerifiedInstall()
|
||||
{
|
||||
using var orchestrator = new FakeLauncherOrchestrator
|
||||
{
|
||||
Session = FakeLauncherOrchestrator.CreateSession(
|
||||
LauncherActivityState.Exited,
|
||||
"Exited cleanly."),
|
||||
};
|
||||
var installer = new FakeLauncherInstaller();
|
||||
using var viewModel = CreateInitialized(orchestrator, installer);
|
||||
|
||||
viewModel.FirstRunWizardShell.OpenCommand.Execute(null);
|
||||
|
||||
Assert.Equal(installer.DetectedDirectory, viewModel.FirstRunWizardShell.DatDirectory);
|
||||
Assert.True(viewModel.FirstRunWizardShell.IsDatDirectoryValid);
|
||||
Assert.True(viewModel.FirstRunWizardShell.StartCommand.CanExecute(null));
|
||||
|
||||
viewModel.FirstRunWizardShell.SelectDatDirectory("incomplete-manual-path");
|
||||
Assert.False(viewModel.FirstRunWizardShell.IsDatDirectoryValid);
|
||||
viewModel.FirstRunWizardShell.SelectDatDirectory(installer.DetectedDirectory);
|
||||
Assert.True(viewModel.FirstRunWizardShell.IsDatDirectoryValid);
|
||||
|
||||
viewModel.FirstRunWizardShell.ThreadsText = "3";
|
||||
await viewModel.FirstRunWizardShell.StartCommand.ExecuteAsync();
|
||||
|
||||
Assert.Equal((installer.DetectedDirectory, 3), installer.InstallRequest);
|
||||
Assert.Equal(installer.Record, orchestrator.InstalledRecord);
|
||||
Assert.False(viewModel.IsFirstRunRequired);
|
||||
Assert.Equal(LauncherInstallPhase.Completed, viewModel.FirstRunWizardShell.Phase);
|
||||
Assert.Equal(100, viewModel.FirstRunWizardShell.ProgressPercent);
|
||||
Assert.False(viewModel.FirstRunWizardShell.HasError);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FirstRunWizardCancellationAndFailureRemainVisibleAndPublishNothing()
|
||||
{
|
||||
using var orchestrator = new FakeLauncherOrchestrator
|
||||
{
|
||||
Session = FakeLauncherOrchestrator.CreateSession(
|
||||
LauncherActivityState.Exited,
|
||||
"Exited cleanly."),
|
||||
};
|
||||
var entered = new TaskCompletionSource(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var installer = new FakeLauncherInstaller
|
||||
{
|
||||
InstallHandler = async (_, _, progress, cancellationToken) =>
|
||||
{
|
||||
progress?.Report(new LauncherInstallProgress(
|
||||
LauncherInstallPhase.BakingMeshes,
|
||||
"Baking mesh assets.",
|
||||
1,
|
||||
10));
|
||||
entered.SetResult();
|
||||
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
|
||||
throw new InvalidOperationException("unreachable");
|
||||
},
|
||||
};
|
||||
using var viewModel = CreateInitialized(orchestrator, installer);
|
||||
viewModel.FirstRunWizardShell.OpenCommand.Execute(null);
|
||||
|
||||
Task install = viewModel.FirstRunWizardShell.StartCommand.ExecuteAsync();
|
||||
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
Assert.True(viewModel.FirstRunWizardShell.CancelCommand.CanExecute(null));
|
||||
Assert.False(viewModel.FirstRunWizardShell.CanEditInputs);
|
||||
viewModel.FirstRunWizardShell.CancelCommand.Execute(null);
|
||||
await install;
|
||||
|
||||
Assert.Equal(LauncherInstallPhase.Cancelled, viewModel.FirstRunWizardShell.Phase);
|
||||
Assert.Null(orchestrator.InstalledRecord);
|
||||
Assert.False(viewModel.FirstRunWizardShell.HasError);
|
||||
|
||||
installer.InstallHandler = (_, _, _, _) =>
|
||||
Task.FromException<LauncherInstallResult>(
|
||||
new LauncherInstallException("fixture bake failed"));
|
||||
await viewModel.FirstRunWizardShell.StartCommand.ExecuteAsync();
|
||||
|
||||
Assert.Equal(LauncherInstallPhase.Failed, viewModel.FirstRunWizardShell.Phase);
|
||||
Assert.Contains(
|
||||
"fixture bake failed",
|
||||
viewModel.FirstRunWizardShell.Error ?? string.Empty,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Null(orchestrator.InstalledRecord);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ActiveSessionStopAndFinishedSessionClearUseCoreOwnership()
|
||||
{
|
||||
|
|
@ -345,11 +432,13 @@ public sealed class LauncherWindowViewModelTests
|
|||
}
|
||||
|
||||
private static LauncherWindowViewModel CreateInitialized(
|
||||
FakeLauncherOrchestrator orchestrator)
|
||||
FakeLauncherOrchestrator orchestrator,
|
||||
ILauncherInstaller? installer = null)
|
||||
{
|
||||
var viewModel = new LauncherWindowViewModel(
|
||||
orchestrator,
|
||||
new ImmediateUiDispatcher());
|
||||
new ImmediateUiDispatcher(),
|
||||
installer);
|
||||
viewModel.Initialize();
|
||||
return viewModel;
|
||||
}
|
||||
|
|
@ -425,14 +514,18 @@ public sealed class LauncherWindowViewModelTests
|
|||
|
||||
public string? StoppedSessionId { get; private set; }
|
||||
|
||||
public LauncherInstallRecord? InstalledRecord { get; private set; }
|
||||
|
||||
public void LoadProfiles() => LoadCalled = true;
|
||||
|
||||
public LauncherStateSnapshot GetSnapshot() => new(
|
||||
[CreateServerSnapshot()],
|
||||
[Session],
|
||||
Platform,
|
||||
IsInstallationReady: false,
|
||||
InstallationStatus: "No installed client is configured.");
|
||||
IsInstallationReady: InstalledRecord is not null,
|
||||
InstallationStatus: InstalledRecord is null
|
||||
? "No installed client is configured."
|
||||
: "Client content verified.");
|
||||
|
||||
public LauncherCapability GetLaunchCapability(LaunchMode mode) =>
|
||||
Platform.ForLaunchMode(mode);
|
||||
|
|
@ -448,6 +541,8 @@ public sealed class LauncherWindowViewModelTests
|
|||
|
||||
public void SetInstallRecord(LauncherInstallRecord? installRecord)
|
||||
{
|
||||
InstalledRecord = installRecord;
|
||||
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void AddServer(string name, string host, int port) =>
|
||||
|
|
@ -589,4 +684,83 @@ public sealed class LauncherWindowViewModelTests
|
|||
ActivityStatus: "Connected."),
|
||||
]);
|
||||
}
|
||||
|
||||
private sealed class FakeLauncherInstaller : ILauncherInstaller
|
||||
{
|
||||
public string DetectedDirectory { get; } = Path.GetFullPath("retail-dats");
|
||||
|
||||
public LauncherInstallRecord Record { get; }
|
||||
|
||||
public (string DatDirectory, int Threads)? InstallRequest { get; private set; }
|
||||
|
||||
public Func<
|
||||
string,
|
||||
int,
|
||||
IProgress<LauncherInstallProgress>?,
|
||||
CancellationToken,
|
||||
Task<LauncherInstallResult>>? InstallHandler { get; set; }
|
||||
|
||||
public FakeLauncherInstaller()
|
||||
{
|
||||
Record = new LauncherInstallRecord(
|
||||
DetectedDirectory,
|
||||
Path.GetFullPath("data/pak/acdream.pak"),
|
||||
new string('a', 64),
|
||||
123,
|
||||
LauncherInstallRecordStore.CurrentBakeToolVersion);
|
||||
}
|
||||
|
||||
public IReadOnlyList<DatDirectoryValidation> DetectDatDirectories() =>
|
||||
[
|
||||
ValidateDatDirectory(DetectedDirectory),
|
||||
];
|
||||
|
||||
public DatDirectoryValidation ValidateDatDirectory(string? directory) =>
|
||||
string.Equals(directory, DetectedDirectory, StringComparison.Ordinal)
|
||||
? new DatDirectoryValidation(
|
||||
DetectedDirectory,
|
||||
true,
|
||||
"All four required retail DAT files were found.",
|
||||
[])
|
||||
: new DatDirectoryValidation(
|
||||
directory ?? string.Empty,
|
||||
false,
|
||||
"The DAT directory is incomplete.",
|
||||
DatDirectoryLocator.RequiredFileNames);
|
||||
|
||||
public Task<InstallRecordVerification> LoadExistingAsync(
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new InstallRecordVerification(
|
||||
InstallRecordVerificationState.Missing,
|
||||
null,
|
||||
"Client content is not installed."));
|
||||
|
||||
public Task<LauncherInstallResult> InstallAsync(
|
||||
string datDirectory,
|
||||
int threads,
|
||||
IProgress<LauncherInstallProgress>? progress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
InstallRequest = (datDirectory, threads);
|
||||
if (InstallHandler is not null)
|
||||
{
|
||||
return InstallHandler(
|
||||
datDirectory,
|
||||
threads,
|
||||
progress,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
progress?.Report(new LauncherInstallProgress(
|
||||
LauncherInstallPhase.BakingMeshes,
|
||||
"Baking mesh assets.",
|
||||
5,
|
||||
10));
|
||||
progress?.Report(new LauncherInstallProgress(
|
||||
LauncherInstallPhase.VerifyingPackage,
|
||||
"Verifying package."));
|
||||
return Task.FromResult(new LauncherInstallResult(Record));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue