diff --git a/src/AcDream.Launcher/MainWindow.axaml b/src/AcDream.Launcher/MainWindow.axaml
index ab945311..da056eca 100644
--- a/src/AcDream.Launcher/MainWindow.axaml
+++ b/src/AcDream.Launcher/MainWindow.axaml
@@ -135,21 +135,23 @@
IsVisible="{Binding IsAccountSelected}">
-
+
-
-
-
@@ -157,6 +159,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -189,36 +221,6 @@
Command="{Binding SaveCharacterSettingsCommand}" />
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/AcDream.Launcher/ViewModels/LauncherWindowViewModel.cs b/src/AcDream.Launcher/ViewModels/LauncherWindowViewModel.cs
index 2a9ec928..a793e152 100644
--- a/src/AcDream.Launcher/ViewModels/LauncherWindowViewModel.cs
+++ b/src/AcDream.Launcher/ViewModels/LauncherWindowViewModel.cs
@@ -65,9 +65,6 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
SaveCharacterSettingsCommand = new RelayCommand(
SaveCharacterSettings,
() => IsCharacterSelected && CanInteract);
- RefreshCharactersCommand = new AsyncRelayCommand(
- RefreshCharactersAsync,
- () => CanProbe);
LaunchGuiCommand = new AsyncRelayCommand(
() => LaunchSelectedAsync(LaunchMode.Gui),
() => CanLaunchGui);
@@ -114,7 +111,6 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
{
if (SetProperty(ref _isBusy, value))
{
- OnPropertyChanged(nameof(CanProbe));
OnPropertyChanged(nameof(CanLaunchGui));
OnPropertyChanged(nameof(CanLaunchHeadless));
OnPropertyChanged(nameof(CanLaunchAccountGuiSelect));
@@ -199,25 +195,6 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
public string LinuxGraphicalNotice =>
_snapshot?.Platform.GraphicalLaunchDisabledReason ?? string.Empty;
- public bool CanProbe =>
- CanInteract
- && TryGetSelectedAccount(out string server, out string account)
- && _orchestrator.GetProbeCapability(server, account).IsAvailable;
-
- public string ProbeDisabledReason
- {
- get
- {
- if (!TryGetSelectedAccount(out string server, out string account))
- {
- return "Select an account or one of its characters.";
- }
-
- return _orchestrator.GetProbeCapability(server, account).Reason
- ?? "Character refresh is available.";
- }
- }
-
public bool CanLaunchGui => CanLaunch(LaunchMode.Gui);
public bool CanLaunchHeadless => CanLaunch(LaunchMode.Headless);
@@ -277,8 +254,6 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
public RelayCommand SaveCharacterSettingsCommand { get; }
- public AsyncRelayCommand RefreshCharactersCommand { get; }
-
public AsyncRelayCommand LaunchGuiCommand { get; }
public AsyncRelayCommand LaunchAccountGuiSelectCommand { get; }
@@ -365,7 +340,6 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
}
OnPropertyChanged(nameof(IsModalOpen));
- OnPropertyChanged(nameof(CanProbe));
OnPropertyChanged(nameof(CanLaunchGui));
OnPropertyChanged(nameof(CanLaunchHeadless));
OnPropertyChanged(nameof(CanLaunchAccountGuiSelect));
@@ -422,8 +396,6 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
OnPropertyChanged(nameof(InstallationStatus));
OnPropertyChanged(nameof(ShowLinuxGraphicalNotice));
OnPropertyChanged(nameof(LinuxGraphicalNotice));
- OnPropertyChanged(nameof(CanProbe));
- OnPropertyChanged(nameof(ProbeDisabledReason));
OnPropertyChanged(nameof(CanLaunchGui));
OnPropertyChanged(nameof(CanLaunchHeadless));
OnPropertyChanged(nameof(CanLaunchAccountGuiSelect));
@@ -461,8 +433,6 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
LoadCharacterDraft();
}
- OnPropertyChanged(nameof(CanProbe));
- OnPropertyChanged(nameof(ProbeDisabledReason));
OnPropertyChanged(nameof(CanLaunchGui));
OnPropertyChanged(nameof(CanLaunchHeadless));
OnPropertyChanged(nameof(CanLaunchAccountGuiSelect));
@@ -728,19 +698,6 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
}
}
- private Task RefreshCharactersAsync()
- {
- if (!TryGetSelectedAccount(out string serverName, out string accountName))
- {
- return Task.CompletedTask;
- }
-
- return RunOperationAsync(
- token => _orchestrator.ProbeAsync(serverName, accountName, token),
- $"Refreshing characters for {accountName}…",
- "Character refresh started. The roster will update from host status.");
- }
-
private Task LaunchSelectedAsync(LaunchMode mode)
{
LauncherCharacterSnapshot? character = GetSelectedCharacterSnapshot();
@@ -1049,7 +1006,6 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
EditSelectedCommand.NotifyCanExecuteChanged();
RemoveSelectedCommand.NotifyCanExecuteChanged();
SaveCharacterSettingsCommand.NotifyCanExecuteChanged();
- RefreshCharactersCommand.NotifyCanExecuteChanged();
LaunchGuiCommand.NotifyCanExecuteChanged();
LaunchAccountGuiSelectCommand.NotifyCanExecuteChanged();
LaunchHeadlessCommand.NotifyCanExecuteChanged();
diff --git a/tests/AcDream.Launcher.Core.Tests/Orchestration/LauncherOrchestratorTests.cs b/tests/AcDream.Launcher.Core.Tests/Orchestration/LauncherOrchestratorTests.cs
index 3c54a23f..7df28994 100644
--- a/tests/AcDream.Launcher.Core.Tests/Orchestration/LauncherOrchestratorTests.cs
+++ b/tests/AcDream.Launcher.Core.Tests/Orchestration/LauncherOrchestratorTests.cs
@@ -326,6 +326,61 @@ public sealed class LauncherOrchestratorTests : IDisposable
Assert.Single(orchestrator.GetSnapshot().Sessions);
}
+ ///
+ /// LU8: the launcher no longer offers "Refresh characters", because opening
+ /// a SECOND connection to an account purely to read its roster is a login
+ /// as far as the server is concerned, and it disconnected the session the
+ /// user was already playing. This is what replaces it — and it is not new
+ /// code, it is the path an ORDINARY login already took: the host reports
+ /// the roster in its own status stream and the orchestrator folds it into
+ /// the profiles. Logging in IS the refresh.
+ ///
+ [Fact]
+ public async Task AnOrdinaryLoginFoldsTheReportedRosterIntoTheStore()
+ {
+ var statusSources = new QueueStatusSourceFactory();
+ using LauncherOrchestrator orchestrator = CreateOrchestrator(
+ includeCharacter: false,
+ statusSourceFactory: statusSources);
+
+ // No cached character, so this is the account-level character-select
+ // launch a new user makes before any character is known.
+ _ = await orchestrator.LaunchAsync(
+ "Local ACE",
+ "testaccount",
+ characterName: null,
+ LaunchMode.GuiSelect);
+
+ QueueStatusSource source = Assert.Single(statusSources.Created);
+ source.Enqueue(new CharacterListStatusEvent
+ {
+ V = 1,
+ E = "characterList",
+ T = DateTimeOffset.UtcNow,
+ SessionId = "s1",
+ AccountName = "testaccount",
+ SlotCount = 6,
+ Characters =
+ [
+ new StatusCharacterEntry(0x5000000Au, "+Acdream", 0),
+ new StatusCharacterEntry(0x5000000Bu, "+Second", 0),
+ ],
+ });
+ orchestrator.PollStatus();
+
+ LauncherAccountSnapshot account = Assert.Single(
+ Assert.Single(orchestrator.GetSnapshot().Servers).Accounts);
+ Assert.Equal(2, account.Characters.Count);
+
+ // Persisted, so the tree still shows them on the next launcher start.
+ var reloaded = new LauncherProfileStore(
+ Path.Combine(_paths.ConfigDirectory, "launcher-profiles.json"));
+ Assert.True(reloaded.Load());
+ Assert.Equal(
+ 2,
+ reloaded.Document.Servers.Single().Accounts.Single().Characters.Count);
+ }
+
[Fact]
public async Task ProbeUsesTheProbeShapeAndFoldsTheReportedRosterIntoTheStore()
{
diff --git a/tests/AcDream.Launcher.Tests/LauncherWindowViewModelTests.cs b/tests/AcDream.Launcher.Tests/LauncherWindowViewModelTests.cs
index 2399e4d0..04f709af 100644
--- a/tests/AcDream.Launcher.Tests/LauncherWindowViewModelTests.cs
+++ b/tests/AcDream.Launcher.Tests/LauncherWindowViewModelTests.cs
@@ -189,25 +189,12 @@ public sealed class LauncherWindowViewModelTests
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);
- }
+ // LU8: the probe-driven "Refresh characters" action is gone. It opened a
+ // SECOND connection to an account purely to read the roster, which the
+ // server treats as a new login and which disconnected a session the user
+ // was already playing. Its test goes with it rather than being skipped;
+ // the roster path that replaced it is covered by
+ // LauncherOrchestrator's roster merge on an ordinary login.
[Fact]
public void LinuxKeepsLauncherAndHeadlessAvailableButExplainsDisabledGuiModes()