fix(runtime)+docs: CC3 re-review CLOSED — R1 second-create wire slot, risk-8 measured LATENT
The CC3 narrow re-review returned CLOSED (both lenses PASS, merge recommended) with residual R1: the post-create wire-slot assignment read the cached wire CharacterList count, which ACE never refreshes after a create — correct for the first create, off by one for a second create in the same session (reachable via create Ok -> server-rejected guid enter -> ReturnToSelection -> create again), the same wire-contract failure class F2 fixed. Root fix now rather than carried: a creates-since-CharacterList counter (the equivalent of retail's own CharacterSet growing via AddIdentity per create), reset on every fresh wire CharacterList apply and at generation reset, applied only to the cached-wire branch since the display-roster fallback already contains prior appends. Regression test drives the full create->Ok->rejected-enter->create-again flow and pins wire slots 0/1/2/3. Docs: CC3 ledger row flipped to REVIEW-CLOSED with real shas (re-review R2); CC7 risk item 8 downgraded to LATENT with measured installed-DAT data (user-prompted): every heritage's single cost override is Arcane Lore at NormalCost=0/PrimaryCost=2 vs global 4/6, so ACE's over-deduction (= NormalCost = 0) cannot fire with end-of-retail data — the earlier "may be rejected" claim was inferred from code without measuring. Runtime 1707/0 Release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
397ccd62cd
commit
3a6b7e3115
3 changed files with 108 additions and 4 deletions
|
|
@ -92,11 +92,25 @@ public sealed class LiveSessionControllerCharacterCreationTests
|
|||
public void EnterWorld(WorldSession session, int activeCharacterIndex) =>
|
||||
EnterWorldCount++;
|
||||
|
||||
/// <summary>R1: when positive, the next guid-enter throws retail's
|
||||
/// server-rejection shape (the transport-valid path that returns
|
||||
/// the controller to selection), decrementing per call — lets a
|
||||
/// test reach the create-again-after-rejected-enter flow.</summary>
|
||||
public int EnterWorldByGuidRejectionsRemaining { get; set; }
|
||||
|
||||
public void EnterWorldByGuid(
|
||||
WorldSession session,
|
||||
uint characterGuid,
|
||||
string accountName) =>
|
||||
string accountName)
|
||||
{
|
||||
EnterWorldByGuidCalls.Add((characterGuid, accountName));
|
||||
if (EnterWorldByGuidRejectionsRemaining > 0)
|
||||
{
|
||||
EnterWorldByGuidRejectionsRemaining--;
|
||||
throw new CharacterSelectionRejectedException(
|
||||
new CharacterError.Parsed(0x0000000Bu));
|
||||
}
|
||||
}
|
||||
|
||||
public void Tick(WorldSession session) { }
|
||||
|
||||
|
|
@ -252,6 +266,57 @@ public sealed class LiveSessionControllerCharacterCreationTests
|
|||
Assert.Equal(0x50001234u, host.EnteredWorld[0].CharacterId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CC3 re-review R1: a SECOND create in the same session must get wire
|
||||
/// slot N+1, not N. ACE never resends CharacterList post-create, so the
|
||||
/// cached wire count alone under-counts by the creates it hasn't seen;
|
||||
/// the controller's creates-since-list counter (reset on every fresh
|
||||
/// wire CharacterList) supplies the difference — the equivalent of
|
||||
/// retail's own CharacterSet growing via AddIdentity per create. The
|
||||
/// create-again path is reached exactly as the re-review described:
|
||||
/// first create Ok, guid-enter rejected by the server
|
||||
/// (CharacterSelectionRejectedException → ReturnToSelection), then a
|
||||
/// second create.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SecondCreate_AfterRejectedEnter_GetsTheNextWireSlot()
|
||||
{
|
||||
(LiveSessionController controller, TestOperations operations, TestHost host, RuntimeGenerationToken generation) =
|
||||
StartAwaitingSelection();
|
||||
BuildReadyCharacter(controller, generation);
|
||||
WorldSession session = operations.Sessions[0];
|
||||
session.GameMessageCapture = (_, _) => { };
|
||||
operations.EnterWorldByGuidRejectionsRemaining = 1;
|
||||
|
||||
Assert.True(controller.Finish(generation).Accepted);
|
||||
InvokeProcessDatagram(session, BuildResponsePacket(
|
||||
(uint)CharGenVerificationResponse.Code.Ok, 0x50001234u, "NewChar"));
|
||||
|
||||
// The rejected enter left us back at selection with the first
|
||||
// created character appended at the true wire slot 2.
|
||||
Assert.False(controller.IsInWorld);
|
||||
Assert.Single(operations.EnterWorldByGuidCalls);
|
||||
Assert.True(controller.CharacterSelectionState.View.TryGet(0x50001234u, out RuntimeCharacterSelectionEntry firstCreated));
|
||||
Assert.Equal(2, firstCreated.ActiveIndex);
|
||||
|
||||
// Second create in the same session: ACE's own list now holds
|
||||
// Zed(0), Amy(1), NewChar(2) — the cached wire list still only
|
||||
// holds Zed and Amy. The second character's slot must be 3.
|
||||
Assert.True(controller.SetName(generation, "SecondChar").Accepted);
|
||||
Assert.True(controller.Finish(generation).Accepted);
|
||||
InvokeProcessDatagram(session, BuildResponsePacket(
|
||||
(uint)CharGenVerificationResponse.Code.Ok, 0x50005678u, "SecondChar"));
|
||||
|
||||
Assert.True(controller.CharacterSelectionState.View.TryGet(0x50005678u, out RuntimeCharacterSelectionEntry secondCreated));
|
||||
Assert.Equal(3, secondCreated.ActiveIndex);
|
||||
// Pre-existing wire indices still intact after both appends.
|
||||
Assert.True(controller.CharacterSelectionState.View.TryGet(0x50000002u, out RuntimeCharacterSelectionEntry zed));
|
||||
Assert.Equal(0, zed.ActiveIndex);
|
||||
Assert.True(controller.CharacterSelectionState.View.TryGet(0x50000003u, out RuntimeCharacterSelectionEntry amy));
|
||||
Assert.Equal(1, amy.ActiveIndex);
|
||||
Assert.Equal(2, host.Created.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Finish_ThenNameInUseResponse_SurfacesRejectionAndStaysAwaitingSelection()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue