LU5. The per-character panel offered "GUI — enter world", "GUI — character select" and "Headless" as three equal-looking buttons, above a "Default launch mode" combo. It now leads with one primary **Play** that enters the world as the selected character, with Character select and Headless kept as deliberate secondary choices. The combo is gone. It was never consulted by anything: every launch button passes its own mode and LauncherOrchestrator.LaunchAsync overrides the profile's stored mode with it (CloneCharacter(character, mode)). A setting that changes nothing is worse than no setting, and this one made the three buttons look like they obeyed it. The stored value is untouched. Worth recording for whoever reads the LU5 acceptance: the launcher-side plumbing was already correct end to end — orchestrator, selector composition, and the client's own "skip character select when a selector is present" gate. What actually made launching a character fail was #420, a client crash on the character-select screen, fixed separately. Every play session in the user's cache had no character selector, which is consistent with them only ever reaching the select-screen paths. LU6. Rows read `server / account / character`, then the launch mode (Gui/GuiSelect/Headless/Probe), then the raw LauncherActivityState enum name, then a status string. The launch mode is launcher bookkeeping — it says how the process was started, which tells the person watching nothing and is meaningless once the client is up. Rows now show the account, the character (or "Character select" while one is still being chosen, "Character refresh" for a roster probe), and one plain word derived from the host's own status stream: Starting -> Character select -> In game -> Stopping -> Stopped / Failed. A Play launch and a character-select launch both read "In game" once the player is actually in it. The orchestrator now KEEPS the identity from the host's enteredWorld event instead of only formatting it into a status sentence, so a character-select session stops being anonymous the moment someone enters the world. Tests: LauncherSessionRowViewModelTests (16 — every state's wording, in-game independent of launch mode, the character-select placeholder and its replacement, probe labelling, stop gating). Full solution 14,370 passed, 0 failed, 0 skipped under the release-gate filter. Campaign LU slices LU5 and LU6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
123 lines
4.4 KiB
C#
123 lines
4.4 KiB
C#
using AcDream.Launcher.Core.Launching;
|
|
using AcDream.Launcher.Core.Profiles;
|
|
using AcDream.Launcher.Core.Orchestration;
|
|
using AcDream.Launcher.ViewModels;
|
|
|
|
namespace AcDream.Launcher.Tests;
|
|
|
|
/// <summary>
|
|
/// LU6. A sessions row answers "who is playing, and are they in yet?" — not
|
|
/// "which launch mode started this process".
|
|
/// </summary>
|
|
public sealed class LauncherSessionRowViewModelTests
|
|
{
|
|
[Theory]
|
|
[InlineData(LauncherActivityState.Starting, "Starting")]
|
|
[InlineData(LauncherActivityState.Running, "Starting")]
|
|
[InlineData(LauncherActivityState.Connected, "Character select")]
|
|
[InlineData(LauncherActivityState.InWorld, "In game")]
|
|
[InlineData(LauncherActivityState.Disconnected, "Stopping")]
|
|
[InlineData(LauncherActivityState.Stopping, "Stopping")]
|
|
[InlineData(LauncherActivityState.Exited, "Stopped")]
|
|
[InlineData(LauncherActivityState.Cancelled, "Stopped")]
|
|
[InlineData(LauncherActivityState.Failed, "Failed")]
|
|
public void StateReadsAsPlainEnglish(LauncherActivityState state, string expected)
|
|
{
|
|
var row = new LauncherSessionRowViewModel(
|
|
Snapshot(state: state),
|
|
_ => Task.CompletedTask);
|
|
|
|
Assert.Equal(expected, row.State);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The same wording regardless of how the session was started: a Play
|
|
/// launch and a character-select launch both read "In game" once the
|
|
/// player is actually in the world, because the state comes from the
|
|
/// host's own status stream rather than from the launch mode.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(LaunchMode.Gui)]
|
|
[InlineData(LaunchMode.GuiSelect)]
|
|
[InlineData(LaunchMode.Headless)]
|
|
public void InGameDoesNotDependOnHowTheSessionWasLaunched(LaunchMode mode)
|
|
{
|
|
var row = new LauncherSessionRowViewModel(
|
|
Snapshot(state: LauncherActivityState.InWorld, mode: mode),
|
|
_ => Task.CompletedTask);
|
|
|
|
Assert.Equal("In game", row.State);
|
|
}
|
|
|
|
[Fact]
|
|
public void ACharacterSelectLaunchSaysSoUntilACharacterIsKnown()
|
|
{
|
|
var row = new LauncherSessionRowViewModel(
|
|
Snapshot(character: null, mode: LaunchMode.GuiSelect),
|
|
_ => Task.CompletedTask);
|
|
|
|
Assert.Equal("Character select", row.Character);
|
|
Assert.Equal("testaccount", row.Account);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Once the host reports who entered the world, the orchestrator fills the
|
|
/// name in — so a character-select session stops being anonymous.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AnEnteredCharacterReplacesTheCharacterSelectPlaceholder()
|
|
{
|
|
var row = new LauncherSessionRowViewModel(
|
|
Snapshot(
|
|
character: "+alex",
|
|
mode: LaunchMode.GuiSelect,
|
|
state: LauncherActivityState.InWorld),
|
|
_ => Task.CompletedTask);
|
|
|
|
Assert.Equal("+alex", row.Character);
|
|
Assert.Equal("In game", row.State);
|
|
}
|
|
|
|
[Fact]
|
|
public void AProbeIsLabelledAsACharacterRefresh()
|
|
{
|
|
var row = new LauncherSessionRowViewModel(
|
|
Snapshot(kind: LauncherActivityKind.Probe, character: null, mode: null),
|
|
_ => Task.CompletedTask);
|
|
|
|
Assert.Equal("Character refresh", row.Character);
|
|
Assert.Equal("Reading characters", row.State);
|
|
}
|
|
|
|
[Fact]
|
|
public void StopIsOfferedOnlyWhileTheSessionIsActive()
|
|
{
|
|
var running = new LauncherSessionRowViewModel(
|
|
Snapshot(state: LauncherActivityState.InWorld),
|
|
_ => Task.CompletedTask);
|
|
var finished = new LauncherSessionRowViewModel(
|
|
Snapshot(state: LauncherActivityState.Exited),
|
|
_ => Task.CompletedTask);
|
|
|
|
Assert.True(running.StopCommand.CanExecute(null));
|
|
Assert.False(finished.StopCommand.CanExecute(null));
|
|
}
|
|
|
|
private static LauncherSessionSnapshot Snapshot(
|
|
LauncherActivityState state = LauncherActivityState.Connected,
|
|
LauncherActivityKind kind = LauncherActivityKind.Play,
|
|
string? character = "+Acdream",
|
|
LaunchMode? mode = LaunchMode.Gui,
|
|
string status = "Fixture status.") => new(
|
|
"session-1",
|
|
kind,
|
|
"Local ACE",
|
|
"testaccount",
|
|
character,
|
|
mode,
|
|
state,
|
|
status,
|
|
ExitCode: state == LauncherActivityState.Exited ? 0 : null,
|
|
Error: null,
|
|
CreatedAt: DateTimeOffset.UnixEpoch);
|
|
}
|