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:
Erik 2026-08-15 15:20:03 +02:00
parent 397ccd62cd
commit 3a6b7e3115
3 changed files with 108 additions and 4 deletions

View file

@ -433,6 +433,16 @@ public sealed class LiveSessionController
private bool _disposed;
private ulong _generation;
private RuntimeTeardownStage _lastTeardownStages;
/// <summary>
/// CC3 re-review R1: creates accepted since the last wire
/// CharacterList was applied. ACE appends each created character to
/// its own session list but never resends the list, so the cached
/// wire count under-counts by exactly this number — the equivalent
/// of retail's own CharacterSet growing via AddIdentity per create.
/// Guarded by <c>_gate</c> like every sibling field.
/// </summary>
private int _createsSinceCharacterList;
private LiveSessionCharacterSelection? _activeSelection;
private Action<WorldSession>? _autoSaveTickHook;
private Action<WorldSession>? _preLogoffFlushHook;
@ -745,6 +755,7 @@ public sealed class LiveSessionController
RuntimeGenerationToken activeGeneration = new(generation);
CharacterSelectionState.Reset(activeGeneration);
CharacterCreationState.Reset(activeGeneration);
_createsSinceCharacterList = 0;
try
{
DrainRetiredScope();
@ -818,6 +829,7 @@ public sealed class LiveSessionController
CharacterList.Parsed? characters = _operations.GetCharacters(session);
if (characters is not null)
{
_createsSinceCharacterList = 0;
LiveSessionRosterReport roster = BuildRosterReport(characters);
CharacterSelectionState.ApplyRoster(roster);
host.ReportRoster(roster);
@ -947,6 +959,7 @@ public sealed class LiveSessionController
{
if (!IsCurrent(scope, generation))
return;
_createsSinceCharacterList = 0;
LiveSessionRosterReport report = BuildRosterReport(roster);
CharacterSelectionState.ApplyRoster(report);
scope.Host.ReportRoster(report);
@ -1337,9 +1350,22 @@ public sealed class LiveSessionController
// THIS character by design, but its COUNT is still exactly the
// 0-based slot ACE assigned). Falls back to the display roster
// count only if the cached wire list is unexpectedly unavailable.
// CC3 re-review R1: the cached count is stale by the number of
// creates since the last CharacterList (ACE never resends one
// post-create), so a SECOND create in the same session must add
// the creates the cache hasn't seen — retail's own CharacterSet
// grows via AddIdentity per create, keeping GetSlot correct the
// same way. The counter applies ONLY to the cached-wire branch:
// the display-roster fallback already contains every prior
// create (AppendCreatedCharacter added them), so adding the
// counter there would double-count. Resets whenever a fresh wire
// CharacterList is applied and at generation reset.
int wireIndex =
_operations.GetCharacters(scope.Session)?.Characters.Count
?? before.RosterCount;
is int cachedWireCount
? cachedWireCount + _createsSinceCharacterList
: before.RosterCount;
_createsSinceCharacterList++;
CharacterSelectionState.AppendCreatedCharacter(
identity.Guid,
identity.Name,
@ -1662,6 +1688,7 @@ public sealed class LiveSessionController
_activeSelection = null;
CharacterSelectionState.Reset(new RuntimeGenerationToken(_generation));
CharacterCreationState.Reset(new RuntimeGenerationToken(_generation));
_createsSinceCharacterList = 0;
if (_scope is { } scope)
{
_scope = null;