acdream/src/AcDream.Runtime/GameRuntimeCommands.cs
Erik 34e3a534be feat(chargen): Campaign CC slice CC5 — Summary page, Finish flow, RandomizeCharacter port
Fills TS-82's Summary placeholder with a faithful port of gmCGSummaryPage
(name field with NameInputFilter + the retail commit-on-focus-lost/submit
dispatch + the >32-char ID_CharGen_NameTooLong reject-and-revert path, the
REAL three-row-template listbox confirmed against the installed EoR dat
before writing any page code, and Summary's own independent gmCG3DView
preview instance wired through a second ChargenPreviewController pair
mirroring the Appearance page's exact composition shape).

Ports CharGenState::RandomizeCharacter and its six sub-primitives into
RuntimeCharacterCreationState — not approximated: the RandInt/RollDice
semantics are independently confirmed from both the decompiled RNG bodies
and the CharGenStateVtbl union struct in acclient.h. Three consumers:
the chargen screen's open-roll (retiring AP-214's honest-blank deviation
and reproducing the Appearance page's gender-flip-on-init quirk), the
Summary page's Random button (behind the retail randomize-warning
confirm), and the Appearance page's Random button (narrowing AP-212 to
just Heritage/Profession/Town's still-approximated rolls and Skills'
still-unported RandomizeSkills).

Wires the Finish button (previously ghosted) with retail's NoName/
CreditWarning dialog pair, adds the F12 amendment's HeritageOrGenderUnset
local refusal to TryBeginFinish (register AP-223) as a defensive backstop
now that the screen-open roll normally makes it unreachable, and wires
the four ID_Character_Err_* rejection dialogs for the 0xF643 response
codes CC3 already parsed but nothing displayed.

Register: TS-82 retired, AP-214 retired, AP-212 narrowed, AP-223/224/225
filed (heritage/gender Finish refusal, Summary's two-bucket skill-list
narrowing, the 32-vs-33 name-length threshold reconciliation).

Runtime 1722/0 (was 1713), App 5240/3 skips (was 5223/3), Headless 166/0
unchanged, full solution Release build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 00:01:07 +02:00

524 lines
15 KiB
C#

namespace AcDream.Runtime;
public enum RuntimeCommandStatus
{
Accepted,
Inactive,
StaleGeneration,
Unsupported,
Rejected,
}
public readonly record struct RuntimeCommandResult(
RuntimeCommandStatus Status,
RuntimeGenerationToken Generation,
uint ResultObjectId = 0u)
{
public bool Accepted => Status == RuntimeCommandStatus.Accepted;
}
public enum RuntimeSessionStartStatus
{
Disabled,
MissingCredentials,
NoCharacters,
Connected,
Deferred,
Failed,
Inactive,
StaleGeneration,
/// <summary>
/// Campaign LA slice LA2: mirrors
/// <see cref="Session.LiveSessionStartStatus.ProbeComplete"/> — a probe
/// session connected, reported its roster, and gracefully disconnected
/// before EnterWorld. A SUCCESS outcome for the headless process host's
/// exit-code mapping, not a failure.
/// </summary>
ProbeComplete,
AwaitingCharacterSelection,
}
public readonly record struct RuntimeSessionStartResult(
RuntimeSessionStartStatus Status,
RuntimeGenerationToken Generation,
uint CharacterId = 0u,
string CharacterName = "",
Exception? Error = null);
public enum RuntimeSelectionCommand
{
SelectClosestHostile,
SelectPrevious,
ExamineSelected,
UseSelected,
PickUpSelected,
}
public enum RuntimeCombatCommand
{
ToggleMode,
}
public enum RuntimeMovementCommand
{
ToggleRunLock,
Stop,
Ready,
Sit,
Crouch,
Sleep,
}
public enum RuntimeChatChannel
{
Say,
Tell,
Fellowship,
Allegiance,
Vassals,
Patron,
Monarch,
CoVassals,
General,
Trade,
LookingForGroup,
Roleplay,
Society,
Olthoi,
/// <summary>
/// Campaign CH slice CH3 (2026-08-09): the legacy
/// <c>ChatChannel.AllegianceBroadcast (0x02000000)</c> — the monarch/
/// speaker-permission broadcast bound to retail's <c>@ab</c> verb.
/// Distinct from <see cref="Allegiance"/>, which is ALWAYS the Turbine
/// room now (retail's <c>@a</c> binds unconditionally to
/// <c>DoTurbineChat_Allegiance</c> once Turbine chat starts — see the
/// research doc §4.3/§6.7).
/// </summary>
AllegianceBroadcast,
}
public readonly record struct RuntimeChatCommand(
RuntimeChatChannel Channel,
string Text,
string? TargetName = null);
public enum RuntimePortalCommand
{
RecallLifestone,
RecallMarketplace,
RecallHouse,
RecallMansion,
}
public interface IRuntimeSessionCommands
{
RuntimeSessionStartResult Start(RuntimeGenerationToken expectedGeneration);
RuntimeSessionStartResult Reconnect(RuntimeGenerationToken expectedGeneration);
RuntimeTeardownAcknowledgement Stop(RuntimeGenerationToken expectedGeneration);
}
public interface IRuntimeSelectionCommands
{
RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimeSelectionCommand command);
RuntimeCommandResult SelectObject(
RuntimeGenerationToken expectedGeneration,
uint objectId);
RuntimeCommandResult Clear(
RuntimeGenerationToken expectedGeneration);
}
public interface IRuntimeCombatCommands
{
RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimeCombatCommand command);
RuntimeCommandResult ExecuteAttack(
RuntimeGenerationToken expectedGeneration,
in Gameplay.RuntimeCombatAttackInput command);
}
public readonly record struct RuntimeMagicCommand(uint SpellId);
public interface IRuntimeMagicCommands
{
RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
in RuntimeMagicCommand command);
}
public interface IRuntimeMovementCommands
{
RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimeMovementCommand command);
RuntimeCommandResult SetIntent(
RuntimeGenerationToken expectedGeneration,
in Gameplay.MovementInput input);
RuntimeCommandResult ClearIntent(
RuntimeGenerationToken expectedGeneration);
}
public interface IRuntimeChatCommands
{
RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
in RuntimeChatCommand command);
}
public interface IRuntimePortalCommands
{
RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimePortalCommand command);
}
public readonly record struct RuntimeShortcutCommand(
int Index,
uint ObjectId,
uint SpellId);
public interface IRuntimeInventoryStateCommands
{
RuntimeCommandResult AddShortcut(
RuntimeGenerationToken expectedGeneration,
in RuntimeShortcutCommand command);
RuntimeCommandResult RemoveShortcut(
RuntimeGenerationToken expectedGeneration,
int index);
}
public interface IRuntimeSpellbookCommands
{
RuntimeCommandResult AddFavorite(
RuntimeGenerationToken expectedGeneration,
int tabIndex,
int position,
uint spellId);
RuntimeCommandResult RemoveFavorite(
RuntimeGenerationToken expectedGeneration,
int tabIndex,
uint spellId);
RuntimeCommandResult SetFilter(
RuntimeGenerationToken expectedGeneration,
uint filters);
RuntimeCommandResult ForgetSpell(
RuntimeGenerationToken expectedGeneration,
uint spellId);
RuntimeCommandResult SetDesiredComponent(
RuntimeGenerationToken expectedGeneration,
uint componentId,
uint amount);
RuntimeCommandResult ClearDesiredComponents(
RuntimeGenerationToken expectedGeneration);
}
public enum RuntimeAdvancementKind
{
Attribute,
Vital,
Skill,
TrainSkill,
}
public readonly record struct RuntimeAdvancementCommand(
RuntimeAdvancementKind Kind,
uint StatId,
ulong Cost);
public interface IRuntimeCharacterCommands
{
RuntimeCommandResult Advance(
RuntimeGenerationToken expectedGeneration,
in RuntimeAdvancementCommand command);
/// <summary>
/// Retail <c>SetSingleCharacterOption (GameActionType 0x0005)</c> —
/// toggles one character option. Campaign CH slice CH3 (2026-08-09)
/// replaced the former <c>SetOptions1</c> (the malformed, callerless
/// full-Options1-blob 0x01A1 builder) with this: the only wire message
/// that actually changes Turbine room membership for the six
/// <c>ListenTo*Chat</c> ids.
/// </summary>
RuntimeCommandResult SetSingleOption(
RuntimeGenerationToken expectedGeneration,
uint optionId,
bool value);
/// <summary>
/// Retail <c>CPlayerModule::SaveToServer(force: 0)</c> — the batched-
/// option blob-flush verb (Campaign OP slice OP1, 2026-08-10). Flushes
/// <c>SetCharacterOptions (0x01A1)</c> iff the module is dirty; a clean
/// module sends nothing, matching retail exactly (both its own
/// production call sites — Apply, logout — pass <c>force = 0</c>).
/// </summary>
RuntimeCommandResult SaveOptions(RuntimeGenerationToken expectedGeneration);
}
public enum RuntimeFriendCommandKind
{
Add,
Remove,
Clear,
RequestLegacyList,
}
public readonly record struct RuntimeFriendCommand(
RuntimeFriendCommandKind Kind,
uint CharacterId = 0u,
string? Name = null);
public enum RuntimeSquelchScope
{
Character,
Account,
Global,
}
public readonly record struct RuntimeSquelchCommand(
RuntimeSquelchScope Scope,
bool Add,
uint CharacterId = 0u,
string? Name = null,
uint MessageType = 0u);
public interface IRuntimeSocialCommands
{
RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
in RuntimeFriendCommand command);
RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
in RuntimeSquelchCommand command);
}
// ── Fellowship / Allegiance (Campaign FA slice FA2, 2026-08-12) ────────────
/// <summary>
/// Generation-gated fellowship outbound actions. <see cref="Quit"/>
/// implements retail's leader hand-off rule itself (lane B §2.5/§3.6): the
/// current leader quitting WITHOUT disbanding sends <c>0x0290</c> (assign
/// a non-leader fellow) BEFORE <c>0x00A3</c> — see
/// <c>RuntimeFellowshipState.RequiresLeaderHandoffBeforeQuit</c>.
/// <see cref="SetPanelOpen"/> is D4's <c>0x00A6</c> panel-visibility
/// declaration, the command the fellowship page's show/hide seam calls
/// (FA3+); without it ACE freezes the roster's vitals stream at join.
/// </summary>
public interface IRuntimeFellowshipCommands
{
RuntimeCommandResult Create(
RuntimeGenerationToken expectedGeneration,
string fellowshipName,
bool shareXp);
RuntimeCommandResult Recruit(
RuntimeGenerationToken expectedGeneration,
uint targetGuid);
RuntimeCommandResult Dismiss(
RuntimeGenerationToken expectedGeneration,
uint targetGuid);
RuntimeCommandResult Quit(
RuntimeGenerationToken expectedGeneration,
bool disband);
RuntimeCommandResult AssignLeader(
RuntimeGenerationToken expectedGeneration,
uint newLeaderGuid);
RuntimeCommandResult SetOpen(
RuntimeGenerationToken expectedGeneration,
bool isOpen);
RuntimeCommandResult SetPanelOpen(
RuntimeGenerationToken expectedGeneration,
bool panelOpen);
}
/// <summary>Generation-gated allegiance outbound actions.</summary>
public interface IRuntimeAllegianceCommands
{
RuntimeCommandResult Swear(
RuntimeGenerationToken expectedGeneration,
uint patronGuid);
RuntimeCommandResult Break(
RuntimeGenerationToken expectedGeneration,
uint targetGuid);
RuntimeCommandResult Kick(
RuntimeGenerationToken expectedGeneration,
uint vassalGuid);
/// <summary><c>0x027B</c> — empty name queries self.</summary>
RuntimeCommandResult RequestInfo(
RuntimeGenerationToken expectedGeneration,
string playerName);
/// <summary><c>0x001F</c> — the allegiance panel's subscribe/unsubscribe toggle.</summary>
RuntimeCommandResult SetUpdateSubscription(
RuntimeGenerationToken expectedGeneration,
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"/>.
/// <paramref name="confirmUnspentCredits"/> is retail's <c>arg2 == 0</c>
/// — the credit-warning dialog's own confirm click — skipping the
/// unspent-attribute-credits gate; the ordinary caller passes
/// <c>false</c> (retail's <c>arg2 = 1</c> button click), which shows
/// that warning instead of sending when credits remain. See
/// <see cref="Session.RuntimeCharacterCreationState.TryBeginFinish"/>'s
/// doc comment for the full citation.</summary>
RuntimeCommandResult Finish(
RuntimeGenerationToken expectedGeneration,
bool confirmUnspentCredits = false);
RuntimeCommandResult AcknowledgeRejection(
RuntimeGenerationToken expectedGeneration);
// ── Campaign CC slice CC5: RandomizeCharacter port ──────────────────
/// <summary>Retail's ctor-time <c>CharGenState::RandomizeCharacter</c>
/// roll (mirrored at the App layer's screen-open edge) and the Summary
/// page's Random button (<c>gmCharGenMainUI::DoRandom</c> case 5, behind
/// the caller's own <c>ID_CharGen_RandomizeWarning</c> confirmation) —
/// see <see cref="Session.RuntimeCharacterCreationState.TryRandomizeCharacter"/>.</summary>
RuntimeCommandResult RandomizeCharacter(
RuntimeGenerationToken expectedGeneration);
/// <summary>The Appearance page's Random button while its Face sub-tab
/// is showing (<c>gmCharGenMainUI::DoRandom</c> case 3's <c>else</c>
/// arm) — see
/// <see cref="Session.RuntimeCharacterCreationState.TryRandomizeAppearance"/>.</summary>
RuntimeCommandResult RandomizeAppearance(
RuntimeGenerationToken expectedGeneration);
/// <summary>The Appearance page's Random button while its Clothes
/// sub-tab is showing (<c>gmCharGenMainUI::DoRandom</c> case 3's
/// <c>RandomizeClothing(state, 1)</c> arm) — see
/// <see cref="Session.RuntimeCharacterCreationState.TryRandomizeClothing"/>.</summary>
RuntimeCommandResult RandomizeClothing(
RuntimeGenerationToken expectedGeneration);
}
public interface IGameRuntimeCommands
{
IRuntimeSessionCommands Session { get; }
Session.IRuntimeCharacterSelectionCommands CharacterSelection =>
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; }
IRuntimeMagicCommands Magic { get; }
IRuntimeMovementCommands Movement { get; }
IRuntimeChatCommands Chat { get; }
IRuntimePortalCommands Portal { get; }
IRuntimeInventoryStateCommands InventoryState { get; }
IRuntimeSpellbookCommands Spellbook { get; }
IRuntimeCharacterCommands Character { get; }
IRuntimeSocialCommands Social { get; }
IRuntimeFellowshipCommands Fellowship { get; }
IRuntimeAllegianceCommands Allegiance { get; }
}