feat(chargen): Campaign CC slice CC7 — end-to-end create flow + connected checklist
Create button un-ghosts: retail's exact gate (gmCharacterManagementUI:: UpdateButtons @0x004ec240, roster count < allowed slot count) ported into RuntimeCharacterSelectionButtons.CanCreate; the button's OnClick opens the chargen screen through the same CharacterCreationUiController.Open() seam the ACDREAM_OPEN_CHARGEN=1 dev path already used. Exit/Back confirm on chargen needed no new return-path code — character-management is never hidden while chargen is open on top of it — verified end-to-end by a new cross-controller test rather than left as an inspection claim. Full-flow test coverage: a new comprehensive test decodes every 0xF656 field (including the trailing checksum, recomputed via the production CharacterCreate.ComputeChecksum) against a fully populated creation (heritage/gender/all appearance slots/template/explicit skill command/ town/name); a new Theory drives the remaining six 0xF643 rejection codes through the real wire decode path, closing the gap between the already-covered isolated state-machine Theory and an actual WorldSession round trip. Launcher payload cycle: two new tests drive a real Runtime create/reject through the real SessionStatusWriter (wired exactly as LiveSessionRuntimeFactory/HeadlessSessionHost do in production) and read the result back with the real Launcher.Core StatusFileTailer/ StatusEventParser — closing the one gap CC2's own per-layer tests never reached. No gap was found in production wiring itself: GameWindow already constructs a real, non-null SessionStatusWriter for both hosts. Also fixes 4 pre-existing LiveSessionControllerTests assertions that compared a full RuntimeCharacterSelectionButtons record and would have failed once CanCreate started being computed; corrects register row AP-211 to reflect that its own predicted resolution (the Create-button gate landing) has now happened — both layers are intentionally kept as retail-matching enforcement plus defense-in-depth, not one superseding the other. Adds docs/research/2026-08-16-campaign-cc-test-script.md, the user's connected-gate script covering both the launcher and dev-shortcut launch paths, the six-page create flow, every Finish outcome, and the known cosmetic/behavioral divergences (AP-212/213/215/216/217/218/219/220/222/ 224/226/228) so they aren't mistaken for new bugs during the gate. Gates: full solution Release build green; Runtime 1735/0 (was 1726/0, +9), App 5256/3 skips (was 5254/3, +2), Headless 166/0 (unchanged), Launcher.Core 324/0, one full-solution pass across every project clean (no known flakes reproduced this run). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
bb22ee8bde
commit
9cf6c52283
10 changed files with 957 additions and 37 deletions
|
|
@ -125,12 +125,16 @@ internal sealed class CharacterManagementUiController : IDisposable
|
|||
Root.Width > 0f ? Root.Width : 800f,
|
||||
Root.Height > 0f ? Root.Height : 600f);
|
||||
|
||||
// Create Character belongs to a future campaign. Keep retail's
|
||||
// authored control in place and visibly ghosted; do not hide it or
|
||||
// invent an action.
|
||||
// Campaign CC slice CC7: gmCharacterManagementUI::ListenToElementMessage
|
||||
// @ 0x004ed5a0 case 3 dispatches Create unconditionally on click
|
||||
// (QueueUIMode(0x1000000b) — no gate at click time); the gate lives
|
||||
// entirely in UpdateButtons @ 0x004ec240's own Enabled/ghosted state
|
||||
// (see ApplyButtons below), so the click handler is wired once here
|
||||
// and Enabled tracks the borrowed snapshot every tick. Starts
|
||||
// disabled/ghosted until the first real snapshot arrives.
|
||||
_create.Visible = true;
|
||||
_create.Enabled = false;
|
||||
_create.OnClick = null;
|
||||
_create.OnClick = RequestCreate;
|
||||
_enter.OnClick = EnterSelected;
|
||||
_delete.OnClick = RequestDelete;
|
||||
_restore.OnClick = RestoreSelected;
|
||||
|
|
@ -534,7 +538,7 @@ internal sealed class CharacterManagementUiController : IDisposable
|
|||
private void ApplyButtons(RuntimeCharacterSelectionButtons buttons)
|
||||
{
|
||||
_create.Visible = true;
|
||||
_create.Enabled = false;
|
||||
_create.Enabled = buttons.CanCreate;
|
||||
_enter.Enabled = buttons.CanEnter;
|
||||
_delete.Visible = buttons.DeleteVisible;
|
||||
_delete.Enabled = buttons.CanDelete;
|
||||
|
|
@ -550,6 +554,22 @@ internal sealed class CharacterManagementUiController : IDisposable
|
|||
InvalidateAndTick();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign CC slice CC7: <c>ListenToElementMessage</c> case 3 ->
|
||||
/// <c>QueueUIMode(0x1000000b)</c>. Purely presentational — no Runtime
|
||||
/// command, no roster/state change here; <see cref="_bindings"/>'
|
||||
/// <c>RequestCreate</c> is resolved per-call (never captured) so it
|
||||
/// reflects whatever <see cref="RetailUiRuntime"/> wired at the time of
|
||||
/// the click, matching every other late-bound seam in this bindings
|
||||
/// record.
|
||||
/// </summary>
|
||||
private void RequestCreate()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_bindings.RequestCreate?.Invoke();
|
||||
}
|
||||
|
||||
private void EnterSelected()
|
||||
{
|
||||
if (_disposed)
|
||||
|
|
|
|||
|
|
@ -398,7 +398,24 @@ public sealed record CharacterSelectionRuntimeBindings(
|
|||
Func<RuntimeCommandResult> ConfirmDelete,
|
||||
Func<RuntimeCommandResult> Restore,
|
||||
Func<RuntimeCommandResult> Cancel,
|
||||
Action RequestExit);
|
||||
Action RequestExit,
|
||||
/// <summary>
|
||||
/// Campaign CC slice CC7: retail's Create button
|
||||
/// (<c>gmCharacterManagementUI::ListenToElementMessage @ 0x004ed5a0</c>
|
||||
/// case 3 -> <c>UIFramework::QueueUIMode(this, 0x1000000b)</c>, the
|
||||
/// <c>gmCharGenMainUI</c> mode). Wired by <see cref="RetailUiRuntime"/>
|
||||
/// itself (it alone holds both the character-management and
|
||||
/// character-creation controllers) to
|
||||
/// <c>CharacterCreationController?.Open()</c> — resolved lazily so
|
||||
/// mount order between the two screens does not matter.
|
||||
/// <see langword="null"/> when no chargen screen is mounted (e.g. a
|
||||
/// headless bot's <c>LiveCharacterSelector</c> path, where
|
||||
/// <see cref="RetailUiRuntimeBindings.CharacterCreation"/> is also
|
||||
/// null) — the button then behaves as a no-op click while its own
|
||||
/// <c>Enabled</c> gate (<see cref="RuntimeCharacterSelectionButtons.CanCreate"/>)
|
||||
/// still reflects the real roster-vs-slot state.
|
||||
/// </summary>
|
||||
Action? RequestCreate = null);
|
||||
|
||||
public sealed record RetailUiRuntimeBindings(
|
||||
UiHost Host,
|
||||
|
|
@ -3816,9 +3833,18 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
if (bindings is null || _characterManagementMount is not null)
|
||||
return;
|
||||
|
||||
// Campaign CC slice CC7: RetailUiRuntime is the one object holding
|
||||
// BOTH controllers, so it supplies the cross-screen seam locally
|
||||
// rather than routing it through the externally-composed bindings
|
||||
// record (which is built before this runtime exists — see
|
||||
// CharacterSelectionRuntimeBindings.RequestCreate's own doc
|
||||
// comment). The lambda closes over `this` and reads
|
||||
// CharacterCreationController per call, so it is safe even though
|
||||
// ConfigureCharacterCreation() has not run yet at this point (see
|
||||
// its call site immediately below this method's own caller).
|
||||
_characterManagementMount = new CharacterManagementUiMountCoordinator(
|
||||
Host.Root,
|
||||
bindings,
|
||||
bindings with { RequestCreate = () => CharacterCreationController?.Open() },
|
||||
EnsureDialogFactory,
|
||||
LoadCharacterManagementResources);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,17 @@ public readonly record struct RuntimeCharacterSelectionButtons(
|
|||
bool CanDelete,
|
||||
bool CanRestore,
|
||||
bool DeleteVisible,
|
||||
bool RestoreVisible)
|
||||
bool RestoreVisible,
|
||||
/// <summary>
|
||||
/// Campaign CC slice CC7: retail's Create-character gate
|
||||
/// (<c>gmCharacterManagementUI::UpdateButtons @ 0x004ec240</c>,
|
||||
/// ~0x004ec319-0x004ec32e) — unconditional on selection, purely
|
||||
/// <c>_charSet.set_.m_num < _charSet.numAllowedCharacters_</c> (the
|
||||
/// live roster count against the allowed-slot ceiling). Mirrors
|
||||
/// <see cref="RuntimeCharacterSelectionSnapshot.RosterCount"/> <
|
||||
/// <see cref="RuntimeCharacterSelectionSnapshot.SlotCount"/>.
|
||||
/// </summary>
|
||||
bool CanCreate = false)
|
||||
{
|
||||
public static RuntimeCharacterSelectionButtons None { get; } =
|
||||
new(false, false, false, true, false);
|
||||
|
|
@ -848,13 +858,21 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
|||
|
||||
private RuntimeCharacterSelectionButtons BuildButtons(int selectedIndex)
|
||||
{
|
||||
// gmCharacterManagementUI::UpdateButtons @ 0x004ec240's Create gate
|
||||
// is unconditional on the delete/selection state below it — it only
|
||||
// ever compares the live roster count against the allowed-slot
|
||||
// ceiling (~0x004ec319-0x004ec32e:
|
||||
// `if (_charSet.set_.m_num < _charSet.numAllowedCharacters_)
|
||||
// SetState(1); else SetState(0xd);`).
|
||||
bool canCreate = _entries.Length < _slotCount;
|
||||
|
||||
if (_operation is RuntimeCharacterSelectionOperation.DeleteRequested
|
||||
or RuntimeCharacterSelectionOperation.DeleteAcknowledged)
|
||||
{
|
||||
return RuntimeCharacterSelectionButtons.None;
|
||||
return RuntimeCharacterSelectionButtons.None with { CanCreate = canCreate };
|
||||
}
|
||||
if (selectedIndex < 0)
|
||||
return RuntimeCharacterSelectionButtons.None;
|
||||
return RuntimeCharacterSelectionButtons.None with { CanCreate = canCreate };
|
||||
|
||||
RuntimeCharacterSelectionEntry selected = _entries[selectedIndex];
|
||||
if (selected.IsPendingDelete)
|
||||
|
|
@ -864,7 +882,8 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
|||
CanDelete: false,
|
||||
CanRestore: !_restoreResponseArmed,
|
||||
DeleteVisible: false,
|
||||
RestoreVisible: true);
|
||||
RestoreVisible: true,
|
||||
CanCreate: canCreate);
|
||||
}
|
||||
|
||||
return new RuntimeCharacterSelectionButtons(
|
||||
|
|
@ -872,7 +891,8 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
|||
CanDelete: selected.CanEnter,
|
||||
CanRestore: false,
|
||||
DeleteVisible: true,
|
||||
RestoreVisible: false);
|
||||
RestoreVisible: false,
|
||||
CanCreate: canCreate);
|
||||
}
|
||||
|
||||
private int FindDisplayIndex(uint characterId)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue