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
|
|
@ -61,8 +61,12 @@ public sealed class CharacterManagementUiControllerTests
|
|||
CharacterManagementUiController.RestoreElementId);
|
||||
|
||||
Assert.True(create.Visible);
|
||||
Assert.False(create.Enabled);
|
||||
Assert.Null(create.OnClick);
|
||||
// Campaign CC slice CC7: gmCharacterManagementUI::UpdateButtons @
|
||||
// 0x004ec240's Create gate — 3 characters against SlotCount 5.
|
||||
Assert.True(create.Enabled);
|
||||
Assert.NotNull(create.OnClick);
|
||||
create.OnClick!();
|
||||
Assert.Equal(1, environment.Runtime.RequestCreateCalls);
|
||||
Assert.True(enter.Enabled);
|
||||
Assert.True(delete.Visible);
|
||||
Assert.True(delete.Enabled);
|
||||
|
|
@ -99,6 +103,45 @@ public sealed class CharacterManagementUiControllerTests
|
|||
Assert.True(restore.Enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign CC slice CC7: <c>gmCharacterManagementUI::UpdateButtons @
|
||||
/// 0x004ec240</c>'s Create gate (~0x004ec319-0x004ec32e) is purely
|
||||
/// <c>_charSet.set_.m_num < _charSet.numAllowedCharacters_</c> — a
|
||||
/// full roster (roster count == the allowed-slot ceiling) ghosts Create
|
||||
/// exactly like retail, and refilling below the ceiling un-ghosts it
|
||||
/// again on the next Tick.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateButton_GhostsWhenRosterReachesTheSlotCeiling_AndUnGhostsBelowIt()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
CharacterManagementUiController controller = environment.Controller;
|
||||
UiButton create = environment.Button(
|
||||
CharacterManagementUiController.CreateElementId);
|
||||
|
||||
// The fixture's SlotCount is 5 — five characters exactly fills it.
|
||||
RuntimeCharacterSelectionEntry[] full = Enumerable.Range(0, 5)
|
||||
.Select(index => new RuntimeCharacterSelectionEntry(
|
||||
index,
|
||||
(uint)(0x50000200 + index),
|
||||
$"Full {index:D2}",
|
||||
0u))
|
||||
.ToArray();
|
||||
environment.Runtime.ReplaceRoster(full, highlightedCharacterId: full[0].CharacterId);
|
||||
controller.Tick();
|
||||
|
||||
Assert.True(create.Visible);
|
||||
Assert.False(create.Enabled);
|
||||
|
||||
RuntimeCharacterSelectionEntry[] belowCeiling = full[..4];
|
||||
environment.Runtime.ReplaceRoster(
|
||||
belowCeiling,
|
||||
highlightedCharacterId: belowCeiling[0].CharacterId);
|
||||
controller.Tick();
|
||||
|
||||
Assert.True(create.Enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA gate round 2 finding 3: retail's UpdateWorldName@0x004ec120
|
||||
/// / RecvNotice_WorldName@0x004ec360 both push Client::GetWorldName()
|
||||
|
|
@ -906,7 +949,8 @@ public sealed class CharacterManagementUiControllerTests
|
|||
ConfirmDelete,
|
||||
Restore,
|
||||
Cancel,
|
||||
RequestExit);
|
||||
RequestExit,
|
||||
RequestCreate);
|
||||
}
|
||||
|
||||
public FakeView View { get; } = new();
|
||||
|
|
@ -918,6 +962,14 @@ public sealed class CharacterManagementUiControllerTests
|
|||
public int CancelCalls { get; private set; }
|
||||
public int RestoreCalls { get; private set; }
|
||||
public int RequestExitCalls { get; private set; }
|
||||
public int RequestCreateCalls { get; private set; }
|
||||
|
||||
/// <summary>Campaign CC slice CC7: the fixture's fixed allowed-slot
|
||||
/// ceiling — mirrors <c>Snapshot</c>'s own hard-coded
|
||||
/// <c>SlotCount: 5</c> so <see cref="ButtonsFor"/> computes the SAME
|
||||
/// roster-vs-slot gate the real <c>RuntimeCharacterSelectionState.BuildButtons</c>
|
||||
/// does, instead of a fixture-only shortcut.</summary>
|
||||
private const int SlotCount = 5;
|
||||
public RuntimeCommandStatus RestoreStatus { get; set; } =
|
||||
RuntimeCommandStatus.Accepted;
|
||||
public bool ThrowOnRestore { get; set; }
|
||||
|
|
@ -929,7 +981,8 @@ public sealed class CharacterManagementUiControllerTests
|
|||
RuntimeCharacterSelectionButtons buttons = operation is
|
||||
RuntimeCharacterSelectionOperation.DeleteRequested
|
||||
or RuntimeCharacterSelectionOperation.DeleteAcknowledged
|
||||
? RuntimeCharacterSelectionButtons.None
|
||||
? RuntimeCharacterSelectionButtons.None with
|
||||
{ CanCreate = View.Entries.Length < SlotCount }
|
||||
: ButtonsFor(View.Snapshot.HighlightedCharacterId);
|
||||
Update(snapshot => snapshot with
|
||||
{
|
||||
|
|
@ -1022,7 +1075,8 @@ public sealed class CharacterManagementUiControllerTests
|
|||
{
|
||||
PendingDeleteCharacterId = 0u,
|
||||
Operation = RuntimeCharacterSelectionOperation.DeleteRequested,
|
||||
Buttons = RuntimeCharacterSelectionButtons.None,
|
||||
Buttons = RuntimeCharacterSelectionButtons.None with
|
||||
{ CanCreate = View.Entries.Length < SlotCount },
|
||||
});
|
||||
return Result(RuntimeCommandStatus.Accepted, id);
|
||||
}
|
||||
|
|
@ -1045,7 +1099,8 @@ public sealed class CharacterManagementUiControllerTests
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
true),
|
||||
true,
|
||||
View.Entries.Length < SlotCount),
|
||||
});
|
||||
AfterRestoreProjection?.Invoke();
|
||||
return Result(RuntimeCommandStatus.Accepted, id);
|
||||
|
|
@ -1065,13 +1120,22 @@ public sealed class CharacterManagementUiControllerTests
|
|||
|
||||
private void RequestExit() => RequestExitCalls++;
|
||||
|
||||
private void RequestCreate() => RequestCreateCalls++;
|
||||
|
||||
/// <summary>Campaign CC slice CC7: mirrors
|
||||
/// <c>RuntimeCharacterSelectionState.BuildButtons</c>'s own
|
||||
/// unconditional <c>CanCreate</c> computation — roster length
|
||||
/// against <see cref="SlotCount"/> — so every branch below carries
|
||||
/// the SAME real gate the production state machine does, not a
|
||||
/// fixture-only shortcut.</summary>
|
||||
private RuntimeCharacterSelectionButtons ButtonsFor(uint characterId)
|
||||
{
|
||||
bool canCreate = View.Entries.Length < SlotCount;
|
||||
RuntimeCharacterSelectionEntry? selected = View.Entries
|
||||
.Cast<RuntimeCharacterSelectionEntry?>()
|
||||
.FirstOrDefault(entry => entry?.CharacterId == characterId);
|
||||
if (selected is null)
|
||||
return RuntimeCharacterSelectionButtons.None;
|
||||
return RuntimeCharacterSelectionButtons.None with { CanCreate = canCreate };
|
||||
if (selected.Value.IsPendingDelete)
|
||||
{
|
||||
return new RuntimeCharacterSelectionButtons(
|
||||
|
|
@ -1079,14 +1143,16 @@ public sealed class CharacterManagementUiControllerTests
|
|||
false,
|
||||
true,
|
||||
false,
|
||||
true);
|
||||
true,
|
||||
canCreate);
|
||||
}
|
||||
return new RuntimeCharacterSelectionButtons(
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false);
|
||||
false,
|
||||
canCreate);
|
||||
}
|
||||
|
||||
private void Update(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue