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:
Erik 2026-08-15 14:26:10 +02:00
parent f3ef7baae2
commit 9a84230c4f
9 changed files with 3043 additions and 5 deletions

View file

@ -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; }