feat(net) Campaign CA CA2 #431: parse the inbound attribute/skill update family
The server's authoritative answers to a raise were dropped on the floor: only the vitals pair (0x02E7/0x02E9) had parsers, so after any RaiseAttribute/RaiseSkill/TrainSkill the client's stat model stayed frozen at login's PlayerDescription — the root cause of #431's stale derived skills and run speed. The GUI looked alive only because the panel applies optimistic local raises. New parsers with three-source-verified layouts (CA1 research doc §2.5/ §2.8): PrivateUpdateAttribute (0x02E3) and PrivateUpdateSkill (0x02DD — the wire's ushort ranks + hardcoded adjustPP=1 pair and f64 lastUsedTime preserved exactly). WorldSession dispatches both as typed events; LiveSessionEventRouter routes them into the J4 character owner's LocalPlayerState like every other private update. The vestigial PrivateUpdateSkillLevel (0x02DF) is deliberately unparsed — ACE has no producer (verified). OnAttributeUpdate now fans out to the derived-value observers, mirroring retail's live-at-inquiry model (CACQualities::InqSkill 0x00592660 — Set* writes raw, Inq* recomputes, notification carries no value): an Endurance write notifies the Health AND Stamina vital observers (ACE pushes only a Health record and its own comment says the client must refresh both), Self notifies Mana, and every attribute write notifies character-sheet consumers whose formula contributions just changed. OnSkillWireUpdate preserves the login FormulaBonus — the wire record carries no attribute contribution; CA3 replaces the cached field with the live computation. Also corrected while in the neighborhood: PropertyString.cs's comment claimed opcode 0x02DD for PrivateUpdatePropertyString; ACE's enum says 0x02D5/0x02D6 (doc-only — nothing dispatched on either). Conformance tests cover both layouts (including holtburger's golden skill fixture with adjustPP=1), truncation/wrong-opcode rejection, the Endurance/Self/Quickness fan-out contract, and FormulaBonus preservation. Full hermetic suite 15,333 passed / 0 failed (one load-sensitive transport flake observed on the first run, passed alone and on the clean re-run — filed as #439 rather than chased). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1fc64984c9
commit
65430d4c7c
9 changed files with 474 additions and 4 deletions
|
|
@ -444,15 +444,43 @@ public sealed class LocalPlayerState
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a primary-attribute update from PlayerDescription's
|
||||
/// attribute block (ids 1..=6). Vital ids (7..=9) here are silently
|
||||
/// dropped — feed them through <see cref="OnVitalUpdate"/> instead.
|
||||
/// Apply a primary-attribute update — from PlayerDescription's
|
||||
/// attribute block at login, or (Campaign CA CA2, #431) from the live
|
||||
/// <c>PrivateUpdateAttribute (0x02E3)</c> record after a raise. Vital
|
||||
/// ids (7..=9) here are silently dropped — feed them through
|
||||
/// <see cref="OnVitalUpdate"/> instead.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Retail computes every derived value LIVE at inquiry
|
||||
/// (<c>CACQualities::InqSkill @ 0x00592660</c>,
|
||||
/// <c>InqAttribute2nd</c>), so applying the raw write is the whole
|
||||
/// recompute — what remains is telling the observers. Retail's
|
||||
/// notification carries no value; widgets re-pull
|
||||
/// (<c>QualityRegistrar</c> pattern). Vitals maxima derive from
|
||||
/// Endurance (health, stamina) and Self (mana), so those raises fan
|
||||
/// out to the vital observers too — note ACE pushes a full Health
|
||||
/// record after an Endurance raise but NOT a Stamina one, with the
|
||||
/// explicit comment that the client is expected to refresh both; this
|
||||
/// local fan-out is that expectation (research doc §4.1).
|
||||
/// </remarks>
|
||||
public void OnAttributeUpdate(uint atType, uint ranks, uint start, uint xp)
|
||||
{
|
||||
if (AttributeIdToKind(atType) is not AttributeKind kind) return;
|
||||
_attrs[kind] = new AttributeSnapshot(ranks, start, xp);
|
||||
AttributeChanged?.Invoke(kind);
|
||||
switch (kind)
|
||||
{
|
||||
case AttributeKind.Endurance:
|
||||
Changed?.Invoke(VitalKind.Health);
|
||||
Changed?.Invoke(VitalKind.Stamina);
|
||||
break;
|
||||
case AttributeKind.Self:
|
||||
Changed?.Invoke(VitalKind.Mana);
|
||||
break;
|
||||
}
|
||||
// Skill formula contributions derive from attribute currents —
|
||||
// character-sheet consumers re-pull.
|
||||
CharacterChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Replace the local player's top-level property snapshot from PlayerDescription.</summary>
|
||||
|
|
@ -490,6 +518,33 @@ public sealed class LocalPlayerState
|
|||
CharacterChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign CA CA2 (#431): apply the live
|
||||
/// <c>PrivateUpdateSkill (0x02DD)</c> record — the authoritative skill
|
||||
/// state after raise / train / specialize / untrain / reset. The wire
|
||||
/// record carries no attribute-formula contribution (retail computes it
|
||||
/// live at inquiry), so the existing snapshot's <c>FormulaBonus</c> is
|
||||
/// preserved — a skill update never changes attributes. A skill unseen
|
||||
/// at login (fresh train) starts at 0 until the CA3 live computation
|
||||
/// replaces the cached field entirely.
|
||||
/// </summary>
|
||||
public void OnSkillWireUpdate(
|
||||
uint skillId,
|
||||
uint ranks,
|
||||
uint status,
|
||||
uint xp,
|
||||
uint init,
|
||||
uint resistance,
|
||||
double lastUsed)
|
||||
{
|
||||
uint formulaBonus = _skills.TryGetValue(skillId, out var prev)
|
||||
? prev.FormulaBonus
|
||||
: 0u;
|
||||
_skills[skillId] = new SkillSnapshot(
|
||||
skillId, ranks, status, xp, init, resistance, lastUsed, formulaBonus);
|
||||
CharacterChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimistically apply a successful local attribute-raise action.
|
||||
/// The next server snapshot remains authoritative; this keeps UI state current
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace AcDream.Core.Properties;
|
|||
|
||||
/// <summary>
|
||||
/// AC's <c>PropertyString</c> property table — the numeric keys the server sends in
|
||||
/// <c>PrivateUpdatePropertyString (0x02DD) / PublicUpdatePropertyString (0x02DE)</c>
|
||||
/// <c>PrivateUpdatePropertyString (0x02D5) / PublicUpdatePropertyString (0x02D6)</c>
|
||||
/// and in the property bundles carried by <c>CreateObject</c> / <c>PlayerDescription</c> /
|
||||
/// <c>IdentifyResponse</c>. The CLR payload for this table is <c>string</c>.
|
||||
///
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue