feat(runtime): Campaign CC slice CC3 — RuntimeCharacterCreationState
Ports retail's CharGenState as the one Runtime-owned character-creation state machine, mirroring RuntimeCharacterSelectionState's exact pattern (snapshot/delta/event-stream, borrow-only view, generation-gated commands, one mutable owner, no App types). Every command ports a named retail function: SetHeritageGroup, SetGender, SetTemplate/ApplyTemplate (Custom = template 0, Olthoi force-lock), the six attribute setters plus GetAbsRemainingCredits/BalanceAttributes (retail's literal round-robin order and fairness cursor), SetSkillLevel plus ResetSkillLevels' free-skill baseline (reusing CC1's ChargenSkillCreditMath two-tier cost lookup verbatim), RandomizeStartArea, and DoFinish's complete gate sequence (empty name / unspent attribute credits / already-Pending / client-side roster-vs-slotCount cap). LiveSessionController gained a sibling IRuntimeCharacterCreationCommands implementation, a CreateCharacter wire hook, and a response handler that reuses existing machinery rather than inventing new paths: the Ok identity is appended to the roster via RuntimeCharacterSelectionState's own ApplyRoster, and the "log straight in" behavior reuses the private EnterSelectedCore. ILiveSessionLifecycleHost gained two default-no-op hooks (ApplyCharacterCreated/ApplyCreationFailed) so AcDream.App needs zero changes to keep compiling; wiring them to the status stream is a CC4 follow-up. Filed four divergence-register rows for the corners deliberately not ported: the FPU-unrecoverable FitTemplateToCharacter auto-detect (AP-207, ACE only reads the field for title text), the per-style color-count approximation (AP-208, CC1's model has no per-style palette data), the classID DAT-DID placeholder (AP-209, ACE ignores the field), and ApplyTemplate's atomic-vs-sequential attribute apply (AP-210). 34 new tests: full state-machine coverage (every Finish gate, every rejection-code mapping, duplicate-NameInUse tolerance, Olthoi lock, attribute balance/lock interaction, uncostable-skill rejection) plus a LiveSessionController integration suite proving the wire send is exactly 55 skill slots (decoded from a real WorldSession + GameMessageCapture) and the full Ok/rejection round trip through WorldSession.ProcessDatagram. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f3ef7baae2
commit
9a84230c4f
9 changed files with 3043 additions and 5 deletions
|
|
@ -378,6 +378,82 @@ public interface IRuntimeAllegianceCommands
|
|||
bool on);
|
||||
}
|
||||
|
||||
// ── Character creation (Campaign CC slice CC3, 2026-08-15) ─────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Generation-gated character-creation (CharGenState) outbound actions —
|
||||
/// lands beside <see cref="IGameRuntimeCommands.CharacterSelection"/>, the
|
||||
/// same command family shape.
|
||||
/// </summary>
|
||||
public interface IRuntimeCharacterCreationCommands
|
||||
{
|
||||
RuntimeCommandResult SelectHeritage(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint heritageId);
|
||||
|
||||
RuntimeCommandResult SelectGender(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint genderKey);
|
||||
|
||||
RuntimeCommandResult SelectTemplate(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint templateIndex);
|
||||
|
||||
RuntimeCommandResult SetAttribute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
Session.ChargenAttributeId attributeId,
|
||||
int value);
|
||||
|
||||
RuntimeCommandResult SetAttributeLock(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
Session.ChargenAttributeId attributeId,
|
||||
bool locked);
|
||||
|
||||
RuntimeCommandResult TrainSkill(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint skillId);
|
||||
|
||||
RuntimeCommandResult SpecializeSkill(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint skillId);
|
||||
|
||||
RuntimeCommandResult UntrainSkill(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint skillId);
|
||||
|
||||
RuntimeCommandResult SetAppearanceIndex(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
Session.ChargenAppearanceSlot slot,
|
||||
uint index);
|
||||
|
||||
RuntimeCommandResult SetShade(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
Session.ChargenShadeSlot slot,
|
||||
double value);
|
||||
|
||||
RuntimeCommandResult SelectStartArea(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
int startAreaIndex);
|
||||
|
||||
RuntimeCommandResult SetName(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
string name);
|
||||
|
||||
RuntimeCommandResult SetSlot(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint slot);
|
||||
|
||||
/// <summary>Retail's Finish button (<c>gmCharGenMainUI::DoFinish @
|
||||
/// 0x004E9170</c>). On acceptance the request is already on the wire;
|
||||
/// the Ok/rejection reply arrives asynchronously as a status delta —
|
||||
/// see <see cref="Session.RuntimeCharacterCreationState"/>.</summary>
|
||||
RuntimeCommandResult Finish(
|
||||
RuntimeGenerationToken expectedGeneration);
|
||||
|
||||
RuntimeCommandResult AcknowledgeRejection(
|
||||
RuntimeGenerationToken expectedGeneration);
|
||||
}
|
||||
|
||||
public interface IGameRuntimeCommands
|
||||
{
|
||||
IRuntimeSessionCommands Session { get; }
|
||||
|
|
@ -386,6 +462,10 @@ public interface IGameRuntimeCommands
|
|||
throw new NotSupportedException(
|
||||
"This command adapter does not project character selection.");
|
||||
|
||||
IRuntimeCharacterCreationCommands CharacterCreation =>
|
||||
throw new NotSupportedException(
|
||||
"This command adapter does not project character creation.");
|
||||
|
||||
IRuntimeSelectionCommands Selection { get; }
|
||||
|
||||
IRuntimeCombatCommands Combat { get; }
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ public sealed class DirectGameRuntimeCommandAdapter
|
|||
public IRuntimeSessionCommands Session => this;
|
||||
public IRuntimeCharacterSelectionCommands CharacterSelection =>
|
||||
_runtime.Session;
|
||||
public IRuntimeCharacterCreationCommands CharacterCreation =>
|
||||
_runtime.Session;
|
||||
public IRuntimeSelectionCommands Selection => this;
|
||||
public IRuntimeCombatCommands Combat => this;
|
||||
public IRuntimeMagicCommands Magic => this;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using AcDream.Core.CharGen;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
||||
|
|
@ -107,6 +108,22 @@ public interface ILiveSessionLifecycleHost
|
|||
void ApplySelectedCharacter(LiveSessionCharacterSelection selection);
|
||||
void ApplyEnteredWorld(LiveSessionCharacterSelection selection);
|
||||
void DetachSession(WorldSession session);
|
||||
|
||||
/// <summary>
|
||||
/// Campaign CC slice CC3: reported once per successful character
|
||||
/// creation (the <c>0xF643</c> Ok identity), right before the roster is
|
||||
/// re-reported with the new entry appended and the reused enter-selected
|
||||
/// path runs — the same spot <see cref="ReportRoster"/> and
|
||||
/// <see cref="ApplyEnteredWorld"/> already occupy. Default no-op: a host
|
||||
/// that wants status-stream parity with
|
||||
/// <c>SessionStatusWriter.CharacterCreated</c> overrides this.
|
||||
/// </summary>
|
||||
void ApplyCharacterCreated(RuntimeCharacterCreationIdentity identity) { }
|
||||
|
||||
/// <summary>Campaign CC slice CC3: reported once per non-Ok <c>0xF643</c>
|
||||
/// creation response. Default no-op — see <see cref="ApplyCharacterCreated"/>.
|
||||
/// </summary>
|
||||
void ApplyCreationFailed(RuntimeCharacterCreationRejection rejection) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -192,6 +209,15 @@ public interface ILiveSessionOperations
|
|||
session.SendDeleteCharacter(accountName, activeCharacterIndex);
|
||||
void RestoreCharacter(WorldSession session, uint characterId) =>
|
||||
session.SendRestoreCharacter(characterId);
|
||||
/// <summary>Campaign CC slice CC3: retail's <c>Proto_UI::SendCharGenResult
|
||||
/// @ 0x00546A70</c> outbound send, reached from <c>gmCharGenMainUI::DoFinish</c>.
|
||||
/// </summary>
|
||||
void CreateCharacter(
|
||||
WorldSession session,
|
||||
string accountName,
|
||||
CharacterCreate.Request request,
|
||||
ReadOnlySpan<uint> skillAdvancementClasses) =>
|
||||
session.SendCharacterCreation(accountName, request, skillAdvancementClasses);
|
||||
void Tick(WorldSession session);
|
||||
void DisposeSession(WorldSession session);
|
||||
}
|
||||
|
|
@ -247,7 +273,8 @@ internal sealed class ProductionLiveSessionOperations : ILiveSessionOperations
|
|||
public sealed class LiveSessionController
|
||||
: IDisposable,
|
||||
IRuntimeLiveSessionFramePhase,
|
||||
IRuntimeCharacterSelectionCommands
|
||||
IRuntimeCharacterSelectionCommands,
|
||||
IRuntimeCharacterCreationCommands
|
||||
{
|
||||
private sealed class CharacterSelectionWireBinding : IDisposable
|
||||
{
|
||||
|
|
@ -257,6 +284,7 @@ public sealed class LiveSessionController
|
|||
private readonly Action<CharacterRestore.Parsed> _restore;
|
||||
private readonly Action<CharacterError.Parsed> _error;
|
||||
private readonly Action<ServerName.Parsed> _worldName;
|
||||
private readonly Action<CharGenVerificationResponse.Parsed> _created;
|
||||
|
||||
public CharacterSelectionWireBinding(
|
||||
WorldSession session,
|
||||
|
|
@ -264,7 +292,8 @@ public sealed class LiveSessionController
|
|||
Action delete,
|
||||
Action<CharacterRestore.Parsed> restore,
|
||||
Action<CharacterError.Parsed> error,
|
||||
Action<ServerName.Parsed> worldName)
|
||||
Action<ServerName.Parsed> worldName,
|
||||
Action<CharGenVerificationResponse.Parsed> created)
|
||||
{
|
||||
_session = session;
|
||||
_roster = roster;
|
||||
|
|
@ -272,11 +301,13 @@ public sealed class LiveSessionController
|
|||
_restore = restore;
|
||||
_error = error;
|
||||
_worldName = worldName;
|
||||
_created = created;
|
||||
session.CharacterListReceived += roster;
|
||||
session.CharacterDeleteAcknowledged += delete;
|
||||
session.CharacterRestoreReceived += restore;
|
||||
session.CharacterErrorReceived += error;
|
||||
session.ServerNameReceived += worldName;
|
||||
session.CharacterCreateResponseReceived += created;
|
||||
}
|
||||
|
||||
public bool IsDisposed => _session is null;
|
||||
|
|
@ -291,6 +322,7 @@ public sealed class LiveSessionController
|
|||
session.CharacterRestoreReceived -= _restore;
|
||||
session.CharacterErrorReceived -= _error;
|
||||
session.ServerNameReceived -= _worldName;
|
||||
session.CharacterCreateResponseReceived -= _created;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -397,11 +429,19 @@ public sealed class LiveSessionController
|
|||
|
||||
public LiveSessionController(
|
||||
ILiveSessionOperations operations,
|
||||
TimeProvider? timeProvider = null)
|
||||
TimeProvider? timeProvider = null,
|
||||
ChargenOptions? chargenOptions = null)
|
||||
{
|
||||
_operations = operations ?? throw new ArgumentNullException(nameof(operations));
|
||||
CharacterSelectionState = new RuntimeCharacterSelectionState(
|
||||
timeProvider);
|
||||
// Campaign CC slice CC3: defaults to ChargenOptions.Empty (no
|
||||
// heritages configured) — loading the installed DAT's chargen table
|
||||
// and threading it through is a future App/GameRuntime wiring slice,
|
||||
// not this one. A caller that never supplies real options simply
|
||||
// gets an inert chargen surface (every heritage lookup misses).
|
||||
CharacterCreationState = new RuntimeCharacterCreationState(
|
||||
chargenOptions ?? ChargenOptions.Empty);
|
||||
}
|
||||
|
||||
public RuntimeCharacterSelectionState CharacterSelectionState { get; }
|
||||
|
|
@ -409,6 +449,11 @@ public sealed class LiveSessionController
|
|||
public IRuntimeCharacterSelectionView CharacterSelection =>
|
||||
CharacterSelectionState.View;
|
||||
|
||||
public RuntimeCharacterCreationState CharacterCreationState { get; }
|
||||
|
||||
public IRuntimeCharacterCreationView CharacterCreation =>
|
||||
CharacterCreationState.View;
|
||||
|
||||
public WorldSession? CurrentSession
|
||||
{
|
||||
get { lock (_gate) return _scope?.Session; }
|
||||
|
|
@ -684,6 +729,7 @@ public sealed class LiveSessionController
|
|||
ulong generation = ++_generation;
|
||||
RuntimeGenerationToken activeGeneration = new(generation);
|
||||
CharacterSelectionState.Reset(activeGeneration);
|
||||
CharacterCreationState.Reset(activeGeneration);
|
||||
try
|
||||
{
|
||||
DrainRetiredScope();
|
||||
|
|
@ -710,6 +756,7 @@ public sealed class LiveSessionController
|
|||
return new LiveSessionStartResult(LiveSessionStartStatus.MissingCredentials);
|
||||
|
||||
CharacterSelectionState.Begin(activeGeneration);
|
||||
CharacterCreationState.Begin(activeGeneration);
|
||||
|
||||
SessionScope? scope = null;
|
||||
try
|
||||
|
|
@ -921,6 +968,14 @@ public sealed class LiveSessionController
|
|||
if (IsCurrent(scope, generation))
|
||||
CharacterSelectionState.ApplyWorldName(worldName.WorldName);
|
||||
}
|
||||
},
|
||||
created =>
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (IsCurrent(scope, generation))
|
||||
HandleCharacterCreationResponse(scope, generation, created);
|
||||
}
|
||||
});
|
||||
|
||||
public RuntimeCommandResult Highlight(
|
||||
|
|
@ -1149,6 +1204,350 @@ public sealed class LiveSessionController
|
|||
new RuntimeGenerationToken(_generation),
|
||||
characterId);
|
||||
|
||||
// ── Character creation (Campaign CC slice CC3) ─────────────────────
|
||||
|
||||
/// <summary>Collects <see cref="RuntimeCharacterSelectionEntry"/> rows
|
||||
/// into <see cref="LiveSessionRosterEntry"/> form for
|
||||
/// <see cref="HandleCharacterCreationResponse"/>'s roster-append
|
||||
/// composition.</summary>
|
||||
private sealed class RosterCollector(List<LiveSessionRosterEntry> entries)
|
||||
: IRuntimeCharacterSelectionVisitor
|
||||
{
|
||||
public void Visit(in RuntimeCharacterSelectionEntry character) =>
|
||||
entries.Add(new LiveSessionRosterEntry(
|
||||
character.CharacterId,
|
||||
character.Name,
|
||||
character.SecondsGreyedOut));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ports the Ok half of <c>Handle_CharGenVerificationResponse @
|
||||
/// 0x0055E8B0</c> case 1 (the PENDING/create branch —
|
||||
/// <c>CharacterSet::AddIdentity</c>) composed with
|
||||
/// <c>gmCharGenMainUI::Update @ 0x004E8460</c>'s per-frame roster scan
|
||||
/// (which finds the freshly appended identity by name and calls
|
||||
/// <c>CPlayerSystem::LogOnCharacter</c> directly): a fresh
|
||||
/// <see cref="RuntimeCharacterCreationState"/> is disambiguated locally
|
||||
/// (we already have the exact created guid/name from the SAME reply
|
||||
/// that triggered the roster append, so there is no need to re-scan for
|
||||
/// it the way retail's per-frame poll does — an equivalent, not a
|
||||
/// divergent, substitution). Reuses <see cref="RuntimeCharacterSelectionState.ApplyRoster"/>
|
||||
/// for the append (there is no single-entry append primitive to
|
||||
/// duplicate) and <see cref="EnterSelectedCore"/> for the log-straight-in
|
||||
/// (no second enter route). A non-Ok reply only needs the state-machine
|
||||
/// update already performed by <see cref="RuntimeCharacterCreationState.ApplyCreationResponse"/>
|
||||
/// — no roster/enter side effects.
|
||||
/// </summary>
|
||||
private void HandleCharacterCreationResponse(
|
||||
SessionScope scope,
|
||||
ulong generation,
|
||||
CharGenVerificationResponse.Parsed response)
|
||||
{
|
||||
CharacterCreationState.ApplyCreationResponse(response);
|
||||
RuntimeCharacterCreationSnapshot creation = CharacterCreationState.Snapshot;
|
||||
|
||||
if (creation.LastCreated is { } created)
|
||||
scope.Host.ApplyCharacterCreated(created);
|
||||
else if (creation.LastRejection is { } rejection)
|
||||
scope.Host.ApplyCreationFailed(rejection);
|
||||
|
||||
if (creation.LastCreated is not { } identity)
|
||||
return;
|
||||
|
||||
RuntimeCharacterSelectionSnapshot before = CharacterSelectionState.Snapshot;
|
||||
var entries = new List<LiveSessionRosterEntry>(before.RosterCount + 1);
|
||||
CharacterSelectionState.View.Visit(new RosterCollector(entries));
|
||||
entries.Add(new LiveSessionRosterEntry(
|
||||
identity.Guid,
|
||||
identity.Name,
|
||||
SecondsGreyedOut: 0u));
|
||||
var report = new LiveSessionRosterReport(
|
||||
before.AccountName,
|
||||
before.SlotCount,
|
||||
entries);
|
||||
CharacterSelectionState.ApplyRoster(report);
|
||||
scope.Host.ReportRoster(report);
|
||||
if (!IsCurrent(scope, generation))
|
||||
return;
|
||||
|
||||
if (!CharacterSelectionState.TryHighlight(identity.Guid))
|
||||
return;
|
||||
if (!IsCurrent(scope, generation))
|
||||
return;
|
||||
|
||||
// Inline, not through the public Enter() command: we are already
|
||||
// running inside Tick()'s top-level operation (this handler fires
|
||||
// synchronously from _operations.Tick's inbound processing), exactly
|
||||
// the same calling convention StartCore's own inline enter uses.
|
||||
_ = EnterSelectedCore();
|
||||
}
|
||||
|
||||
public RuntimeCommandResult SelectHeritage(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint heritageId)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return CharacterCreationResult(gate);
|
||||
return CharacterCreationResult(
|
||||
CharacterCreationState.TrySelectHeritage(heritageId));
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeCommandResult SelectGender(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint genderKey)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return CharacterCreationResult(gate);
|
||||
return CharacterCreationResult(
|
||||
CharacterCreationState.TrySelectGender(genderKey));
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeCommandResult SelectTemplate(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint templateIndex)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return CharacterCreationResult(gate);
|
||||
return CharacterCreationResult(
|
||||
CharacterCreationState.TrySelectTemplate(templateIndex));
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeCommandResult SetAttribute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
ChargenAttributeId attributeId,
|
||||
int value)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return CharacterCreationResult(gate);
|
||||
return CharacterCreationResult(
|
||||
CharacterCreationState.TrySetAttribute(attributeId, value));
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeCommandResult SetAttributeLock(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
ChargenAttributeId attributeId,
|
||||
bool locked)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return CharacterCreationResult(gate);
|
||||
return CharacterCreationResult(
|
||||
CharacterCreationState.TrySetAttributeLock(attributeId, locked));
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeCommandResult TrainSkill(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint skillId)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return CharacterCreationResult(gate);
|
||||
return CharacterCreationResult(CharacterCreationState.TryTrainSkill(skillId));
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeCommandResult SpecializeSkill(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint skillId)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return CharacterCreationResult(gate);
|
||||
return CharacterCreationResult(CharacterCreationState.TrySpecializeSkill(skillId));
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeCommandResult UntrainSkill(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint skillId)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return CharacterCreationResult(gate);
|
||||
return CharacterCreationResult(CharacterCreationState.TryUntrainSkill(skillId));
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeCommandResult SetAppearanceIndex(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
ChargenAppearanceSlot slot,
|
||||
uint index)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return CharacterCreationResult(gate);
|
||||
return CharacterCreationResult(
|
||||
CharacterCreationState.TrySetAppearanceIndex(slot, index));
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeCommandResult SetShade(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
ChargenShadeSlot slot,
|
||||
double value)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return CharacterCreationResult(gate);
|
||||
return CharacterCreationResult(CharacterCreationState.TrySetShade(slot, value));
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeCommandResult SelectStartArea(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
int startAreaIndex)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return CharacterCreationResult(gate);
|
||||
return CharacterCreationResult(
|
||||
CharacterCreationState.TrySelectStartArea(startAreaIndex));
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeCommandResult SetName(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
string name)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return CharacterCreationResult(gate);
|
||||
return CharacterCreationResult(CharacterCreationState.TrySetName(name ?? string.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeCommandResult SetSlot(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint slot)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return CharacterCreationResult(gate);
|
||||
return CharacterCreationResult(CharacterCreationState.TrySetSlot(slot));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ports <c>gmCharGenMainUI::DoFinish @ 0x004E9170</c>'s send half: the
|
||||
/// local gates live in <see cref="RuntimeCharacterCreationState.TryBeginFinish"/>;
|
||||
/// this method supplies the roster/slot-cap inputs from
|
||||
/// <see cref="CharacterSelectionState"/> and, on acceptance, sends the
|
||||
/// wire request via <c>Proto_UI::SendCharGenResult</c>'s port
|
||||
/// (<see cref="ILiveSessionOperations.CreateCharacter"/>). A transport
|
||||
/// failure resets the verification latch the same way an unsolicited
|
||||
/// Undef/Pending reply does (<see cref="RuntimeCharacterCreationState.ApplyCreationResponse"/>)
|
||||
/// rather than leaving it stuck Pending forever.
|
||||
/// </summary>
|
||||
public RuntimeCommandResult Finish(RuntimeGenerationToken expectedGeneration)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return CharacterCreationResult(gate);
|
||||
|
||||
RuntimeCharacterSelectionSnapshot selection = CharacterSelectionState.Snapshot;
|
||||
if (!CharacterCreationState.TryBeginFinish(
|
||||
selection.RosterCount,
|
||||
selection.SlotCount,
|
||||
out CharacterCreate.Request request,
|
||||
out uint[] skillAdvancementClasses,
|
||||
out _))
|
||||
{
|
||||
return CharacterCreationResult(RuntimeCommandStatus.Rejected);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_operations.CreateCharacter(
|
||||
_scope!.Session,
|
||||
selection.AccountName,
|
||||
request,
|
||||
skillAdvancementClasses);
|
||||
return CharacterCreationResult(RuntimeCommandStatus.Accepted);
|
||||
}
|
||||
catch
|
||||
{
|
||||
CharacterCreationState.ApplyCreationResponse(
|
||||
new CharGenVerificationResponse.Parsed(
|
||||
(uint)CharGenVerificationResponse.Code.Undef,
|
||||
null,
|
||||
null,
|
||||
null));
|
||||
return CharacterCreationResult(RuntimeCommandStatus.Rejected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeCommandResult AcknowledgeRejection(
|
||||
RuntimeGenerationToken expectedGeneration)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return CharacterCreationResult(gate);
|
||||
return CharacterCreationResult(
|
||||
CharacterCreationState.TryAcknowledgeRejection());
|
||||
}
|
||||
}
|
||||
|
||||
private RuntimeCommandStatus ValidateCharacterCreationCommand(
|
||||
RuntimeGenerationToken expectedGeneration)
|
||||
{
|
||||
RuntimeGenerationToken current = new(_generation);
|
||||
if (expectedGeneration != current)
|
||||
return RuntimeCommandStatus.StaleGeneration;
|
||||
if (_disposed || _disposeRequested || _scope is null || _inWorld)
|
||||
return RuntimeCommandStatus.Inactive;
|
||||
return CharacterSelectionState.Snapshot.Lifecycle
|
||||
== RuntimeCharacterSelectionLifecycle.AwaitingSelection
|
||||
? RuntimeCommandStatus.Accepted
|
||||
: RuntimeCommandStatus.Inactive;
|
||||
}
|
||||
|
||||
private RuntimeCommandResult CharacterCreationResult(bool accepted) =>
|
||||
CharacterCreationResult(
|
||||
accepted ? RuntimeCommandStatus.Accepted : RuntimeCommandStatus.Rejected);
|
||||
|
||||
private RuntimeCommandResult CharacterCreationResult(RuntimeCommandStatus status) =>
|
||||
new(status, new RuntimeGenerationToken(_generation));
|
||||
|
||||
private void StopCore()
|
||||
{
|
||||
// MUST-FIX 1: TS-71's logout-flush half — retail's
|
||||
|
|
@ -1165,6 +1564,7 @@ public sealed class LiveSessionController
|
|||
_inWorld = false;
|
||||
_activeSelection = null;
|
||||
CharacterSelectionState.Reset(new RuntimeGenerationToken(_generation));
|
||||
CharacterCreationState.Reset(new RuntimeGenerationToken(_generation));
|
||||
if (_scope is { } scope)
|
||||
{
|
||||
_scope = null;
|
||||
|
|
@ -1306,6 +1706,7 @@ public sealed class LiveSessionController
|
|||
{
|
||||
StopCore();
|
||||
CharacterSelectionState.Dispose();
|
||||
CharacterCreationState.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
1469
src/AcDream.Runtime/Session/RuntimeCharacterCreationState.cs
Normal file
1469
src/AcDream.Runtime/Session/RuntimeCharacterCreationState.cs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue