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>
This commit is contained in:
parent
30d294506c
commit
84b7d2d7cd
15 changed files with 391 additions and 29 deletions
|
|
@ -0,0 +1,81 @@
|
|||
# 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
|
||||
|
||||
```text
|
||||
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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue