feat(launcher): Campaign LA add Avalonia desktop shell

This commit is contained in:
Erik 2026-08-14 18:15:14 +02:00
parent 6c4cd2bbc6
commit d0a9c65d85
31 changed files with 4831 additions and 16 deletions

View file

@ -325,13 +325,59 @@ public sealed class LauncherProfileStore
server.Accounts.Remove(profile);
}
// --- Character settings (roster-driven add/remove; user-edited settings) ---
// --- Character CRUD / user-owned settings --------------------------
/// <summary>
/// Edits the user-owned settings of an existing character row. There
/// is no manual add/remove for characters — the roster (
/// <see cref="MergeRoster"/>) is the only source of new rows, per
/// spec §5/§6.
/// Adds a manually configured character row. Normal operation discovers
/// characters through <see cref="MergeRoster"/>, but LA4's full in-UI
/// CRUD contract also lets a user create a cached row before a successful
/// probe (for example, to launch by a known character name while a server
/// is temporarily unavailable). A later roster merge remains
/// authoritative for the id/name pair and preserves these user settings.
/// </summary>
public CharacterProfile AddCharacter(
string serverName,
string account,
string characterName,
string? id = null,
LaunchMode launchMode = LaunchMode.GuiSelect,
IReadOnlyList<string>? plugins = null,
IReadOnlyList<string>? loginCommands = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(characterName);
ServerProfile server = FindServerOrThrow(serverName);
AccountProfile profile = FindAccountOrThrow(server, account);
if (FindCharacter(profile, characterName) is not null)
{
throw new LauncherProfileException(
$"Character '{characterName}' already exists on account '{account}'.");
}
string? normalizedId = NormalizeCharacterId(id);
if (normalizedId is not null
&& profile.Characters.Any(character => CharacterIdsEqual(character.Id, normalizedId)))
{
throw new LauncherProfileException(
$"Character id '{normalizedId}' already exists on account '{account}'.");
}
var character = new CharacterProfile
{
Name = characterName,
Id = normalizedId,
LaunchMode = launchMode,
Plugins = plugins is null ? [] : [.. plugins],
LoginCommands = loginCommands is null ? [] : [.. loginCommands],
};
profile.Characters.Add(character);
return character;
}
/// <summary>
/// Edits the identity cache and/or user-owned settings of an existing
/// character row. Passing an empty <paramref name="newId"/> clears a
/// manually entered id so launches fall back to the character name.
/// </summary>
public void EditCharacter(
string serverName,
@ -339,12 +385,42 @@ public sealed class LauncherProfileStore
string characterName,
LaunchMode? launchMode = null,
IReadOnlyList<string>? plugins = null,
IReadOnlyList<string>? loginCommands = null)
IReadOnlyList<string>? loginCommands = null,
string? newName = null,
string? newId = null)
{
ServerProfile server = FindServerOrThrow(serverName);
AccountProfile profile = FindAccountOrThrow(server, account);
CharacterProfile character = FindCharacterOrThrow(profile, characterName);
if (newName is not null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(newName);
if (!string.Equals(newName, character.Name, StringComparison.Ordinal)
&& FindCharacter(profile, newName) is not null)
{
throw new LauncherProfileException(
$"Character '{newName}' already exists on account '{account}'.");
}
character.Name = newName;
}
if (newId is not null)
{
string? normalizedId = NormalizeCharacterId(newId);
if (normalizedId is not null
&& profile.Characters.Any(candidate =>
!ReferenceEquals(candidate, character)
&& CharacterIdsEqual(candidate.Id, normalizedId)))
{
throw new LauncherProfileException(
$"Character id '{normalizedId}' already exists on account '{account}'.");
}
character.Id = normalizedId;
}
if (launchMode is not null)
{
character.LaunchMode = launchMode.Value;
@ -361,6 +437,17 @@ public sealed class LauncherProfileStore
}
}
public void RemoveCharacter(
string serverName,
string account,
string characterName)
{
ServerProfile server = FindServerOrThrow(serverName);
AccountProfile profile = FindAccountOrThrow(server, account);
CharacterProfile character = FindCharacterOrThrow(profile, characterName);
profile.Characters.Remove(character);
}
/// <summary>
/// Folds a reported character roster into an account's
/// <see cref="AccountProfile.Characters"/> (Campaign LA spec §3/§5/
@ -456,20 +543,46 @@ public sealed class LauncherProfileStore
$"No account '{account}' on server '{server.Name}'.");
}
private static CharacterProfile? FindCharacter(
AccountProfile profile,
string characterName) =>
profile.Characters.Find(
character => string.Equals(
character.Name,
characterName,
StringComparison.Ordinal));
private static CharacterProfile FindCharacterOrThrow(
AccountProfile profile,
string characterName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(characterName);
return profile.Characters.Find(
character => string.Equals(
character.Name,
characterName,
StringComparison.Ordinal))
return FindCharacter(profile, characterName)
?? throw new LauncherProfileException(
$"No character '{characterName}' on account '{profile.Account}'.");
}
private static string? NormalizeCharacterId(string? id)
{
if (string.IsNullOrWhiteSpace(id))
{
return null;
}
if (!CharacterIdFormat.TryParse(id, out uint parsed) || parsed == 0)
{
throw new LauncherProfileException(
"Character id must be a non-zero hexadecimal value with a 0x prefix.");
}
return CharacterIdFormat.ToHexString(parsed);
}
private static bool CharacterIdsEqual(string? left, string? right) =>
CharacterIdFormat.TryParse(left, out uint leftId)
&& CharacterIdFormat.TryParse(right, out uint rightId)
&& leftId == rightId;
private static void RequireValidPort(int port)
{
if (port is < 1 or > 65535)