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; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue