feat(launcher): stabilize prepared content updates
Some checks failed
CI / linux-portable (push) Failing after 3m12s
CI / windows-gate (push) Failing after 6m35s
CI / release (push) Has been skipped

This commit is contained in:
Erik 2026-08-25 19:17:13 +02:00
parent f160f3fee1
commit af9327a17b
42 changed files with 3706 additions and 147 deletions

View file

@ -49,23 +49,6 @@ public sealed partial class App : Application
Path.Combine(
AppContext.BaseDirectory,
"acdream-bake" + executableSuffix));
InstallRecordVerification verification;
try
{
// Hashing the package before constructing the orchestrator is
// intentional: no launch action is enabled until the persisted
// size/SHA/tool-version record has been verified.
verification = installer.LoadExistingAsync()
.GetAwaiter()
.GetResult();
}
catch (Exception ex)
{
verification = new InstallRecordVerification(
InstallRecordVerificationState.Invalid,
null,
$"Client content verification failed: {ex.Message}");
}
LauncherUpdateComposition updates = LauncherUpdateComposition.Create(
paths,
@ -81,8 +64,8 @@ public sealed partial class App : Application
profiles,
paths,
updates.Executables,
verification.Record,
installationStatus: verification.Status,
installRecord: null,
installationStatus: "Checking installed game content…",
updateSessionBarrier: updates.Versions.Barrier);
// LU2: a launcher update installs and restarts by itself. The
// helper waits on THIS process id and cannot replace files the
@ -108,18 +91,32 @@ public sealed partial class App : Application
updates.Updater,
applyLauncherUpdate,
() => desktop.Shutdown());
_viewModel.Initialize();
desktop.MainWindow = new MainWindow
var mainWindow = new MainWindow
{
DataContext = _viewModel,
};
desktop.MainWindow = mainWindow;
_viewModel.Initialize();
mainWindow.Opened += OnMainWindowOpened;
desktop.Exit += OnDesktopExit;
}
base.OnFrameworkInitializationCompleted();
}
private void OnMainWindowOpened(object? sender, EventArgs e)
{
if (sender is MainWindow window)
{
window.Opened -= OnMainWindowOpened;
}
if (_viewModel is not null)
{
_ = _viewModel.StartBackgroundInitializationAsync();
}
}
private void OnDesktopExit(object? sender, ControlledApplicationLifetimeExitEventArgs e)
{
_viewModel?.Dispose();

View file

@ -71,9 +71,15 @@ internal sealed class LauncherUpdateComposition : IDisposable
ReleaseManifestClient? manifestClient = null;
try
{
_ = initialize is null
? versions.LoadAndRecoverAsync(rid).GetAwaiter().GetResult()
: initialize(versions, rid);
// Production initialization is deliberately deferred until after
// MainWindow.Opened. Client-version recovery verifies every file
// in the active version and is therefore not composition-root
// work. The injectable callback remains only for focused failure
// composition tests.
if (initialize is not null)
{
_ = initialize(versions, rid);
}
artifactClient = new HttpClient(
new HttpClientHandler
{

View file

@ -56,14 +56,15 @@
<Border Classes="card"
Padding="12"
Background="#4B3820"
IsVisible="{Binding IsFirstRunRequired}">
IsVisible="{Binding ShowInstallationBanner}">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Spacing="3">
<TextBlock Text="Client setup required" FontWeight="SemiBold" />
<TextBlock Text="{Binding InstallationBannerTitle}" FontWeight="SemiBold" />
<TextBlock Text="{Binding InstallationStatus}" TextWrapping="Wrap" />
</StackPanel>
<Button Grid.Column="1"
Content="Open setup"
IsVisible="{Binding !IsInstallationChecking}"
Command="{Binding FirstRunWizardShell.OpenCommand}" />
</Grid>
</Border>
@ -472,7 +473,7 @@
IsCancel="True"
AutomationProperties.Name="Close first-run setup"
Command="{Binding FirstRunWizardShell.CloseCommand}" />
<Button Content="Build and install"
<Button Content="{Binding FirstRunWizardShell.StartActionText}"
Classes="primary"
IsDefault="True"
AutomationProperties.Name="Build and install client content"

View file

@ -30,6 +30,9 @@ public sealed class FirstRunInstallerViewModel : ObservableObject, IDisposable
private bool _isOpen;
private bool _isRunning;
private bool _isDatDirectoryValid;
private LauncherInstallRecord? _contentUpdateBase;
private ContentMigrationPlan? _contentMigration;
private string? _completionRequirement;
private bool _disposed;
public FirstRunInstallerViewModel(
@ -57,12 +60,24 @@ public sealed class FirstRunInstallerViewModel : ObservableObject, IDisposable
() => IsRunning && _cancellation is not null);
}
public string Title => "First-run setup";
public bool IsContentUpdate => _contentMigration is not null;
public string Body =>
"Select the retail Asheron's Call DAT folder. acdream will validate "
+ "the four required files, build DataDirectory/pak/acdream.pak, and "
+ "verify its SHA-256 before enabling launch.";
public string Title => IsContentUpdate
? "World data update required"
: "First-run setup";
public string Body => IsContentUpdate
? BuildContentUpdateBody()
: "Select the retail Asheron's Call DAT folder. acdream will validate "
+ "the four required files, build DataDirectory/pak/acdream.pak, and "
+ "verify its SHA-256 before enabling launch. No work begins until "
+ "you choose Build and install.";
public string StartActionText => IsContentUpdate
? _contentMigration?.Kind == ContentWorkKind.Overlay
? "Build small update"
: "Rebuild world data"
: "Build and install";
public string DatDirectory
{
@ -233,10 +248,51 @@ public sealed class FirstRunInstallerViewModel : ObservableObject, IDisposable
/// <summary>The form and the completion panel are mutually exclusive.</summary>
public bool ShowSetupForm => !IsCompleted;
public string CompletedTitle => "Setup complete";
public string CompletedTitle => IsContentUpdate
? "World data updated"
: "Setup complete";
public string CompletedBody =>
"acdream built and verified your game content. You can play now.";
public string CompletedBody => IsContentUpdate
? _completionRequirement
?? "acdream built and verified the required world data. You can play now."
: "acdream built and verified your game content. You can play now.";
/// <summary>
/// Keeps the completion panel honest when prepared content is ready but
/// cannot be paired with the active client until the update step finishes.
/// </summary>
public void SetCompletionRequirement(string? requirement)
{
_completionRequirement = string.IsNullOrWhiteSpace(requirement)
? null
: requirement;
OnPropertyChanged(nameof(CompletedBody));
}
/// <summary>
/// Switches the existing setup surface into an explicit content-update
/// confirmation. Merely opening this surface never starts a hash or bake.
/// </summary>
public void PrepareContentUpdate(
LauncherInstallRecord baseRecord,
ContentMigrationPlan migration)
{
ArgumentNullException.ThrowIfNull(baseRecord);
ArgumentNullException.ThrowIfNull(migration);
_contentUpdateBase = baseRecord;
_contentMigration = migration;
SetCompletionRequirement(null);
_datDirectory = baseRecord.DatDirectory;
Status = "Review the required work. Nothing has started.";
OnPropertyChanged(nameof(DatDirectory));
OnPropertyChanged(nameof(IsContentUpdate));
OnPropertyChanged(nameof(Title));
OnPropertyChanged(nameof(Body));
OnPropertyChanged(nameof(StartActionText));
OnPropertyChanged(nameof(CompletedTitle));
OnPropertyChanged(nameof(CompletedBody));
Open();
}
public void NotifyCommandStates()
{
@ -277,6 +333,7 @@ public sealed class FirstRunInstallerViewModel : ObservableObject, IDisposable
{
IsCompleted = false;
IsOpen = false;
ClearContentUpdateMode();
}
private void Open()
@ -343,22 +400,33 @@ public sealed class FirstRunInstallerViewModel : ObservableObject, IDisposable
Error = null;
ProgressPercent = 0;
Phase = LauncherInstallPhase.ValidatingDatFiles;
Status = "Starting installation...";
Status = IsContentUpdate
? "Starting the approved world-data update..."
: "Starting installation...";
var progress = new CallbackProgress<LauncherInstallProgress>(value =>
_dispatcher.Post(() => ApplyProgress(value)));
try
{
LauncherInstallResult result = await _installer.InstallAsync(
DatDirectory,
threads,
progress,
cancellation.Token)
LauncherInstallResult result = await (IsContentUpdate
? _installer.ApplyContentUpdateAsync(
DatDirectory,
threads,
_contentMigration!,
progress,
cancellation.Token)
: _installer.InstallAsync(
DatDirectory,
threads,
progress,
cancellation.Token))
.ConfigureAwait(true);
_onInstalled(result.Record);
Phase = LauncherInstallPhase.Completed;
ProgressPercent = 100;
Status = "Client content installed and verified. Launch is enabled.";
Status = _completionRequirement is null
? "Client content installed and verified. Launch is enabled."
: "World data is ready. The matching game update is still required.";
// 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
@ -389,6 +457,44 @@ public sealed class FirstRunInstallerViewModel : ObservableObject, IDisposable
}
}
private string BuildContentUpdateBody()
{
ContentMigrationPlan migration = _contentMigration!;
string work = migration.Kind == ContentWorkKind.Overlay
? "a small filtered overlay"
: "a complete replacement pak";
string estimate = migration.Kind == ContentWorkKind.Overlay
? $"Affected filters: {migration.EffectiveDatIds.Count:N0} DAT id(s), "
+ $"{migration.EffectiveLandblocks.Count:N0} landblock(s)."
: _contentUpdateBase is { PreparedAssetSize: > 0 } record
? $"Free-space guidance: allow about "
+ $"{Math.Ceiling(record.PreparedAssetSize * 1.1 / (1024d * 1024d * 1024d)):N0} GiB."
: "Free-space guidance: allow room for one complete replacement pak.";
return $"This client needs recipe {migration.TargetRecipeVersion}: "
+ $"{migration.Reason}. acdream will build {work} from your installed "
+ "Asheron's Call DAT files. The existing package stays in place "
+ $"until the new one has finished and verified. {estimate} "
+ "No work begins until you confirm below.";
}
private void ClearContentUpdateMode()
{
if (_contentMigration is null)
{
return;
}
_contentMigration = null;
_contentUpdateBase = null;
SetCompletionRequirement(null);
OnPropertyChanged(nameof(IsContentUpdate));
OnPropertyChanged(nameof(Title));
OnPropertyChanged(nameof(Body));
OnPropertyChanged(nameof(StartActionText));
OnPropertyChanged(nameof(CompletedTitle));
OnPropertyChanged(nameof(CompletedBody));
}
private void ApplyProgress(LauncherInstallProgress progress)
{
if (_disposed)

View file

@ -27,6 +27,7 @@ public sealed class LauncherUpdateViewModel : ObservableObject, IDisposable
private readonly ILauncherUpdater _updater;
private readonly IUiDispatcher _dispatcher;
private readonly Action _onClientChanged;
private readonly Func<bool> _canOpen;
private readonly Func<bool> _canMutate;
private readonly Func<CancellationToken, Task<bool>>? _applyLauncherUpdateAsync;
private readonly Action? _requestShutdown;
@ -40,6 +41,13 @@ public sealed class LauncherUpdateViewModel : ObservableObject, IDisposable
private double _progressPercent;
private bool _isProgressIndeterminate;
/// <summary>
/// Raised after the one startup check has either produced an authoritative
/// result or failed. Content publication uses this edge to avoid pairing a
/// newly prepared pak with an unconfirmed active client.
/// </summary>
public event EventHandler? StartupCheckCompleted;
/// <param name="applyLauncherUpdateAsync">Applies an already-staged
/// launcher update against the running process, returning true when the
/// replacement helper started and this process must now exit. Null (tests,
@ -60,9 +68,7 @@ public sealed class LauncherUpdateViewModel : ObservableObject, IDisposable
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
_onClientChanged = onClientChanged
?? throw new ArgumentNullException(nameof(onClientChanged));
// Retained so the composition root's call shape is unchanged; there is
// no user-openable update panel any more, so nothing consults it.
_ = canOpen;
_canOpen = canOpen ?? (() => true);
_canMutate = canMutate ?? (() => true);
_applyLauncherUpdateAsync = applyLauncherUpdateAsync;
_requestShutdown = requestShutdown;
@ -174,6 +180,10 @@ public sealed class LauncherUpdateViewModel : ObservableObject, IDisposable
public bool IsLauncherUpdateAvailable => _check?.IsLauncherUpdateAvailable == true;
public bool IsStartupCheckComplete { get; private set; }
public bool StartupCheckSucceeded { get; private set; }
/// <summary>The single affirmative action. See <see cref="UpdateAsync"/>.</summary>
public AsyncRelayCommand UpdateCommand { get; }
@ -195,16 +205,24 @@ public sealed class LauncherUpdateViewModel : ObservableObject, IDisposable
using var cancellation = new CancellationTokenSource();
_cancellation = cancellation;
IsStartupCheckComplete = false;
StartupCheckSucceeded = false;
IsBusy = true;
try
{
// The composition root no longer verifies the active client before
// the launcher window exists. Recover/verify it here, immediately
// before the one startup feed check, while the UI is responsive.
_ = await _updater.InitializeAsync(cancellation.Token)
.ConfigureAwait(true);
_check = await _updater.CheckAsync(cancellation.Token).ConfigureAwait(true);
StartupCheckSucceeded = true;
OnPropertyChanged(nameof(Body));
OnPropertyChanged(nameof(IsClientUpdateAvailable));
OnPropertyChanged(nameof(IsLauncherUpdateAvailable));
if (HasSomethingToUpdate)
{
IsOpen = true;
TryOpenPendingUpdate();
}
}
catch
@ -221,6 +239,10 @@ public sealed class LauncherUpdateViewModel : ObservableObject, IDisposable
}
IsBusy = false;
IsStartupCheckComplete = true;
OnPropertyChanged(nameof(IsStartupCheckComplete));
OnPropertyChanged(nameof(StartupCheckSucceeded));
StartupCheckCompleted?.Invoke(this, EventArgs.Empty);
}
}
@ -232,6 +254,16 @@ public sealed class LauncherUpdateViewModel : ObservableObject, IDisposable
}
}
/// <summary>Opens a previously discovered update once another startup
/// question (notably required world-data work) has finished.</summary>
public void TryOpenPendingUpdate()
{
if (!_disposed && HasSomethingToUpdate && _canOpen())
{
IsOpen = true;
}
}
public void NotifyCommandStates()
{
UpdateCommand.NotifyCanExecuteChanged();

View file

@ -27,7 +27,15 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
private LauncherTreeNodeViewModel? _selectedNode;
private CancellationTokenSource? _operationCancellation;
private bool _isBusy;
private bool _isInstallationChecking = true;
private bool _isClientCompatibilityCheckBlocking;
private bool _isContentUpdateRequired;
private bool _disposed;
private Task? _startupInitialization;
private bool _openUpdateAfterContentCompletion;
private bool _isClientCompatibilityPending;
private LauncherInstallRecord? _pendingInstalledContent;
private readonly CancellationTokenSource _startupCancellation = new();
private string? _lastError;
private string _operationStatus = "Ready";
private LaunchMode _characterLaunchMode;
@ -66,6 +74,7 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
EditorDialog.PropertyChanged += OnModalPropertyChanged;
FirstRunWizardShell.PropertyChanged += OnModalPropertyChanged;
UpdatePrompt.PropertyChanged += OnModalPropertyChanged;
UpdatePrompt.StartupCheckCompleted += OnStartupUpdateCheckCompleted;
AddServerCommand = new RelayCommand(OpenAddServerDialog, () => CanInteract);
AddAccountCommand = new RelayCommand(OpenAddAccountDialog, CanAddAccount);
@ -195,7 +204,23 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
set => SetProperty(ref _characterLoginCommandsText, value);
}
public bool IsFirstRunRequired => _snapshot is { IsInstallationReady: false };
public bool IsInstallationChecking => _isInstallationChecking;
public bool IsFirstRunRequired =>
!IsInstallationChecking
&& (_isContentUpdateRequired
|| _snapshot is { IsInstallationReady: false });
public bool ShowInstallationBanner =>
IsInstallationChecking || IsFirstRunRequired;
public string InstallationBannerTitle => IsInstallationChecking
? "Checking installation"
: _isClientCompatibilityPending
? "Game update required"
: _isContentUpdateRequired
? "World data update required"
: "Client setup required";
public string InstallationStatus => _snapshot?.InstallationStatus
?? "Installation state is loading.";
@ -211,6 +236,7 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
public bool CanLaunchAccountGuiSelect =>
CanInteract
&& !_isClientCompatibilityCheckBlocking
&& IsAccountSelected
&& TryGetSelectedAccount(out string server, out string account)
&& _orchestrator.GetAccountLaunchCapability(
@ -291,7 +317,29 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
}
RefreshFromCore();
_ = UpdatePrompt.StartupCheckAsync();
}
/// <summary>
/// Starts only after the real launcher window has raised Opened. The exact
/// order is intentional: content discovery first, versioned-client
/// recovery second (inside StartupCheckAsync), network feed check last.
/// This prevents pre-window hashing and competing startup modals.
/// </summary>
public Task StartBackgroundInitializationAsync()
{
if (_startupInitialization is not null)
{
return _startupInitialization;
}
_isClientCompatibilityCheckBlocking = true;
OnPropertyChanged(nameof(CanLaunchGui));
OnPropertyChanged(nameof(CanLaunchHeadless));
OnPropertyChanged(nameof(CanLaunchAccountGuiSelect));
NotifyCommandStates();
_startupInitialization = InitializeInstalledContentAndUpdatesAsync(
_startupCancellation.Token);
return _startupInitialization;
}
public void PollStatus()
@ -319,6 +367,7 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
}
_disposed = true;
_startupCancellation.Cancel();
_operationCancellation?.Cancel();
_operationCancellation?.Dispose();
_operationCancellation = null;
@ -326,8 +375,91 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
EditorDialog.PropertyChanged -= OnModalPropertyChanged;
FirstRunWizardShell.PropertyChanged -= OnModalPropertyChanged;
UpdatePrompt.PropertyChanged -= OnModalPropertyChanged;
UpdatePrompt.StartupCheckCompleted -= OnStartupUpdateCheckCompleted;
FirstRunWizardShell.Dispose();
UpdatePrompt.Dispose();
_startupCancellation.Dispose();
}
private async Task InitializeInstalledContentAndUpdatesAsync(
CancellationToken cancellationToken)
{
OperationStatus = "Checking installed game content…";
try
{
InstallRecordVerification verification = await _installer
.LoadExistingWithProgressAsync(
cancellationToken,
progress: new Progress<string>(status =>
OperationStatus = status))
.ConfigureAwait(true);
if (_disposed)
{
return;
}
_isContentUpdateRequired = verification.RequiresContentUpdate;
if (verification.IsVerified
&& verification.Record is
{ RequiresClientCompatibilityConfirmation: true } pendingRecord)
{
_pendingInstalledContent = pendingRecord;
_isClientCompatibilityPending = true;
_orchestrator.SetInstallationState(null, verification.Status);
}
else
{
_orchestrator.SetInstallationState(
verification.IsVerified || verification.RequiresContentUpdate
? verification.Record
: null,
verification.Status);
}
OperationStatus = verification.Status;
if (verification.RequiresContentUpdate
&& verification.Record is not null
&& verification.RequiredContentWork is not null)
{
FirstRunWizardShell.PrepareContentUpdate(
verification.Record,
verification.RequiredContentWork);
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return;
}
catch (Exception ex)
{
if (_disposed)
{
return;
}
string status = "Client content verification failed: "
+ SafeDisplayError(ex, secret: null);
_orchestrator.SetInstallationState(null, status);
OperationStatus = status;
LastError = status;
}
if (!_disposed && !cancellationToken.IsCancellationRequested)
{
OperationStatus = "Checking for game updates…";
await UpdatePrompt.StartupCheckAsync().ConfigureAwait(true);
}
if (!_disposed)
{
_isClientCompatibilityCheckBlocking = false;
_isInstallationChecking = false;
OnPropertyChanged(nameof(IsInstallationChecking));
OnPropertyChanged(nameof(IsFirstRunRequired));
OnPropertyChanged(nameof(ShowInstallationBanner));
OnPropertyChanged(nameof(InstallationBannerTitle));
OnPropertyChanged(nameof(InstallationStatus));
RefreshFromCore();
}
}
private void OnOrchestratorStateChanged(object? sender, EventArgs e) =>
@ -356,6 +488,14 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
OnPropertyChanged(nameof(ShowGuiLaunchDisabledReason));
OnPropertyChanged(nameof(ShowHeadlessLaunchDisabledReason));
NotifyCommandStates();
if (ReferenceEquals(sender, FirstRunWizardShell)
&& e.PropertyName == nameof(FirstRunInstallerViewModel.IsOpen)
&& !FirstRunWizardShell.IsOpen
&& _openUpdateAfterContentCompletion)
{
_openUpdateAfterContentCompletion = false;
UpdatePrompt.TryOpenPendingUpdate();
}
}
public void CloseActiveModal()
@ -403,6 +543,7 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
SetSelectedNode(restored, preserveDraft);
OnPropertyChanged(nameof(IsFirstRunRequired));
OnPropertyChanged(nameof(ShowInstallationBanner));
OnPropertyChanged(nameof(InstallationStatus));
OnPropertyChanged(nameof(ShowLinuxGraphicalNotice));
OnPropertyChanged(nameof(LinuxGraphicalNotice));
@ -832,6 +973,7 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
private bool CanLaunch(LaunchMode mode) =>
CanInteract
&& !_isClientCompatibilityCheckBlocking
&& IsCharacterSelected
&& TryGetSelectedAccount(out string server, out string account)
&& _orchestrator.GetAccountLaunchCapability(server, account, mode).IsAvailable;
@ -967,11 +1109,21 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
try
{
InstallRecordVerification verification = await _installer
.LoadExistingAsync(cancellation.Token, forceFullVerification: true)
.LoadExistingWithProgressAsync(
cancellation.Token,
forceFullVerification: true,
progress: new Progress<string>(status =>
OperationStatus = status))
.ConfigureAwait(true);
_orchestrator.SetInstallRecord(verification.Record);
_isContentUpdateRequired = verification.RequiresContentUpdate;
_orchestrator.SetInstallationState(
verification.IsVerified || verification.RequiresContentUpdate
? verification.Record
: null,
verification.Status);
OperationStatus = verification.Status;
if (!verification.IsVerified)
if (!verification.IsVerified
&& !verification.RequiresContentUpdate)
{
LastError = verification.Status;
}
@ -995,19 +1147,93 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
private void OnInstallCompleted(LauncherInstallRecord record)
{
_orchestrator.SetInstallRecord(record);
OperationStatus = "Client content installed and verified.";
bool attemptImmediateCompatibilityConfirmation = false;
_isContentUpdateRequired = false;
_openUpdateAfterContentCompletion = true;
if (FirstRunWizardShell.IsContentUpdate
&& (record.RequiresClientCompatibilityConfirmation
|| !UpdatePrompt.IsStartupCheckComplete
|| !UpdatePrompt.StartupCheckSucceeded
|| UpdatePrompt.IsClientUpdateAvailable))
{
_pendingInstalledContent = record;
_isClientCompatibilityPending = true;
const string requirement = "acdream built and verified the world data. "
+ "Install the matching game update next; Play stays disabled until it finishes.";
FirstRunWizardShell.SetCompletionRequirement(requirement);
_orchestrator.SetInstallationState(
null,
"World data is ready; install the matching game update before playing.");
OperationStatus = "World data is ready; matching game update required.";
attemptImmediateCompatibilityConfirmation = true;
}
else
{
_orchestrator.SetInstallRecord(record);
OperationStatus = "Client content installed and verified.";
}
LastError = null;
RefreshFromCore();
if (attemptImmediateCompatibilityConfirmation)
{
PublishPendingContentIfCompatible(clientWasInstalled: false);
}
}
private void OnClientVersionChanged()
{
PublishPendingContentIfCompatible(clientWasInstalled: true);
OperationStatus = "Versioned client activation changed.";
LastError = null;
RefreshFromCore();
}
private void OnStartupUpdateCheckCompleted(object? sender, EventArgs e) =>
PublishPendingContentIfCompatible(clientWasInstalled: false);
private void PublishPendingContentIfCompatible(bool clientWasInstalled)
{
if (_pendingInstalledContent is null)
{
return;
}
bool compatible = clientWasInstalled
|| (UpdatePrompt.StartupCheckSucceeded
&& !UpdatePrompt.IsClientUpdateAvailable);
if (!compatible)
{
return;
}
LauncherInstallRecord pendingRecord = _pendingInstalledContent;
try
{
_installer.ConfirmClientCompatibility();
}
catch (Exception ex)
{
string status = "The matching client is ready, but the content gate "
+ "could not be cleared: " + SafeDisplayError(ex, secret: null);
_orchestrator.SetInstallationState(null, status);
OperationStatus = status;
LastError = status;
return;
}
LauncherInstallRecord record = pendingRecord with
{
RequiresClientCompatibilityConfirmation = false,
};
_pendingInstalledContent = null;
_isClientCompatibilityPending = false;
FirstRunWizardShell.SetCompletionRequirement(null);
_orchestrator.SetInstallRecord(record);
OperationStatus = "Client and world data are installed and verified.";
OnPropertyChanged(nameof(InstallationBannerTitle));
}
private void NotifyCommandStates()
{
AddServerCommand.NotifyCanExecuteChanged();