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
|
|
@ -44,38 +44,40 @@ Copy this block when adding a new issue:
|
|||
|
||||
---
|
||||
|
||||
## #216 — Inventory drags into the 3-D world could only drop items
|
||||
## #217 — Character windows did not receive live 64-bit experience updates
|
||||
|
||||
**Status:** IN-PROGRESS
|
||||
**Status:** IN-PROGRESS — implementation complete; connected visual gate pending
|
||||
**Severity:** HIGH
|
||||
**Filed:** 2026-07-13
|
||||
**Component:** retained UI / inventory / interaction / net
|
||||
**Component:** retained UI / character sheet / net / player state
|
||||
|
||||
**Description:** Releasing a physical inventory item over an NPC always sent
|
||||
the ground-drop path. A starter-dungeon character therefore could not hand the
|
||||
exit token to the exit NPC and could not complete the dungeon.
|
||||
**Description:** The Attributes window always displayed Total Experience as 0
|
||||
and an empty level-progress meter after the player earned XP. The Skills footer
|
||||
also displayed Unassigned Experience as 0, preventing an accurate raise-cost
|
||||
decision. The Total Experience number was centered instead of right-aligned.
|
||||
|
||||
**Root cause / status:** Implementation complete; connected live gate pending.
|
||||
The exact `AttemptPlaceIn3D` policy and the `0x00CD`
|
||||
packet builder already existed, but `GameWindow.OnUiDragReleasedOutside`
|
||||
discarded the release coordinates and always called `DropToWorld`, hard-coding
|
||||
`TargetId=0`. The release path now world-picks at the actual release point,
|
||||
the controller executes its existing `GiveToTarget` action without optimistic
|
||||
mutation, `WorldSession` sends the exact request, and PlayerDescription drives
|
||||
the retail player secure-trade preference.
|
||||
**Root cause / status:** acdream parsed the neighboring private/public Int32
|
||||
quality messages (`0x02CD`/`0x02CE`) but omitted retail
|
||||
`PrivateUpdatePropertyInt64 (0x02CF)`. Both TotalExperience (quality 1) and
|
||||
AvailableExperience (quality 2) use that message. The exact parser/event/state
|
||||
path is now ported, both local-player projections are synchronized in one
|
||||
wiring owner, the existing retail XP-band formula receives live data, and only
|
||||
the value element `0x10000235` is explicitly right-aligned.
|
||||
|
||||
**Files:** `src/AcDream.App/UI/ItemInteractionController.cs`;
|
||||
`src/AcDream.App/Rendering/GameWindow.cs`;
|
||||
`src/AcDream.Core.Net/WorldSession.cs`.
|
||||
**Files:** `src/AcDream.Core.Net/Messages/PrivateUpdatePropertyInt64.cs`;
|
||||
`src/AcDream.Core.Net/WorldSession.cs`;
|
||||
`src/AcDream.Core.Net/ObjectTableWiring.cs`;
|
||||
`src/AcDream.Core/Player/LocalPlayerState.cs`;
|
||||
`src/AcDream.App/UI/Layout/CharacterStatController.cs`.
|
||||
|
||||
**Research:** `docs/research/2026-07-13-retail-give-item-pseudocode.md`;
|
||||
`ItemHolder::AttemptPlaceIn3D @ 0x00588600`;
|
||||
`ACCWeenieObject::UIAttemptGive @ 0x0058D620`;
|
||||
`CM_Inventory::Event_GiveObjectRequest @ 0x006ABB00`.
|
||||
**Research:** `docs/research/2026-07-13-retail-experience-update-pseudocode.md`;
|
||||
`CM_Qualities::DispatchUI_PrivateUpdateInt64 @ 0x006AEAD0`;
|
||||
`ClientObjMaintSystem::Handle_Qualities__PrivateUpdateInt64 @ 0x00559000`;
|
||||
`gmStatManagementUI::UpdateExperience @ 0x004F0A70`.
|
||||
|
||||
**Acceptance:** Drag the starter-dungeon exit token from the backpack onto the
|
||||
exit NPC. ACE accepts the token, the inventory removes/decrements it only from
|
||||
the authoritative server response, and the quest teleport exits the dungeon.
|
||||
**Acceptance:** Earn XP while logged in. Attributes immediately shows the new
|
||||
right-aligned total and red progress fill; Skills immediately shows the same
|
||||
server-authoritative unassigned XP available for raises.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -8225,6 +8227,18 @@ Fixed by the A7 lighting rework, specifically A7 Fix D (`0980bea4`, `cf627933`,
|
|||
**Status:** DONE (2026-07-09, investigated this session).
|
||||
The described mechanism (live entities collapsing to one root-centered cylinder) no longer exists. `RegisterLiveEntityCollision` (`GameWindow.cs:4244`) now builds a multi-shape list via `ShadowShapeBuilder.FromSetup` — one shape per CylSphere, Sphere, and BSP-bearing Part — and registers via `RegisterMultiPart` (A6.P4 Task 7, `ca9341c2`). The retail `CSphere` and `CCylSphere` collision families were ported verbatim in `78e57581` (sphere primitive), `6ab26989` (fix #172, CCylSphere family), and `96ae2740` (fix #182, CSphere family, retires TS-45). The remaining `setup.Radius` single-cylinder path is now a deliberately-audited fallback used only when no other shape data exists. `tests/AcDream.Core.Tests/Physics/ShadowShapeBuilderShapeSourceTests.cs` covers both a multi-shape object and a live-creature case.
|
||||
|
||||
---
|
||||
|
||||
## #216 — Inventory drags into the 3-D world could only drop items
|
||||
|
||||
**Status:** DONE (2026-07-13, `30d29450`, user-confirmed connected gate).
|
||||
Dragging the starter-dungeon exit token onto its NPC now world-picks at the
|
||||
actual release point, executes the retail `GiveToTarget` path without optimistic
|
||||
inventory mutation, receives ACE's authoritative removal, and completes the
|
||||
quest teleport out of the dungeon.
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Example:
|
||||
|
||||
|
|
|
|||
|
|
@ -504,6 +504,7 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar.
|
|||
- **M2 basic retail combat bar implemented 2026-07-11; corrective live visual gate pending.** Production mounts authored `gmCombatUI` LayoutDesc `0x21000073`, shows it only for Melee/Missile, and routes mouse plus keyboard through one `CombatAttackController`. Corrections include the authored wider centered dark-red child range (accepted IA-20), full-width left-to-right bright live attack-charge feedback, exact Speed-left/Power-right justification, silent control-only `AttackDone(ActionCancelled)`, target-frame Keep in View with manual orbit, and persistent corpse motion: the AP-80 velocity-only NPC adaptation can now replace only Ready/Walk/Run, never authoritative Dead/actions. `CombatTargetController` ports the selection-cleared AutoTarget consumer, so a selected creature's authoritative Dead motion clears it and selects the nearest eligible creature when enabled. The 2026-07-12 replacement-corpse correction unifies both multi-frame and static/reactive spawns behind retail's CreateObject lifecycle: apply the wire's Dead state while detached, then `MotionTableManager::HandleEnterWorld` strips Ready→Dead links before the first in-world tick; multiple corpses remaining fallen passed the live user gate that day. Follow-up #205 makes the toolbar read the final canonical selection after a reentrant Auto Target notice and restricts automatic candidates to hostile non-player monsters (intentional PK-edge divergence IA-19); that live gate also passed 2026-07-12. Research: `docs/research/2026-07-11-retail-combat-bar-pseudocode.md`, `docs/research/2026-07-11-combat-target-camera-pseudocode.md`, `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`; AP-24/AP-95 retired, AP-80 narrowed, AP-110 narrowed, AP-112 records the remaining attack-start and exact trained-Recklessness seams.
|
||||
- **Retail client command families implemented 2026-07-13; corrective live gate pending.** One shared typed catalog now separates retail client actions from ACE administrator commands for both chat backends. Named-decomp ports cover recall/house/PK travel; age/birth; framerate, lock, version, location, corpse, and die confirmation; clear plus named/automatic UI layouts; AFK/consent; emotes; friends; squelch/filter/message types; and fill-components. App owns execution, Core owns authoritative friends/squelch state, Core.Net owns exact UIQueue/ControlQueue packets. Confirmation reuse now ports retail `DialogFactory` contexts, queue groups/priority, fresh DAT roots, property results, callbacks/close notices, and server abort handling; `/die`, gameplay request tuples, and guarded item-use prompts share its type-1 LayoutDesc `0x2100003C` presenter. The first live gate passed the family except for raw suicide-success code `0x004A` and the title-bar-only FPS presentation; the correction maps retail's text and mounts SmartBox element `0x10000047` with live two-decimal `FPS`/`DEG`. #L.6 is closed; TS-31/TS-47 are narrowed. Research: `docs/research/2026-07-13-retail-client-command-families-pseudocode.md`, `docs/research/2026-07-13-retail-dialog-factory-pseudocode.md`.
|
||||
- **✓ SHIPPED — Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`, 2026-06-26, same branch). **Visually user-confirmed 2026-06-26 — Attributes tab reads as retail.** Three tabs, header (name/heritage/PK), large-gold level number (dat font, `largeDatFont` 18px), "Total Experience (XP):" + "XP for next level:" captions, 9-row attribute list (icons + right-aligned values + Health/Stamina/Mana vitals), click-to-select (top/bottom selection bars + footer State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered footer. User noted "still needs some polish for later" — deferred to Issue #158.
|
||||
- **✓ CORRECTIVE PORT — live character experience qualities** (2026-07-13, #217; connected gate pending). Added retail `PrivateUpdatePropertyInt64 (0x02CF)` so Total Experience, the level-progress meter, and Skills' Unassigned Experience refresh from the same authoritative server update; Total XP value alignment now matches the authored right-justified layout.
|
||||
- **✓ SHIPPED — D.5.4 — Client object/item data model (foundation).** Shipped 2026-06-18 (`b506f53`..`a33e897`, 11 commits). Renamed `ItemRepository`→`ClientObjectTable` / `ItemInstance`→`ClientObject`; broadened the table to hold EVERY server object (retail `weenie_object_table` shape). `CreateObject` is now the canonical merge-upsert (`ClientObjectTable.Ingest`, retail `SetWeenieDesc` semantics) via a new Core.Net `ObjectTableWiring` (off GameWindow); `DeleteObject` evicts; `PlayerDescription` is a membership manifest (`RecordMembership`); live container-membership index (`GetContents`, retail `object_inventory_table`). `_liveEntityInfoByGuid` retired (selection/describe resolve from the one table). Root fix: the old enrich-existing-only `EnrichItem` dropped `CreateObject`s for items with no `PlayerDescription` stub — live-Coldeve 4/6 hotbar slots blank; items are now created, not dropped. **Crux resolved:** retail is TWO tables (`object_table` + `weenie_object_table`), NOT one — acdream's `WorldEntity` (3D system) + `ClientObjectTable` (data/UI) split was already architecturally faithful; the fix was the ingestion path, not a table unification. 2671 tests green.
|
||||
- **Roadmap correction (2026-07-10):** the completion order is now the architecture-first campaign in `docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md`. Retail `gmToolbarUI` is object-only: preserve `ShortCutData.index_`, `objectID_`, and `spellID_`, but do not invent spell glyphs on this bar. `PlayerModule::favorite_spells_[8]` feeds separate spell bars.
|
||||
- **✓ D.5.3 — Toolbar selected-object display (issue #141) SHIPPED.** Exact health/player/pet/Free-PK policy, selected-item mana (`0x100001A2`), stack entry (`0x100001A3`), stack slider (`0x100001A4`), formatted count/name, shared split quantity, click-to-use, peace/war, health/name, and paperdoll self/upper-item selection all passed live by 2026-07-11. Exact raw `ShortCutData` preservation is already shipped in Wave 4.2; the eight `favorite_spells_` lists belong to the separate spellbook/spell-bar phase.
|
||||
|
|
|
|||
|
|
@ -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