fix(runtime): Campaign CC slice CC3 review-fix round — F1-F16
Opus dual-lens review of CC3's RuntimeCharacterCreationState passed on retail fidelity but failed the controller integration: the post-create log-straight-in indexed the CACHED wire CharacterList, which ACE never resends after a create (it only appends server-side and replies Ok) — with zero pre-existing characters this throws, with N it can silently enter the WRONG character. The same stale-index problem corrupted every pre-existing character's delete slot on roster re-sort. Fixes all four blocking findings plus a credit-gate correctness bug (retail warns and lets the user confirm through unspent credits; it does not force a full spend) and eight lower-severity findings from the same review round. F1 (blocking): WorldSession gained a guid-based EnterWorld(uint,string, TimeSpan?) overload sharing EnterWorldCore with the index-based one; ILiveSessionOperations gained a default EnterWorldByGuid method. LiveSessionController factored EnterSelectedCore/the new EnterCreatedCharacterCore through a shared EnterHighlightedCore so the post-create enter sends by the exact guid the 0xF643 Ok reply carried, never by a roster index. F2 (blocking): RuntimeCharacterSelectionState gained a real AppendCreatedCharacter primitive that preserves every existing entry's ActiveIndex (a wire contract — SendDeleteCharacter sends it as the CharacterSet slot) and assigns the new entry's from the pre-create wire roster count, instead of round-tripping the post-create roster through ApplyRoster's name-sort-and-renumber. F3 (blocking): retail's DoFinish(this, arg2) gate is "arg2 != 0 && remainingAtrbCredits > 0" — the ordinary click warns and refuses, but the warning dialog's own confirm re-invokes DoFinish(this, 0), which sends anyway with credits unspent (ACE accepts this). TryBeginFinish/Finish gained a confirmedUnspentCredits parameter; the plan doc's "retail FORCES full spend" line is corrected in the same commit. F4 (blocking): a stale out-of-range template index surviving a heritage switch to a heritage with fewer templates now clears to TemplateUnset, matching ConstrainAllByHeritage's clamp. F5/F9/F10: three register-row/doc citation corrections (AP-207's real FitTemplateToCharacter call sites — a fourth one the original filing also missed; the Slot field's real retail assignment source; AP-209's classID branch table for Olthoi/OlthoiAcid). F6: ApplyCreationResponse no longer publishes from inside the owner lock. F7: two new tests pin BalanceAttributes' persistent donor cursor (successive-overspend advance, Self-to-Strength wrap). F8: ResetSkillLevels' doc corrected to retail's real both-costs->=0 gate. F11: the integration test fixture captures guid-based enter calls and uses two pre-existing characters whose wire order differs from alphabetical order, so the roster assertion actually exercises F2 instead of coinciding with it by accident. F12: filed register row AP-211 for the client-side RosterFull slot-cap refusal (no retail DoFinish-layer counterpart). F13: narrowed Finish's bare catch to InvalidOperationException/SocketException and bound _scope to a local. F15: RandomizeStartAreaLocked leaves the start area unchanged on an empty list instead of forcing -1, matching retail. Runtime 1706/0 (was 1701), Core.Net unchanged at 994/0, full solution Release build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9a84230c4f
commit
397ccd62cd
9 changed files with 578 additions and 68 deletions
|
|
@ -1149,12 +1149,54 @@ public sealed class WorldSession : IDisposable
|
|||
{
|
||||
if (Characters is null || Characters.Characters.Count == 0)
|
||||
throw new InvalidOperationException("Connect() must complete with a non-empty CharacterList");
|
||||
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(10));
|
||||
EnterWorldSelection selection = SelectCharacterForEnterWorld(
|
||||
Characters,
|
||||
characterIndex);
|
||||
CharacterList.Character chosen = selection.Character;
|
||||
_activeCharacterId = chosen.Id;
|
||||
EnterWorldCore(selection.Character.Id, selection.EnterWorldBody, timeout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send CharacterEnterWorldRequest and CharacterEnterWorld for the exact
|
||||
/// (guid, accountName) identity the caller supplies, bypassing the
|
||||
/// cached <see cref="Characters"/> roster entirely. Campaign CC slice
|
||||
/// CC3 review-fix round (F1): the index-based overload above assumes
|
||||
/// <paramref name="characterIndex"/> refers to a slot in
|
||||
/// <see cref="Characters"/> — true for ordinary character-select entry,
|
||||
/// but FALSE immediately after a character create. ACE never resends
|
||||
/// <see cref="CharacterList"/> post-create (it only appends server-side
|
||||
/// and replies with the <c>0xF643</c> Ok identity —
|
||||
/// <c>references/ACE/Source/ACE.Server/Network/Handlers/CharacterHandler.cs:170-172</c>),
|
||||
/// so entering the newly created character by a re-derived index can
|
||||
/// throw (zero pre-existing characters) or silently enter the WRONG
|
||||
/// character (N pre-existing characters, since the caller's display
|
||||
/// order need not match the wire order). Retail's own
|
||||
/// <c>CPlayerSystem::LogOnCharacter(gid)</c> is itself guid-based, so
|
||||
/// this is a more direct port of the same entry point — not a
|
||||
/// deviation from retail — for the one caller (enter-straight-in after
|
||||
/// create) that has an exact identity in hand and no reliable index.
|
||||
///
|
||||
/// <para>
|
||||
/// Retail's own fallback when the freshly created name never appears in
|
||||
/// its per-frame roster poll (<c>gmCharGenMainUI::Update @
|
||||
/// 0x004E8460</c>) bounces the UI back to character management
|
||||
/// (<c>QueueUIMode(0x1000000a) @ 0x004E85D7</c>). acdream has no
|
||||
/// analogous fallback here because this entry point is driven directly
|
||||
/// by the identity carried on the SAME reply that confirms the create
|
||||
/// succeeded — there is no polling step that could fail to find the
|
||||
/// name, so there is nothing for a fallback to catch.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void EnterWorld(uint characterGuid, string accountName, TimeSpan? timeout = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(accountName);
|
||||
byte[] enterWorldBody = CharacterEnterWorld.BuildEnterWorldBody(characterGuid, accountName);
|
||||
EnterWorldCore(characterGuid, enterWorldBody, timeout);
|
||||
}
|
||||
|
||||
private void EnterWorldCore(uint characterGuid, byte[] enterWorldBody, TimeSpan? timeout)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(10));
|
||||
_activeCharacterId = characterGuid;
|
||||
Transition(State.EnteringWorld);
|
||||
|
||||
SendGameMessage(CharacterEnterWorld.BuildEnterWorldRequestBody());
|
||||
|
|
@ -1220,7 +1262,7 @@ public sealed class WorldSession : IDisposable
|
|||
// CPlayerSystem::LogOnCharacter @ 0x0055F890 passes the account
|
||||
// populated by CharacterSet::UnPack, not the spelling supplied to the
|
||||
// login form. ACE validates this canonical account value.
|
||||
SendGameMessage(selection.EnterWorldBody);
|
||||
SendGameMessage(enterWorldBody);
|
||||
|
||||
// LoginComplete is emitted by the host only after the accepted local
|
||||
// Create has completed its canonical first placement. Sending it at
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue