feat(launcher): stabilize prepared content updates
This commit is contained in:
parent
f160f3fee1
commit
af9327a17b
42 changed files with 3706 additions and 147 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue