acdream/docs/research/2026-07-13-retail-experience-update-pseudocode.md
Erik 84b7d2d7cd fix(ui): port live experience quality updates (#217)
Parse retail PrivateUpdatePropertyInt64 and route authoritative Total/Available Experience through both local-player projections so Attributes, the level meter, and Skills refresh together. Preserve the existing retail XP curve and right-align the Total XP value.

Close the user-confirmed item-give gate for #216 and record the named-retail/ACE/holtburger conformance evidence.

Co-Authored-By: Codex <noreply@openai.com>
2026-07-13 20:43:48 +02:00

3.4 KiB

Retail experience-update pipeline pseudocode

Date: 2026-07-13 Scope: live Total Experience, Unassigned Experience, and character-level XP meter

Retail oracle

  • CM_Qualities::DispatchUI_PrivateUpdateInt64 @ 0x006AEAD0 validates opcode 0x02CF, then reads sequence:u8, property:u32, and value:i64 from offsets 4, 5, and 9.
  • ClientObjMaintSystem::Handle_Qualities__PrivateUpdateInt64 @ 0x00559000 applies the property to the local player's qualities through the common UpdateStat<Int64_QualityType, int64_t> path.
  • gmStatManagementUI::PostInit @ 0x004EFD90 registers the panel for player StatType::Int64 qualities 1 (TotalExperience), 2 (AvailableExperience), 6 (AvailableLuminance), and 7 (MaximumLuminance).
  • gmStatManagementUI::Update @ 0x004F0020 calls UpdateExperience whenever the registered player qualities refresh.
  • gmStatManagementUI::UpdateExperience @ 0x004F0A70 reads Int64 quality 1, reads Int quality 0x19 (level), derives the current and next cumulative-XP thresholds, writes the total-XP and XP-to-next text, and sets meter attribute 0x69 to (totalXp - currentThreshold) / (nextThreshold - currentThreshold).

The named top-level dispatch table routes 0x02CD to private Int32, 0x02CE to public Int32, 0x02CF to private Int64, and 0x02D0 to public Int64. acdream already handled the first two and was missing 0x02CF.

Cross-reference

  • ACE GameMessagePrivateUpdatePropertyInt64 writes the same 17-byte body: opcode, one-byte sequence, property id, signed 64-bit value.
  • ACE marks PropertyInt64.TotalExperience (1) and PropertyInt64.AvailableExperience (2) SendOnLogin, and GameEventPlayerDescription includes both in the login Int64 table.
  • holtburger crates/holtburger-protocol/src/messages/object/messages/properties.rs defines PrivateUpdatePropertyInt64Data = UpdatePropertyInt64<false>; its generic private-property unpacker reads u8 sequence, no guid, u32 property, then i64 value.

Faithful pseudocode

on top-level game message(body):
    if body.opcode == 0x02CF:
        require body.length >= 17
        sequence = body.u8_at(4)       // transport ordering byte; latest value wins
        property = body.u32_at(5)
        value    = body.i64_at(9)

        player.qualities.int64[property] = value
        notify registered quality observers

on character-stat panel update(player.qualities):
    totalXp = qualities.int64[1]
    level   = qualities.int32[0x19]
    current = ExperienceToLevel(level)
    next    = ExperienceToLevel(level + 1)

    totalXpText = format(totalXp)
    xpToNextText = format(max(0, next - totalXp))
    xpMeter = next > current
        ? clamp((totalXp - current) / (next - current), 0, 1)
        : 0

on skill panel update(player.qualities):
    unassignedExperienceText = format(qualities.int64[2])

Port shape

  1. Parse 0x02CF as an exact typed top-level message.
  2. Publish a typed PlayerInt64PropertyUpdated event from WorldSession.
  3. Apply the update to both existing local-player projections (ClientObjectTable and LocalPlayerState) so every retained UI consumer observes the same authoritative server value.
  4. Keep the existing ExperienceTable calculation; it already matches UpdateExperience and was only starved of live data.
  5. Preserve the imported retail layout and explicitly right-align only the Total Experience value element (0x10000235); its caption remains left aligned.