diff --git a/src/AcDream.Launcher/MainWindow.axaml b/src/AcDream.Launcher/MainWindow.axaml
index f5029129..44526b0e 100644
--- a/src/AcDream.Launcher/MainWindow.axaml
+++ b/src/AcDream.Launcher/MainWindow.axaml
@@ -350,6 +350,25 @@
VerticalAlignment="Center">
+
+
+
+
+
+
+
+
@@ -423,6 +442,7 @@
AutomationProperties.Name="Build and install client content"
Command="{Binding FirstRunWizardShell.StartCommand}" />
+
diff --git a/src/AcDream.Launcher/ViewModels/FirstRunInstallerViewModel.cs b/src/AcDream.Launcher/ViewModels/FirstRunInstallerViewModel.cs
index 61597ffc..6d4ca733 100644
--- a/src/AcDream.Launcher/ViewModels/FirstRunInstallerViewModel.cs
+++ b/src/AcDream.Launcher/ViewModels/FirstRunInstallerViewModel.cs
@@ -25,6 +25,7 @@ public sealed class FirstRunInstallerViewModel : ObservableObject, IDisposable
private string? _error;
private LauncherInstallPhase _phase = LauncherInstallPhase.Idle;
private double _progressPercent;
+ private bool _isCompleted;
private bool _isOpen;
private bool _isRunning;
private bool _isDatDirectoryValid;
@@ -44,6 +45,9 @@ public sealed class FirstRunInstallerViewModel : ObservableObject, IDisposable
_canStart = canStart ?? (() => true);
OpenCommand = new RelayCommand(Open, () => !_disposed && _canOpen());
+ AcknowledgeCompletionCommand = new RelayCommand(
+ AcknowledgeCompletion,
+ () => IsCompleted && !IsRunning);
CloseCommand = new RelayCommand(Close, () => !IsRunning);
ValidateCommand = new RelayCommand(Validate, () => !IsRunning);
StartCommand = new AsyncRelayCommand(StartAsync, CanBeginInstall);
@@ -183,6 +187,9 @@ public sealed class FirstRunInstallerViewModel : ObservableObject, IDisposable
public RelayCommand OpenCommand { get; }
+ /// LU4: OK on the "Setup complete" panel.
+ public RelayCommand AcknowledgeCompletionCommand { get; }
+
public RelayCommand CloseCommand { get; }
public RelayCommand ValidateCommand { get; }
@@ -200,6 +207,34 @@ public sealed class FirstRunInstallerViewModel : ObservableObject, IDisposable
: message;
}
+ ///
+ /// LU4: true once first-run setup has actually succeeded — the pak was
+ /// built, verified, and its record published. The wizard replaces its form
+ /// with a plain "Setup complete" panel whose single OK button returns to
+ /// the launcher, instead of leaving the user looking at a finished
+ /// progress bar wondering whether they may close the window.
+ ///
+ public bool IsCompleted
+ {
+ get => _isCompleted;
+ private set
+ {
+ if (SetProperty(ref _isCompleted, value))
+ {
+ OnPropertyChanged(nameof(ShowSetupForm));
+ NotifyCommandStates();
+ }
+ }
+ }
+
+ /// The form and the completion panel are mutually exclusive.
+ public bool ShowSetupForm => !IsCompleted;
+
+ public string CompletedTitle => "Setup complete";
+
+ public string CompletedBody =>
+ "acdream built and verified your game content. You can play now.";
+
public void NotifyCommandStates()
{
OpenCommand.NotifyCanExecuteChanged();
@@ -207,6 +242,7 @@ public sealed class FirstRunInstallerViewModel : ObservableObject, IDisposable
ValidateCommand.NotifyCanExecuteChanged();
StartCommand.NotifyCanExecuteChanged();
CancelCommand.NotifyCanExecuteChanged();
+ AcknowledgeCompletionCommand.NotifyCanExecuteChanged();
}
public void Close()
@@ -231,6 +267,15 @@ public sealed class FirstRunInstallerViewModel : ObservableObject, IDisposable
NotifyCommandStates();
}
+ /// LU4: OK on the completion panel. Closes the wizard and leaves
+ /// it ready to be reopened as an ordinary form (a user may reinstall
+ /// content later against a different DAT folder).
+ private void AcknowledgeCompletion()
+ {
+ IsCompleted = false;
+ IsOpen = false;
+ }
+
private void Open()
{
if (string.IsNullOrWhiteSpace(DatDirectory))
@@ -306,6 +351,11 @@ public sealed class FirstRunInstallerViewModel : ObservableObject, IDisposable
Phase = LauncherInstallPhase.Completed;
ProgressPercent = 100;
Status = "Client content installed and verified. Launch is enabled.";
+ // LU4: raised only here, AFTER the record is published, so the
+ // launcher behind the dialog is already in its launch-enabled
+ // state when the user presses OK. The cancelled and failed
+ // branches below deliberately never reach this.
+ IsCompleted = true;
}
catch (OperationCanceledException)
{
diff --git a/tests/AcDream.Launcher.Tests/LauncherWindowViewModelTests.cs b/tests/AcDream.Launcher.Tests/LauncherWindowViewModelTests.cs
index 7bd44358..849e5a7c 100644
--- a/tests/AcDream.Launcher.Tests/LauncherWindowViewModelTests.cs
+++ b/tests/AcDream.Launcher.Tests/LauncherWindowViewModelTests.cs
@@ -442,6 +442,74 @@ public sealed class LauncherWindowViewModelTests
Assert.True(orchestrator.ClearCalled);
}
+ ///
+ /// LU4. A successful first-run setup ends with a plain "Setup complete"
+ /// panel and one OK button that returns to the launcher — not a finished
+ /// progress bar the user has to decide what to do with.
+ ///
+ [Fact]
+ public async Task FirstRunSetupEndsWithACompletionPanelThatOkReturnsFrom()
+ {
+ 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.False(viewModel.FirstRunWizardShell.IsCompleted);
+ Assert.True(viewModel.FirstRunWizardShell.ShowSetupForm);
+
+ await viewModel.FirstRunWizardShell.StartCommand.ExecuteAsync();
+
+ Assert.True(viewModel.FirstRunWizardShell.IsCompleted);
+ // The form is gone; the completion panel is what the user sees.
+ Assert.False(viewModel.FirstRunWizardShell.ShowSetupForm);
+ Assert.True(viewModel.FirstRunWizardShell.IsOpen);
+ // The record is already published, so the launcher behind the dialog
+ // is in its launch-enabled state before OK is pressed.
+ Assert.NotNull(orchestrator.InstalledRecord);
+
+ viewModel.FirstRunWizardShell.AcknowledgeCompletionCommand.Execute(null);
+
+ Assert.False(viewModel.FirstRunWizardShell.IsOpen);
+ Assert.False(viewModel.FirstRunWizardShell.IsCompleted);
+ Assert.True(viewModel.FirstRunWizardShell.ShowSetupForm);
+ }
+
+ ///
+ /// LU4. Cancellation and failure must NOT claim success. They keep their
+ /// existing status/error reporting and leave the form on screen.
+ ///
+ [Fact]
+ public async Task AFailedFirstRunSetupNeverShowsTheCompletionPanel()
+ {
+ using var orchestrator = new FakeLauncherOrchestrator
+ {
+ Session = FakeLauncherOrchestrator.CreateSession(
+ LauncherActivityState.Exited,
+ "Exited cleanly."),
+ };
+ var installer = new FakeLauncherInstaller
+ {
+ InstallHandler = (_, _, _, _) =>
+ Task.FromException(
+ new LauncherInstallException("The bake tool exited with code 3.")),
+ };
+ using var viewModel = CreateInitialized(orchestrator, installer);
+
+ viewModel.FirstRunWizardShell.OpenCommand.Execute(null);
+ await viewModel.FirstRunWizardShell.StartCommand.ExecuteAsync();
+
+ Assert.False(viewModel.FirstRunWizardShell.IsCompleted);
+ Assert.True(viewModel.FirstRunWizardShell.ShowSetupForm);
+ Assert.True(viewModel.FirstRunWizardShell.HasError);
+ Assert.Null(orchestrator.InstalledRecord);
+ }
+
///
/// LU1. Ordinary startup trusts a remembered digest so the window is not
/// held behind a multi-second hash of a ~28 GiB file; "Verify files" is