acdream/tests/AcDream.Launcher.Tests/LauncherWindowViewModelTests.cs

766 lines
31 KiB
C#

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;
public sealed class LauncherWindowViewModelTests
{
[Fact]
public void InitializeProjectsHierarchySessionsAndFutureWorkflowShells()
{
using var orchestrator = new FakeLauncherOrchestrator();
using var viewModel = new LauncherWindowViewModel(
orchestrator,
new ImmediateUiDispatcher());
viewModel.Initialize();
Assert.True(orchestrator.LoadCalled);
LauncherTreeNodeViewModel server = Assert.Single(viewModel.Servers);
Assert.Equal(LauncherTreeNodeKind.Server, server.Kind);
LauncherTreeNodeViewModel account = Assert.Single(server.Children);
Assert.Equal(LauncherTreeNodeKind.Account, account.Kind);
LauncherTreeNodeViewModel character = Assert.Single(account.Children);
Assert.Equal("+Acdream", character.DisplayName);
Assert.Same(server, viewModel.SelectedNode);
LauncherSessionRowViewModel session = Assert.Single(viewModel.Sessions);
Assert.Equal("Gui", session.Mode);
Assert.Equal("Connected", session.State);
Assert.True(session.IsActive);
Assert.True(viewModel.IsFirstRunRequired);
Assert.Contains("SHA-256", viewModel.FirstRunWizardShell.Body, StringComparison.Ordinal);
Assert.Contains("pinned eriknihlen/acdream", viewModel.UpdatePrompt.Body, StringComparison.Ordinal);
viewModel.FirstRunWizardShell.OpenCommand.Execute(null);
Assert.True(viewModel.FirstRunWizardShell.IsOpen);
viewModel.FirstRunWizardShell.CloseCommand.Execute(null);
Assert.False(viewModel.FirstRunWizardShell.IsOpen);
}
[Fact]
public void ProfileCommandsExposeServerAccountCharacterCrudDialogsAndClearPasswords()
{
using var orchestrator = new FakeLauncherOrchestrator();
using var viewModel = CreateInitialized(orchestrator);
viewModel.AddServerCommand.Execute(null);
Assert.Equal(ProfileEditorKind.AddServer, viewModel.EditorDialog.Kind);
viewModel.EditorDialog.Name = "Remote ACE";
viewModel.EditorDialog.Host = "ace.example.test";
viewModel.EditorDialog.Port = "9001";
viewModel.EditorDialog.SubmitCommand.Execute(null);
Assert.Equal(("Remote ACE", "ace.example.test", 9001), orchestrator.AddedServer);
SelectServer(viewModel);
viewModel.AddAccountCommand.Execute(null);
Assert.Equal(ProfileEditorKind.AddAccount, viewModel.EditorDialog.Kind);
viewModel.EditorDialog.Name = "second-account";
viewModel.EditorDialog.Password = "one-use-secret";
viewModel.EditorDialog.SubmitCommand.Execute(null);
Assert.Equal(
("Local ACE", "second-account", "one-use-secret"),
orchestrator.AddedAccount);
Assert.Equal(string.Empty, viewModel.EditorDialog.Password);
SelectAccount(viewModel);
viewModel.AddCharacterCommand.Execute(null);
Assert.Equal(ProfileEditorKind.AddCharacter, viewModel.EditorDialog.Kind);
viewModel.EditorDialog.Name = "+Second";
viewModel.EditorDialog.CharacterId = "0x5000000B";
viewModel.EditorDialog.SubmitCommand.Execute(null);
Assert.Equal(
("Local ACE", "testaccount", "+Second", "0x5000000B"),
orchestrator.AddedCharacter);
SelectCharacter(viewModel);
viewModel.EditSelectedCommand.Execute(null);
Assert.Equal(ProfileEditorKind.EditCharacter, viewModel.EditorDialog.Kind);
viewModel.EditorDialog.Name = "+Renamed";
viewModel.EditorDialog.CharacterId = "0x5000000C";
viewModel.EditorDialog.SubmitCommand.Execute(null);
Assert.Equal(
("Local ACE", "testaccount", "+Acdream", "+Renamed", "0x5000000C"),
orchestrator.EditedCharacter);
SelectCharacter(viewModel);
viewModel.RemoveSelectedCommand.Execute(null);
Assert.Equal(ProfileEditorKind.Remove, viewModel.EditorDialog.Kind);
viewModel.EditorDialog.SubmitCommand.Execute(null);
Assert.Equal(
("Local ACE", "testaccount", "+Acdream"),
orchestrator.RemovedCharacter);
}
[Fact]
public void ServerAndAccountEditRemoveDialogsRouteEveryMutationThroughCore()
{
using var orchestrator = new FakeLauncherOrchestrator();
using var viewModel = CreateInitialized(orchestrator);
SelectServer(viewModel);
viewModel.EditSelectedCommand.Execute(null);
Assert.Equal(ProfileEditorKind.EditServer, viewModel.EditorDialog.Kind);
Assert.Equal("127.0.0.1", viewModel.EditorDialog.Host);
viewModel.EditorDialog.Name = "Renamed ACE";
viewModel.EditorDialog.Host = "renamed.example.test";
viewModel.EditorDialog.Port = "9010";
viewModel.EditorDialog.SubmitCommand.Execute(null);
Assert.Equal(
("Local ACE", "Renamed ACE", "renamed.example.test", 9010),
orchestrator.EditedServer);
SelectAccount(viewModel);
viewModel.EditSelectedCommand.Execute(null);
Assert.Equal(ProfileEditorKind.EditAccount, viewModel.EditorDialog.Kind);
viewModel.EditorDialog.Name = "renamed-account";
viewModel.EditorDialog.Password = "replacement-secret";
viewModel.EditorDialog.SubmitCommand.Execute(null);
Assert.Equal(
("Local ACE", "testaccount", "renamed-account", "replacement-secret"),
orchestrator.EditedAccount);
Assert.Equal(string.Empty, viewModel.EditorDialog.Password);
SelectAccount(viewModel);
viewModel.RemoveSelectedCommand.Execute(null);
viewModel.EditorDialog.SubmitCommand.Execute(null);
Assert.Equal(("Local ACE", "testaccount"), orchestrator.RemovedAccount);
SelectServer(viewModel);
viewModel.RemoveSelectedCommand.Execute(null);
viewModel.EditorDialog.SubmitCommand.Execute(null);
Assert.Equal("Local ACE", orchestrator.RemovedServer);
}
[Fact]
public async Task CharacterSettingsAndLaunchActionsPreserveTheirTypedSemantics()
{
using var orchestrator = new FakeLauncherOrchestrator();
using var viewModel = CreateInitialized(orchestrator);
SelectCharacter(viewModel);
viewModel.CharacterLaunchMode = LaunchMode.Headless;
viewModel.CharacterPluginsText = "Plugin.One\nPlugin.Two\nPlugin.One";
viewModel.CharacterLoginCommandsText = " /tell someone, hi \n/vt start\n/tell someone, hi";
viewModel.SaveCharacterSettingsCommand.Execute(null);
Assert.NotNull(orchestrator.SettingsUpdate);
Assert.Equal(LaunchMode.Headless, orchestrator.SettingsUpdate.Value.Mode);
Assert.Equal(["Plugin.One", "Plugin.Two"], orchestrator.SettingsUpdate.Value.Plugins);
Assert.Equal(
["/tell someone, hi", "/vt start", "/tell someone, hi"],
orchestrator.SettingsUpdate.Value.Commands);
await viewModel.LaunchHeadlessCommand.ExecuteAsync();
Assert.Equal(
("Local ACE", "testaccount", "+Acdream", LaunchMode.Headless),
orchestrator.LaunchRequest);
Assert.Equal("Headless session started for +Acdream.", viewModel.OperationStatus);
}
[Fact]
public async Task AccountGuiSelectWorksWithoutAnyCachedCharacter()
{
using var orchestrator = new FakeLauncherOrchestrator
{
IncludeCharacter = false,
};
using var viewModel = CreateInitialized(orchestrator);
SelectAccount(viewModel);
Assert.Empty(viewModel.SelectedNode!.Children);
Assert.True(viewModel.CanLaunchAccountGuiSelect);
await viewModel.LaunchAccountGuiSelectCommand.ExecuteAsync();
Assert.Equal(
("Local ACE", "testaccount", (string?)null, LaunchMode.GuiSelect),
orchestrator.LaunchRequest);
Assert.Equal(
"Character-select session started for testaccount.",
viewModel.OperationStatus);
}
[Fact]
public async Task ProbeUsesTheSelectedAccountAndRunningAccountDisablesIt()
{
using var orchestrator = new FakeLauncherOrchestrator();
using var viewModel = CreateInitialized(orchestrator);
SelectAccount(viewModel);
Assert.True(viewModel.CanProbe);
await viewModel.RefreshCharactersCommand.ExecuteAsync();
Assert.Equal(("Local ACE", "testaccount"), orchestrator.ProbeRequest);
orchestrator.ProbeCapability = LauncherCapability.Unavailable(
"Stop the active session before refreshing this account.");
orchestrator.RaiseStateChanged();
SelectAccount(viewModel);
Assert.False(viewModel.CanProbe);
Assert.Contains("Stop", viewModel.ProbeDisabledReason, StringComparison.Ordinal);
}
[Fact]
public void LinuxKeepsLauncherAndHeadlessAvailableButExplainsDisabledGuiModes()
{
using var orchestrator = new FakeLauncherOrchestrator
{
Platform = LinuxPlatform(),
};
using var viewModel = CreateInitialized(orchestrator);
SelectCharacter(viewModel);
Assert.True(viewModel.ShowLinuxGraphicalNotice);
Assert.False(viewModel.CanLaunchGui);
Assert.False(viewModel.CanLaunchGuiSelect);
Assert.True(viewModel.CanLaunchHeadless);
Assert.Equal(
LauncherPlatformCapabilities.LinuxGraphicalLaunchDisabledReason,
viewModel.LinuxGraphicalNotice);
Assert.Contains("Slice L", viewModel.GuiLaunchDisabledReason, StringComparison.Ordinal);
Assert.Contains("parked at L1", viewModel.GuiLaunchDisabledReason, StringComparison.Ordinal);
}
[Fact]
public void MissingCoDeployedHostReasonIsVisibleForCharacterActions()
{
using var orchestrator = new FakeLauncherOrchestrator
{
AccountLaunchCapability = LauncherCapability.Unavailable(
"The co-deployed host is missing; reinstall or update the client."),
};
using var viewModel = CreateInitialized(orchestrator);
SelectCharacter(viewModel);
Assert.False(viewModel.CanLaunchGui);
Assert.False(viewModel.CanLaunchHeadless);
Assert.True(viewModel.ShowGuiLaunchDisabledReason);
Assert.True(viewModel.ShowHeadlessLaunchDisabledReason);
Assert.Contains("missing", viewModel.GuiLaunchDisabledReason, StringComparison.Ordinal);
Assert.Contains("missing", viewModel.HeadlessLaunchDisabledReason, StringComparison.Ordinal);
}
[Fact]
public async Task LaunchErrorAndCancellationBecomeSafeVisibleState()
{
using var orchestrator = new FakeLauncherOrchestrator
{
LaunchHandler = _ => throw new LauncherOperationException("spawn failed safely"),
};
using var viewModel = CreateInitialized(orchestrator);
SelectCharacter(viewModel);
await viewModel.LaunchGuiCommand.ExecuteAsync();
Assert.Equal("spawn failed safely", viewModel.LastError);
Assert.Equal("Operation failed.", viewModel.OperationStatus);
Assert.False(viewModel.IsBusy);
orchestrator.LaunchHandler = async cancellationToken =>
{
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
return FakeLauncherOrchestrator.CreateSession();
};
Task launch = viewModel.LaunchGuiCommand.ExecuteAsync();
Assert.True(viewModel.IsBusy);
Assert.True(viewModel.CancelOperationCommand.CanExecute(null));
viewModel.CancelOperationCommand.Execute(null);
await launch;
Assert.Equal("Operation cancelled.", viewModel.OperationStatus);
Assert.False(viewModel.HasError);
Assert.False(viewModel.IsBusy);
}
[Fact]
public void ProfileDialogRedactsARejectedCredentialAndClearsItOnClose()
{
var dialog = new ProfileEditorDialogViewModel();
dialog.Open(
ProfileEditorKind.AddAccount,
"Add account",
candidate => throw new InvalidOperationException(
$"rejected {candidate.Password}"));
dialog.Password = "do-not-display";
dialog.SubmitCommand.Execute(null);
Assert.True(dialog.IsOpen);
Assert.DoesNotContain("do-not-display", dialog.Error ?? string.Empty, StringComparison.Ordinal);
Assert.Contains("[redacted]", dialog.Error ?? string.Empty, StringComparison.Ordinal);
dialog.CancelCommand.Execute(null);
Assert.Equal(string.Empty, dialog.Password);
Assert.False(dialog.IsOpen);
}
[Fact]
public void ModalShellsBlockBackgroundCommandsAndAreMutuallyExclusive()
{
using var orchestrator = new FakeLauncherOrchestrator();
using var viewModel = CreateInitialized(orchestrator);
Assert.True(viewModel.AddServerCommand.CanExecute(null));
viewModel.FirstRunWizardShell.OpenCommand.Execute(null);
Assert.True(viewModel.IsModalOpen);
Assert.False(viewModel.AddServerCommand.CanExecute(null));
Assert.False(viewModel.UpdatePrompt.OpenCommand.CanExecute(null));
Assert.False(Assert.Single(viewModel.Sessions).StopCommand.CanExecute(null));
// ICommand.Execute cannot bypass the modal gate.
viewModel.AddServerCommand.Execute(null);
Assert.False(viewModel.EditorDialog.IsOpen);
Assert.Null(orchestrator.AddedServer);
viewModel.CloseActiveModal();
Assert.False(viewModel.IsModalOpen);
Assert.True(viewModel.AddServerCommand.CanExecute(null));
viewModel.AddServerCommand.Execute(null);
Assert.True(viewModel.EditorDialog.IsOpen);
Assert.False(viewModel.FirstRunWizardShell.OpenCommand.CanExecute(null));
Assert.False(viewModel.ClearFinishedSessionsCommand.CanExecute(null));
viewModel.CloseActiveModal();
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()
{
using var orchestrator = new FakeLauncherOrchestrator();
using var viewModel = CreateInitialized(orchestrator);
LauncherSessionRowViewModel session = Assert.Single(viewModel.Sessions);
await session.StopCommand.ExecuteAsync();
Assert.Equal("session-1", orchestrator.StoppedSessionId);
orchestrator.Session = FakeLauncherOrchestrator.CreateSession(
LauncherActivityState.Exited,
"Exited cleanly.");
orchestrator.RaiseStateChanged();
Assert.True(viewModel.ClearFinishedSessionsCommand.CanExecute(null));
viewModel.ClearFinishedSessionsCommand.Execute(null);
Assert.True(orchestrator.ClearCalled);
}
private static LauncherWindowViewModel CreateInitialized(
FakeLauncherOrchestrator orchestrator,
ILauncherInstaller? installer = null)
{
var viewModel = new LauncherWindowViewModel(
orchestrator,
new ImmediateUiDispatcher(),
installer);
viewModel.Initialize();
return viewModel;
}
private static void SelectServer(LauncherWindowViewModel viewModel) =>
viewModel.SelectedNode = Assert.Single(viewModel.Servers);
private static void SelectAccount(LauncherWindowViewModel viewModel) =>
viewModel.SelectedNode = Assert.Single(Assert.Single(viewModel.Servers).Children);
private static void SelectCharacter(LauncherWindowViewModel viewModel) =>
viewModel.SelectedNode = Assert.Single(
Assert.Single(Assert.Single(viewModel.Servers).Children).Children);
private static LauncherPlatformCapabilities LinuxPlatform() => new(
IsWindows: false,
IsLinux: true,
CanRunHeadless: true,
CanLaunchGraphicalClient: false,
PlatformName: "Linux",
GraphicalLaunchDisabledReason:
LauncherPlatformCapabilities.LinuxGraphicalLaunchDisabledReason);
private sealed class FakeLauncherOrchestrator : ILauncherOrchestrator
{
public event EventHandler? StateChanged;
public bool LoadCalled { get; private set; }
public bool ClearCalled { get; private set; }
public LauncherPlatformCapabilities Platform { get; init; } = new(
IsWindows: true,
IsLinux: false,
CanRunHeadless: true,
CanLaunchGraphicalClient: true,
PlatformName: "Windows",
GraphicalLaunchDisabledReason: null);
public LauncherCapability ProbeCapability { get; set; } = LauncherCapability.Available;
public LauncherCapability? AccountLaunchCapability { get; set; }
public bool IncludeCharacter { get; init; } = true;
public LauncherSessionSnapshot Session { get; set; } = CreateSession();
public Func<CancellationToken, Task<LauncherSessionSnapshot>>? LaunchHandler { get; set; }
public (string Name, string Host, int Port)? AddedServer { get; private set; }
public (string Name, string NewName, string NewHost, int NewPort)? EditedServer { get; private set; }
public string? RemovedServer { get; private set; }
public (string Server, string Account, string Password)? AddedAccount { get; private set; }
public (string Server, string Account, string NewAccount, string? Password)? EditedAccount { get; private set; }
public (string Server, string Account)? RemovedAccount { get; private set; }
public (string Server, string Account, string Character, string? Id)? AddedCharacter { get; private set; }
public (string Server, string Account, string Character, string NewName, string? Id)? EditedCharacter { get; private set; }
public (string Server, string Account, string Character)? RemovedCharacter { get; private set; }
public (LaunchMode Mode, IReadOnlyList<string> Plugins, IReadOnlyList<string> Commands)? SettingsUpdate { get; private set; }
public (string Server, string Account, string? Character, LaunchMode Mode)? LaunchRequest { get; private set; }
public (string Server, string Account)? ProbeRequest { get; private set; }
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: InstalledRecord is not null,
InstallationStatus: InstalledRecord is null
? "No installed client is configured."
: "Client content verified.");
public LauncherCapability GetLaunchCapability(LaunchMode mode) =>
Platform.ForLaunchMode(mode);
public LauncherCapability GetAccountLaunchCapability(
string serverName,
string accountName,
LaunchMode mode) =>
AccountLaunchCapability ?? GetLaunchCapability(mode);
public LauncherCapability GetProbeCapability(string serverName, string accountName) =>
ProbeCapability;
public void SetInstallRecord(LauncherInstallRecord? installRecord)
{
InstalledRecord = installRecord;
StateChanged?.Invoke(this, EventArgs.Empty);
}
public void AddServer(string name, string host, int port) =>
AddedServer = (name, host, port);
public void EditServer(string name, string newName, string newHost, int newPort) =>
EditedServer = (name, newName, newHost, newPort);
public void RemoveServer(string name) => RemovedServer = name;
public void AddAccount(string serverName, string accountName, string password) =>
AddedAccount = (serverName, accountName, password);
public void EditAccount(
string serverName,
string accountName,
string newAccountName,
string? newPassword) =>
EditedAccount = (serverName, accountName, newAccountName, newPassword);
public void RemoveAccount(string serverName, string accountName) =>
RemovedAccount = (serverName, accountName);
public void AddCharacter(
string serverName,
string accountName,
string characterName,
string? characterId) =>
AddedCharacter = (serverName, accountName, characterName, characterId);
public void EditCharacterIdentity(
string serverName,
string accountName,
string characterName,
string newCharacterName,
string? newCharacterId) =>
EditedCharacter = (
serverName,
accountName,
characterName,
newCharacterName,
newCharacterId);
public void UpdateCharacterSettings(
string serverName,
string accountName,
string characterName,
LaunchMode launchMode,
IReadOnlyList<string> plugins,
IReadOnlyList<string> loginCommands) =>
SettingsUpdate = (launchMode, plugins, loginCommands);
public void RemoveCharacter(
string serverName,
string accountName,
string characterName) =>
RemovedCharacter = (serverName, accountName, characterName);
public Task<LauncherSessionSnapshot> LaunchAsync(
string serverName,
string accountName,
string? characterName,
LaunchMode mode,
CancellationToken cancellationToken = default)
{
LaunchRequest = (serverName, accountName, characterName, mode);
return LaunchHandler?.Invoke(cancellationToken) ?? Task.FromResult(Session);
}
public Task<LauncherSessionSnapshot> ProbeAsync(
string serverName,
string accountName,
CancellationToken cancellationToken = default)
{
ProbeRequest = (serverName, accountName);
return Task.FromResult(Session with { Kind = LauncherActivityKind.Probe });
}
public Task StopSessionAsync(
string sessionId,
TimeSpan timeout,
CancellationToken cancellationToken = default)
{
StoppedSessionId = sessionId;
return Task.CompletedTask;
}
public void PollStatus()
{
}
public void ClearFinishedSessions() => ClearCalled = true;
public void Dispose()
{
}
public void RaiseStateChanged() => StateChanged?.Invoke(this, EventArgs.Empty);
public static LauncherSessionSnapshot CreateSession(
LauncherActivityState state = LauncherActivityState.Connected,
string status = "Connected.") => new(
"session-1",
LauncherActivityKind.Play,
"Local ACE",
"testaccount",
"+Acdream",
LaunchMode.Gui,
state,
status,
ExitCode: state == LauncherActivityState.Exited ? 0 : null,
Error: null,
CreatedAt: DateTimeOffset.UnixEpoch);
private LauncherServerSnapshot CreateServerSnapshot() => new(
"Local ACE",
"127.0.0.1",
9000,
[
new LauncherAccountSnapshot(
"Local ACE",
"testaccount",
IncludeCharacter
?
[
new LauncherCharacterSnapshot(
"Local ACE",
"testaccount",
"+Acdream",
"0x5000000A",
LaunchMode.GuiSelect,
["Existing.Plugin"],
["/tell someone, ready"],
HasRunningSession: true,
SessionStatus: "Connected."),
]
: [],
HasRunningActivity: true,
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));
}
}
}