fix(ui): port exact selected health policy
Replace the toolbar's PWD-bit approximation with retail ObjectIsAttackable composed with the exact player and pet short-circuits. Carry second-header PetOwner through CreateObject, session, and ClientObject state so self, pet, and Free-PK cases match retail while friendly NPCs and attackable non-creatures remain name-only. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
52a80dc531
commit
2644d1d527
18 changed files with 416 additions and 50 deletions
|
|
@ -2050,7 +2050,7 @@ See divergence register **AP-59**.
|
|||
|
||||
## #141 — Toolbar interactivity — selected-object display
|
||||
|
||||
**Status:** IN PROGRESS (health/name/flash DONE and visually confirmed 2026-06-20; formatted stack count, entry/slider, partial stack transfers, and selected-item mana visually confirmed 2026-07-11; exact PK/pet health policy remains). Renumbered from #140 on the 2026-06-20 main merge — A7 Fix D held #140 on main; this branch's commits/spec still reference #140.
|
||||
**Status:** IN PROGRESS (all implementation complete; health/name/flash confirmed 2026-06-20; stack controls/transfers and selected-item mana confirmed 2026-07-11; exact self/pet/Free-PK health-policy live gate pending). Renumbered from #140 on the 2026-06-20 main merge — A7 Fix D held #140 on main; this branch's commits/spec still reference #140.
|
||||
**Severity:** MEDIUM
|
||||
**Filed:** 2026-06-17
|
||||
**Component:** ui — D.5 toolbar / selection
|
||||
|
|
@ -2058,10 +2058,11 @@ See divergence register **AP-59**.
|
|||
**Description:** The action bar (D.5.1) is the retail "selected object" display. Wire the B.4 WorldPicker/selection state to the toolbar's currently-hidden elements: the two meters 0x100001A1 (selected-object Health) / 0x100001A2 (selected-object Mana) + the stack slider 0x100001A4 + the object-name line, so the bar shows what the player has selected in the world. Click-to-use + the peace/war stance indicator already shipped in D.5.1. Promote to roadmap D.5.3 (already listed there).
|
||||
|
||||
**Root cause / status:** The selection-state wire was deferred out of D.5.1 scope; the meter/slider elements are present in LayoutDesc 0x21000016 but hidden (no backing data). D.5.3 is the planned port.
|
||||
- **D.5.3a (2026-06-18):** the Health meter (0x100001A1) + the object-name line (0x1000019F) + the overlay state (0x100001A0) are wired via `SelectedObjectController` (port of `gmToolbarUI::HandleSelectionChanged`); `SelectionChanged` event on `GameWindow`; `QueryHealth (0x01BF)` sent on select. Spec/plan: `docs/superpowers/specs|plans/2026-06-18-d53a-*`. Mana was deferred from this slice and lands in D.5.3c below. Divergence row AP-46 remains for exact health-target policy.
|
||||
- **D.5.3a (2026-06-18):** the Health meter (0x100001A1) + the object-name line (0x1000019F) + the overlay state (0x100001A0) are wired via `SelectedObjectController` (port of `gmToolbarUI::HandleSelectionChanged`); `SelectionChanged` event on `GameWindow`; `QueryHealth (0x01BF)` sent on select. Spec/plan: `docs/superpowers/specs|plans/2026-06-18-d53a-*`. Mana was deferred from this slice and lands in D.5.3c below.
|
||||
- **D.5.3a visual gate PASSED (2026-06-20):** name top-aligned in the bar sprite's black band, friendly NPCs/Doors name-only, players/monsters get the bar (gated on PWD BF_ATTACKABLE/BF_PLAYER), bar appears on assess/damage (UpdateHealth-driven, AP-47 retired), brief green selection flash. Fixed during the gate: the two magenta end-lines (UiMeter.DrawHBar resolved slice id 0 → 1x1 magenta placeholder → 1px caps), the stack-entry black box (hid 0x100001A3), and the flash being eaten by a framebuffer-dump diagnostic. Commits `8f627cc` (fixes), `0796585` (CLI apparatus). **Remaining for #141:** Mana meter (0x100001A2).
|
||||
- **D.5.3b implementation (2026-07-11):** stacked selection formats the retail `"%d %hs"` count/name, now preserving the wire `PluralName` selected by `NAME_APPROPRIATE`; reveals authored entry `0x100001A3` and horizontal DAT slider `0x100001A4`; initializes to the full stack; clamps entry edits; honors the entry's DAT right alignment; and uses the exact 1000-step slider conversion. The thumb retains its raw pointer position so minimum/maximum reach both endpoints. One Core `StackSplitQuantityState` feeds the controls plus selected-source merge, container split (`0x0055`), and ground split (`0x0056`). `GetObjectSplitSize @ 0x00586F00` ensures unselected objects still move their full stack. Partial transfers do not optimistically move the original because ACE creates the destination object with a new guid. Warning-free App Release + 4,697 tests; controls/polish, partial inventory split, and partial ground drop visually confirmed.
|
||||
- **D.5.3c implementation + live gate (2026-07-11):** owned non-stack selections send retail `QueryItemMana (0x0263)`; `QueryItemManaResponse (0x0264)` now parses the trailing validity flag and flows through Core `ItemManaState`; valid matching responses reveal/fill authored meter `0x100001A2`, invalid responses cancel with guid zero, and changing selection cancels any visible mana or health query before hiding its meter. Stacked items stay exclusively on the split-control path. Live confirmation used an empty Mana Stone on a source magic item: the source was destroyed, its mana transferred into armor, and the selected armor's bar updated immediately from the server response. Named anchors: `CM_Item::Event_QueryItemMana @ 0x006A8610`, `DispatchUI_QueryItemManaResponse @ 0x006A84D0`, `gmToolbarUI::RecvNotice_UpdateItemMana @ 0x004BD0C0`.
|
||||
- **D.5.3d implementation (2026-07-11; live gate pending):** pure Core `SelectedObjectHealthPolicy` ports `ClientCombatSystem::ObjectIsAttackable @ 0x0056A600` and the toolbar's outer player/pet short-circuit. CreateObject now parses second-header `PetOwner (0x8)` after MaterialType/Cooldown fields and carries it through `WorldSession`/`WeenieData` into `ClientObject`. This restores self, pet, and Free-PK creature queries while friendly NPCs and attackable doors remain name-only. AP-46 retired. Research: `docs/research/2026-07-11-retail-selected-health-policy-pseudocode.md`.
|
||||
|
||||
**Files:** `src/AcDream.App/UI/Layout/ToolbarController.cs` + the selection/WorldPicker state (see `claude-memory/project_interaction_pipeline.md`).
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
|
||||
---
|
||||
|
||||
## 3. Documented approximation (AP) — 98 active rows
|
||||
## 3. Documented approximation (AP) — 97 active rows
|
||||
|
||||
Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84
|
||||
collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered
|
||||
|
|
@ -152,7 +152,6 @@ AP-94..AP-110 for the confirmed retail-UI completion gaps.
|
|||
| AP-41 | Scrollbar thumb 3-slice cap fallback only: single-tile draw (`0x06004C63`) used only when `ThumbTopSprite`/`ThumbBotSprite` are unset; the chat controller passes all three cap ids so the 3-slice path is drawn in practice | `src/AcDream.App/UI/UiScrollbar.cs:35` | The fallback single-tile path is unreachable when caps are bound (chat controller always sets them); the 3-slice path is the active code path | Only if a future caller omits the cap ids will the fallback fire — no visual regression in the chat window | `UIElement_Scrollbar::UpdateLayout @0x4710d0`; cap sprites `0x06004C60` (top) + `0x06004C66` (bottom) from base layout `0x2100003E` |
|
||||
| AP-42 | `UiMenu` item model is flat (label + opaque payload, single-level popup); retail `UIElement_Menu::MakePopup @0x46d310` supports hierarchical nested submenus via recursive popup chain | `src/AcDream.App/UI/UiMenu.cs` | The chat talk-focus menu is single-level (14 rows, 2 columns, no submenu); hierarchy is latent and unreachable through the chat window — no behavioral difference in the current usage | A future menu with nested submenus would render flat (only the top-level items drawn, no drill-down) | `UIElement_Menu::MakePopup` @0x46d310 |
|
||||
| AP-45 | `PublicUpdatePropertyInt (0x02CE)` sequence byte parsed-past but not honored; last update wins (no freshness check against sequence number) | `src/AcDream.Core.Net/Messages/PublicUpdatePropertyInt.cs` | Loopback ACE rarely reorders; latest-wins matches `PrivateUpdateVital`/`UpdatePosition`'s existing non-sequence behavior. Sequence tracking added when needed alongside TS-26. | A reordered 0x02CE on a real network could apply a stale UiEffects value — item icon temporarily shows the wrong effect state, corrected on next update | `PublicUpdatePropertyInt` sequence byte (ACE GameMessagePublicUpdatePropertyInt) |
|
||||
| AP-46 | Health-meter gate approximation: retail shows the health meter for `IsPlayer() || pet_owner || ClientCombatSystem::ObjectIsAttackable()` (full PK/faction logic); acdream's `GameWindow.IsHealthBarTarget` uses the server PWD bits `BF_ATTACKABLE (0x10)` OR `BF_PLAYER (0x8)` | `src/AcDream.App/Rendering/GameWindow.cs` (`IsHealthBarTarget`) → `SelectedObjectController` | The PWD `BF_ATTACKABLE`/`BF_PLAYER` bits distinguish monsters + players (bar) from friendly/vendor NPCs (name-only) for the M1.5 dev loop; the pet case and the full ObjectIsAttackable PK/faction refinement (free-PK, PK-vs-PK, PKLite) are not ported | A PK/faction edge (e.g. a hostile-flagged player whose `BF_ATTACKABLE` is unset, or a pet) could show/hide the bar where retail differs — no impact on the non-PK PvE dev loop | `ClientCombatSystem::ObjectIsAttackable` acclient_2013_pseudo_c.txt:375385; `BF_ATTACKABLE` acclient.h:6437 |
|
||||
| AP-47 | Cursor drag ghost reuses the full composited `m_pIcon` (incl. type-default underlay) instead of retail's dedicated `m_pDragIcon` (base + custom-overlay, NO type-default underlay). Opacity now matches retail (full). | `src/AcDream.App/UI/UiRoot.cs` (`DrawDragGhost`/`GhostAlpha`) → `src/AcDream.App/UI/UiItemSlot.cs` (`GetDragGhost`) | Cosmetic only — the dragged item is still unambiguously identifiable; building the second underlay-less composite is deferred polish; the ghost is item-agnostic in UiRoot via `GetDragGhost()` | The ghost carries the opaque type-default underlay backing rather than retail's underlay-less copy — a subtle look difference while dragging, no functional effect. | `IconData::RenderIcons` acclient_2013_pseudo_c.txt:407594-407625 (m_pDragIcon path); deep-dive §3.2/§5.5 |
|
||||
| AP-48 | Inventory burden reads the player's wire `EncumbranceVal` (PropertyInt 5) when present — delivered by B-Wire (login PD-bundle `UpsertProperties` + live `PrivateUpdatePropertyInt 0x02CD`); `ClientObjectTable.SumCarriedBurden` remains only as a defensive fallback for when the server omits it. | `src/AcDream.App/UI/Layout/InventoryController.cs` (`RefreshBurden`); `src/AcDream.Core.Net/ObjectTableWiring.cs` (player-int route) | B-Wire ports the retail read (`CACQualities::InqLoad` reads the server value); the client sum is now fallback-only, kept defensively. CONFIRM the server actually sends EncumbranceVal at the B-Wire visual gate, then DELETE this row. | If the server omits EncumbranceVal, the bar falls back to the client sum (the original drift) until the first 0x02CD — confirm at the gate. | `CACQualities::InqLoad` 0x0058f130; ACE PropertyInt.EncumbranceVal=5 |
|
||||
| AP-49 | Carry-capacity augmentation (PropertyInt `0xE6`) read from the player's wire property bundle when present (B-Wire PD `UpsertProperties`); defaults to 0 (correct for un-augmented characters) when absent. | `src/AcDream.App/UI/Layout/InventoryController.cs` (`RefreshBurden`) caller of `BurdenMath.EncumbranceCapacity` | B-Wire delivers the player's full PropertyInt table (incl. `0xE6` when the server sets it) via `UpsertProperties`; aug=0 is the correct no-aug default. CONFIRM the server sends `0xE6` for an augmented char at the gate, then DELETE. | An augmented character whose server omits `0xE6` reads slightly low capacity (bar fills higher than retail); un-augmented chars are exact. | `EncumbranceSystem::EncumbranceCapacity` decomp 256393 (0x004fcc00); retail `0xE6` PropertyInt augmentation |
|
||||
|
|
|
|||
|
|
@ -491,11 +491,12 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar.
|
|||
- **✓ SHIPPED — Wave 4.3b source-aware toolbar transactions.** Pure Core `ShortcutDropPlanner` ports `gmToolbarUI::HandleDropRelease @ 0x004BE7C0`: fresh inventory drops displace cyclically right (with wrap), shortcut aliases restore to their vacated source, duplicate objects are removed before placement, full bars intentionally lose the displaced entry when retail finds no empty slot, and every transaction is planned before local mutation then emitted in exact Remove/Add order. Full-stack rekey planning preserves the raw spell word. AP-102 retired; 4,646 automated tests pass; live inventory-to-occupied displacement and toolbar reorder gate passed 2026-07-11.
|
||||
- **✓ SHIPPED — Wave 4.3c remaining drop branches + stack merge.** `UiButton` now has a reusable retained item-drop-target seam; gmToolbarUI's inventory/backpack button accepts only fresh physical items, draws the exact authored green-arrow overlay `0x060011F7`, and sends them to the player container while shortcut aliases remain neutral. Pure Core `StackMergePlanner` ports `IsMergeAttemptLegal @ 0x00586F30` and `AttemptMerge @ 0x005878F0`; inventory drops try legal same-WCID stack merges before normal insertion, clamp `0x0054` amount to destination capacity, immediately broadcast retail's misleadingly named `FullMergingItem` notice to rekey the toolbar, and select the destination. Existing destination shortcuts are removed before rekey. Warning-free Release build and 4,660 tests pass; live stack-merge gate passed 2026-07-11. Backpack-button drop gate remains pending.
|
||||
- **✓ SHIPPED — Wave 4.4a selected-stack controls and transfers.** `SelectedObjectController` formats retail's count-first `NAME_APPROPRIATE` stack name from the wire plural, reveals the authored right-aligned numeric entry and horizontal DAT slider, initializes both to the full stack, refreshes them on selected-object stack changes, and shares one Core split quantity with every item-holder transfer path. The thumb owns its raw pointer position so quantity 1 and maximum reach the exact endpoints; entry writes retain retail's `split/max` attribute behavior. Exact `ItemHolder::GetObjectSplitSize` selection scoping drives merge, split-to-container `0x0055`, and split-to-ground `0x0056`; partial splits wait for the server-created object instead of moving the source optimistically. Exact cdb-pinned 1000-step rounding, plural fallback, real-DAT media/alignment, pointer drag, and wire bytes have conformance tests. Warning-free App Release build and 4,697 tests passed; controls, partial inventory split, and partial ground drop were live-confirmed 2026-07-11.
|
||||
- **✓ SHIPPED — Wave 4.4b selected-item mana.** Owned non-stack selections send exact `QueryItemMana 0x0263`; `0x0264` parses guid/fraction/trailing-valid into Core `ItemManaState`; valid matching responses reveal authored meter `0x100001A2`, invalid and visible selection-change paths cancel with guid zero. Health uses the same retail visible-meter cancellation rule. Wire/state/controller conformance is green in the 4,712-test Release suite. Live Mana Stone source-destruction → armor-mana-transfer → bar-update gate passed 2026-07-11. AP-101 now covers remaining toolbar controls only; AP-46 remains for exact PK/faction/pet health policy.
|
||||
- **✓ SHIPPED — Wave 4.4b selected-item mana.** Owned non-stack selections send exact `QueryItemMana 0x0263`; `0x0264` parses guid/fraction/trailing-valid into Core `ItemManaState`; valid matching responses reveal authored meter `0x100001A2`, invalid and visible selection-change paths cancel with guid zero. Health uses the same retail visible-meter cancellation rule. Wire/state/controller conformance is green in the 4,712-test Release suite. Live Mana Stone source-destruction → armor-mana-transfer → bar-update gate passed 2026-07-11. AP-101 now covers remaining toolbar controls only.
|
||||
- **✓ IMPLEMENTED — Wave 4.4c exact selected-health policy.** Core ports `ObjectIsAttackable @ 0x0056A600`; CreateObject preserves second-header PetOwner; toolbar player/pet composition is exact. Self, pets, and Free-PK creatures now query health while friendly NPCs and attackable non-creatures remain name-only. AP-46 retired; live gate pending.
|
||||
- **✓ 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.
|
||||
- **✓ 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).** Selected-item mana (`0x100001A2`) has the exact `0x0263/0x0264` query, validity, visibility, and cancel path and passed its live gate. Stack entry (`0x100001A3`), stack slider (`0x100001A4`), formatted count/name, shared split quantity, click-to-use, peace/war, and health/name are implemented. Remaining: exact PK/faction/pet health policy (AP-46). Exact raw `ShortCutData` preservation is already shipped in Wave 4.2; the eight `favorite_spells_` lists belong to the separate spellbook/spell-bar phase.
|
||||
- **☐ D.5.3 — Toolbar selected-object display (issue #141).** Implementation complete: 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, and health/name. Remaining: live gate for the newly restored health-policy edge cases, then close #141. Exact raw `ShortCutData` preservation is already shipped in Wave 4.2; the eight `favorite_spells_` lists belong to the separate spellbook/spell-bar phase.
|
||||
- **☐ D.5.5+ — Core panels.** Inventory (`gmInventoryUI`/`gmBackpackUI`), equipment/paperdoll (`gmPaperDollUI`/`gm3DItemsUI` + the `UiViewport` 3D doll), vendor, trade, spellbook. Research drop done (`docs/research/2026-06-16-*`). Depends on **D.5.4** (data model) + the item-slot/list/icon spine (D.5.1/D.5.2) + the **window manager** (Plan 2: open/close/z-order/persist + faithful grip/dragbar drag-resize) + the drag-drop spine wired (`UiRoot` has the chain; the per-cell accept/drop hooks are still stubs in `UiField`). Also deferred from D.5.1: drag/reorder + the `AddShortcut`/`RemoveShortcut` mutate wire.
|
||||
- **D.6 — HUD.** **Radar/compass IMPLEMENTED 2026-07-10; live-world visual gate pending.** It is the retained retail `gmRadarUI` (`LayoutDesc 0x21000074`): a 120×140 circular radar (`0x06004CC1`) with N/E/S/W token sprites orbiting the face, a fixed bright-green player marker, and the coordinate footer (`0x06004CC0`). Projection is `radarPixelRadius / radarRange` with 75 m outdoor / 25 m indoor ranges — there is no 1.18 factor or scrolling compass strip. It lives in `AcDream.App/UI` beside the other imported retail panels. Remaining D.6: target name plate, damage floaters, and other world-space HUD elements through the raw `TextRenderer` path. See slice 06 §A.2, `docs/research/2026-07-10-retail-radar-pseudocode.md`, and the named `gmRadarUI` decomp.
|
||||
- **✓ SHIPPED — D.7 — Cursor manager (user-confirmed 2026-07-11).** All reachable `ClientUISystem::UpdateCursorState @ 0x00564630` branches resolve through production DAT EnumIDMap table 6, with event-ordered global-default versus captured/hovered `MediaDescCursor` layering from `UIElementManager::CheckCursor @ 0x0045ABF0`. OS fallback remains only AP-72 and emits a visible diagnostic. The live gate covered world/object, combat, targeted-use, text, move/resize, and drag contexts; combat-indicator mouse clicks and the quick key share the same toggle command.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
# Retail selected-object health policy pseudocode — 2026-07-11
|
||||
|
||||
## Oracle
|
||||
|
||||
- `gmToolbarUI::HandleSelectionChanged @ 0x004BF380`, health branch
|
||||
`0x004BF725..0x004BF76C`.
|
||||
- `ClientCombatSystem::ObjectIsAttackable @ 0x0056A600`.
|
||||
- `ACCWeenieObject::IsPlayer/IsPK/IsPKLite @ 0x0058C890/0x0058C8B0/0x0058C8A0`.
|
||||
- `PublicWeenieDesc::UnPack @ 0x005AD470`, second-header PetOwner read
|
||||
`0x005AD7EC..0x005AD7F8`.
|
||||
- Struct/flag definitions: `acclient.h` `PublicWeenieDesc`,
|
||||
`PublicWeenieDescPackHeader2::PWD2_Packed_PetOwner = 0x8`.
|
||||
|
||||
ACE cross-check: `WorldObject_Networking.SerializeCreateObject` writes the second
|
||||
header after the ordinary optional tail in this order: IconUnderlay, MaterialType,
|
||||
CooldownId, CooldownDuration (`double`), PetOwner (`u32`), then alignment. Its
|
||||
`UpdateObjectDescriptionFlags` maps the same PWD player/attackable/PK/Free-PK/PKLite
|
||||
bits read by retail.
|
||||
|
||||
Chorizite/holtburger cross-check: the independently generated
|
||||
`Chorizite.ACProtocol/Types/PublicWeenieDesc` schema and holtburger inventory client
|
||||
inventory are catalogued in `deepdives/r06-items-inventory.md`; that schema pins the
|
||||
same Header2 `0x01/0x02/0x04/0x08` order and field widths. Neither interpretation
|
||||
aid changes the client behavior above; the named retail branch order remains ground truth.
|
||||
|
||||
## Constants
|
||||
|
||||
```text
|
||||
ITEM_TYPE_CREATURE = 0x10
|
||||
|
||||
BF_PLAYER = 0x00000008
|
||||
BF_ATTACKABLE = 0x00000010
|
||||
BF_PLAYER_KILLER = 0x00000020
|
||||
BF_FREE_PKSTATUS = 0x00200000
|
||||
BF_PKLITE_PKSTATUS = 0x02000000
|
||||
|
||||
PWD2_PET_OWNER = 0x00000008
|
||||
```
|
||||
|
||||
## Exact pseudocode
|
||||
|
||||
```text
|
||||
ToolbarShouldQueryHealth(selectedId):
|
||||
selected = lookup selectedId
|
||||
if selected missing:
|
||||
return false
|
||||
|
||||
if selected.stackSize > 1:
|
||||
return false // stack UI is mutually exclusive
|
||||
|
||||
isPlayer = (selected.pwd.bitfield & BF_PLAYER) != 0
|
||||
isPet = selected.pwd.petOwner != 0
|
||||
|
||||
if isPlayer OR isPet OR ObjectIsAttackable(selectedId):
|
||||
QueryHealth(selectedId)
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
ObjectIsAttackable(targetId):
|
||||
if targetId == 0 OR targetId == localPlayerId:
|
||||
return true
|
||||
|
||||
target = lookup targetId
|
||||
if target missing OR (target.type & ITEM_TYPE_CREATURE) == 0:
|
||||
return false
|
||||
|
||||
if (target.pwd.bitfield & BF_FREE_PKSTATUS) != 0:
|
||||
return true
|
||||
|
||||
player = lookup localPlayerId
|
||||
if player missing:
|
||||
return false
|
||||
|
||||
if (player.pwd.bitfield & BF_FREE_PKSTATUS) != 0:
|
||||
return true
|
||||
|
||||
if target.IsPlayer():
|
||||
if target.IsPK() AND player.IsPK():
|
||||
return true
|
||||
if target.IsPKLite() AND player.IsPKLite():
|
||||
return true
|
||||
return false
|
||||
|
||||
if target.pwd.petOwner != 0:
|
||||
return false
|
||||
|
||||
return (target.pwd.bitfield & BF_ATTACKABLE) != 0
|
||||
```
|
||||
|
||||
## Toolbar consequence
|
||||
|
||||
The toolbar's outer `IsPlayer || petOwner` test runs before
|
||||
`ObjectIsAttackable`. Therefore every selected player and every selected pet gets
|
||||
a health query regardless of PK pairing. The full PK/PKLite branches remain part
|
||||
of the faithful reusable `ObjectIsAttackable` port, but they do not decide toolbar
|
||||
player visibility. Relative to acdream's former PWD-only approximation, the
|
||||
observable missing toolbar cases are:
|
||||
|
||||
1. the local player/self (formerly rejected explicitly),
|
||||
2. creatures with `PetOwner != 0`,
|
||||
3. Free-PK creatures, or ordinary creatures while the local player is Free-PK.
|
||||
|
||||
Friendly non-pet NPCs remain name-only. Non-creature objects remain name-only even
|
||||
when they carry `BF_ATTACKABLE` (the existing attackable-door regression guard).
|
||||
|
|
@ -524,7 +524,7 @@ their live action gate remains pending. The inventory-button gate remains pendin
|
|||
`RecvNotice_SplitStack` activation behavior, and `HandleSelectionChanged` stack
|
||||
precedence. Vendor-specific initial quantity remains naturally dormant until a
|
||||
vendor panel owns an active vendor id.
|
||||
- [ ] Replace the PWD-only health gate with the exact attackable/player/pet policy.
|
||||
- [x] Replace the PWD-only health gate with the exact attackable/player/pet policy.
|
||||
- [x] Add item-mana query/cancel state and health query cancellation.
|
||||
- [x] Add one shared split-quantity owner for the entry, slider, and selected-source
|
||||
item-holder merge, container-split, and ground-split payloads.
|
||||
|
|
@ -559,6 +559,15 @@ Automated wire, state, and retained-controller conformance is green. The live ga
|
|||
transferred its mana into armor, and the selected armor's authored mana bar updated from
|
||||
the authoritative `0x0264` response.
|
||||
|
||||
**Wave 4.4c implementation (2026-07-11):** the former PWD-only approximation is
|
||||
replaced by a pure Core port of `ClientCombatSystem::ObjectIsAttackable @ 0x0056A600`
|
||||
composed exactly with toolbar `IsPlayer || pet_owner`. CreateObject now continues through
|
||||
the complete second header and preserves `PetOwner` after MaterialType/Cooldown fields,
|
||||
matching `PublicWeenieDesc::UnPack @ 0x005AD7AC..0x005AD7F8` and ACE serialization.
|
||||
The policy restores self, pet, and Free-PK creature cases while retaining name-only
|
||||
friendly NPCs and rejecting attackable non-creatures. AP-46 is retired; automated
|
||||
parser/model/policy conformance is green and the live gate remains pending.
|
||||
|
||||
**Wave 4.1 implementation (2026-07-11):** bare `1..9` now use slots 0..8,
|
||||
Ctrl+`1..9` selects them, Alt+`5..9` uses slots 13..17, and `0` creates a
|
||||
shortcut to the selected owned/eligible object in the first empty slot. A focused
|
||||
|
|
|
|||
|
|
@ -12568,39 +12568,17 @@ public sealed class GameWindow : IDisposable
|
|||
return (LiveItemType(guid) & AcDream.Core.Items.ItemType.Creature) != 0;
|
||||
}
|
||||
|
||||
// PublicWeenieDesc _bitfield flags (acclient.h:6431-6463) — same bitfield RadarBlipColors reads.
|
||||
private const uint BfPlayer = 0x8u; // BF_PLAYER (acclient.h:6434)
|
||||
private const uint BfAttackable = 0x10u; // BF_ATTACKABLE (acclient.h:6437)
|
||||
|
||||
/// <summary>
|
||||
/// True if the selected-object strip should show a Health meter for <paramref name="guid"/>.
|
||||
/// Approximates retail's <c>IsPlayer() || pet_owner || ClientCombatSystem::ObjectIsAttackable()</c>
|
||||
/// gate (gmToolbarUI::HandleSelectionChanged :198754) using the server-provided PWD flags:
|
||||
/// the <c>BF_ATTACKABLE</c> bit (monsters) or the <c>BF_PLAYER</c> bit (other players).
|
||||
/// A friendly NPC (e.g. a vendor) has neither bit set → name-only, matching retail.
|
||||
/// The full PK/faction logic of ObjectIsAttackable + the pet case are not ported (divergence AP-46).
|
||||
/// Exact projection of retail's <c>IsPlayer() || pet_owner ||
|
||||
/// ClientCombatSystem::ObjectIsAttackable()</c> gate from
|
||||
/// <c>gmToolbarUI::HandleSelectionChanged @ 0x004BF380</c>.
|
||||
/// </summary>
|
||||
private bool IsHealthBarTarget(uint guid)
|
||||
{
|
||||
if (guid == _playerServerGuid)
|
||||
return false;
|
||||
if (!_entitiesByServerGuid.ContainsKey(guid))
|
||||
return false;
|
||||
|
||||
uint pwd = _lastSpawnByGuid.TryGetValue(guid, out var spawn)
|
||||
&& spawn.ObjectDescriptionFlags is { } odf ? odf : 0u;
|
||||
|
||||
// Another player → health bar (retail IsPlayer branch).
|
||||
if ((pwd & BfPlayer) != 0)
|
||||
return true;
|
||||
|
||||
// Attackable branch: retail ObjectIsAttackable requires the object to be a CREATURE
|
||||
// first (InqType() & 0x10, acclient_2013_pseudo_c.txt:375406), THEN attackable. A Door
|
||||
// carries the BF_ATTACKABLE bit but is ItemType Misc, so it is never a health-bar target —
|
||||
// require the Creature flag here too (matches retail; excludes attackable doors/objects).
|
||||
bool isCreature = (LiveItemType(guid) & AcDream.Core.Items.ItemType.Creature) != 0;
|
||||
return isCreature && (pwd & BfAttackable) != 0;
|
||||
}
|
||||
=> AcDream.Core.Combat.SelectedObjectHealthPolicy.ShouldQueryHealth(
|
||||
_playerServerGuid,
|
||||
Objects.Get(_playerServerGuid),
|
||||
Objects.Get(guid));
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -37,13 +37,6 @@ namespace AcDream.App.UI.Layout;
|
|||
/// "Drudge Slinker" reference shot).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <strong>Divergence — health-target gate approximation.</strong>
|
||||
/// Retail sends <c>Event_QueryHealth</c> for <c>IsPlayer() || pet_owner || ObjectIsAttackable()</c>
|
||||
/// (<c>:198754</c>). acdream uses <c>IsLiveCreatureTarget</c> (the <c>ItemType.Creature</c>
|
||||
/// flag) to gate the QueryHealth send. Visibility itself is health-data-driven (above), so
|
||||
/// the gate only affects whether we proactively query; recorded in the divergence register.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class SelectedObjectController : IRetainedPanelController
|
||||
{
|
||||
|
|
|
|||
|
|
@ -197,7 +197,9 @@ public static class CreateObject
|
|||
byte? CombatUse = null,
|
||||
// PublicWeenieDesc._plural_name, gated by WeenieHeader flag 0x1.
|
||||
// ACCWeenieObject::GetObjectName(NAME_APPROPRIATE) selects this for stacks.
|
||||
string? PluralName = null);
|
||||
string? PluralName = null,
|
||||
// PublicWeenieDesc._pet_owner, gated by second-header flag 0x8.
|
||||
uint? PetOwnerId = null);
|
||||
|
||||
/// <summary>
|
||||
/// The relevant subset of the server-sent <c>MovementData</c> /
|
||||
|
|
@ -760,6 +762,7 @@ public static class CreateObject
|
|||
uint iconUnderlayId = 0;
|
||||
uint uiEffects = 0;
|
||||
uint weenieFlags2 = 0;
|
||||
uint? petOwnerId = null;
|
||||
try
|
||||
{
|
||||
// BF_INCLUDES_SECOND_HEADER = 0x04000000 per acclient.h:6458
|
||||
|
|
@ -952,6 +955,28 @@ public static class CreateObject
|
|||
{
|
||||
iconUnderlayId = ReadPackedDwordOfKnownType(body, ref pos, IconTypePrefix);
|
||||
}
|
||||
// PublicWeenieDesc::UnPack @ 0x005AD7AC..0x005AD7F8 and ACE
|
||||
// SerializeCreateObject both place these fields after IconUnderlay.
|
||||
if ((weenieFlags & 0x80000000u) != 0) // MaterialType u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc MaterialType");
|
||||
pos += 4;
|
||||
}
|
||||
if ((weenieFlags2 & 0x00000002u) != 0) // CooldownId u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc CooldownId");
|
||||
pos += 4;
|
||||
}
|
||||
if ((weenieFlags2 & 0x00000004u) != 0) // CooldownDuration f64
|
||||
{
|
||||
if (body.Length - pos < 8) throw new FormatException("trunc CooldownDuration");
|
||||
pos += 8;
|
||||
}
|
||||
if ((weenieFlags2 & 0x00000008u) != 0) // PetOwner u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc PetOwner");
|
||||
petOwnerId = ReadU32(body, ref pos);
|
||||
}
|
||||
}
|
||||
catch { /* truncated weenie tail — keep whatever we got. */ }
|
||||
|
||||
|
|
@ -974,7 +999,8 @@ public static class CreateObject
|
|||
Workmanship: wWorkmanship,
|
||||
RadarBlipColor: radarBlipColor, RadarBehavior: radarBehavior,
|
||||
CombatUse: combatUse,
|
||||
PluralName: pluralName);
|
||||
PluralName: pluralName,
|
||||
PetOwnerId: petOwnerId);
|
||||
|
||||
// Local helper: if we ran out of fields past PhysicsData, still
|
||||
// return the useful prefix (guid/position/setup/animParts/textures/palettes/scale/motion).
|
||||
|
|
|
|||
|
|
@ -88,5 +88,6 @@ public static class ObjectTableWiring
|
|||
RadarBehavior: s.RadarBehavior,
|
||||
PublicWeenieBitfield: s.ObjectDescriptionFlags,
|
||||
CombatUse: s.CombatUse,
|
||||
PluralName: s.PluralName);
|
||||
PluralName: s.PluralName,
|
||||
PetOwnerId: s.PetOwnerId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,8 @@ public sealed class WorldSession : IDisposable
|
|||
byte? RadarBlipColor = null,
|
||||
byte? RadarBehavior = null,
|
||||
byte? CombatUse = null,
|
||||
string? PluralName = null);
|
||||
string? PluralName = null,
|
||||
uint? PetOwnerId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Projects the wire-level CreateObject result into the stable session
|
||||
|
|
@ -177,7 +178,8 @@ public sealed class WorldSession : IDisposable
|
|||
RadarBlipColor: parsed.RadarBlipColor,
|
||||
RadarBehavior: parsed.RadarBehavior,
|
||||
CombatUse: parsed.CombatUse,
|
||||
PluralName: parsed.PluralName);
|
||||
PluralName: parsed.PluralName,
|
||||
PetOwnerId: parsed.PetOwnerId);
|
||||
|
||||
/// <summary>Fires when the session finishes parsing a CreateObject.</summary>
|
||||
public event Action<EntitySpawn>? EntitySpawned;
|
||||
|
|
|
|||
79
src/AcDream.Core/Combat/SelectedObjectHealthPolicy.cs
Normal file
79
src/AcDream.Core/Combat/SelectedObjectHealthPolicy.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.Core.Combat;
|
||||
|
||||
/// <summary>
|
||||
/// Retail selected-object health-query eligibility.
|
||||
/// Ports <c>gmToolbarUI::HandleSelectionChanged @ 0x004BF380</c> and
|
||||
/// <c>ClientCombatSystem::ObjectIsAttackable @ 0x0056A600</c>.
|
||||
/// </summary>
|
||||
public static class SelectedObjectHealthPolicy
|
||||
{
|
||||
public const uint BfPlayer = 0x00000008u;
|
||||
public const uint BfAttackable = 0x00000010u;
|
||||
public const uint BfPlayerKiller = 0x00000020u;
|
||||
public const uint BfFreePkStatus = 0x00200000u;
|
||||
public const uint BfPkLiteStatus = 0x02000000u;
|
||||
|
||||
/// <summary>
|
||||
/// Exact toolbar composition: every player, every pet, otherwise retail's
|
||||
/// reusable combat attackability predicate. Stacked objects take the separate
|
||||
/// split-control branch before this policy is called.
|
||||
/// </summary>
|
||||
public static bool ShouldQueryHealth(
|
||||
uint playerId,
|
||||
ClientObject? player,
|
||||
ClientObject? selected)
|
||||
{
|
||||
if (selected is null)
|
||||
return false;
|
||||
|
||||
uint flags = selected.PublicWeenieBitfield ?? 0u;
|
||||
return (flags & BfPlayer) != 0
|
||||
|| selected.PetOwnerId != 0
|
||||
|| ObjectIsAttackable(playerId, player, selected.ObjectId, selected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Line-for-line branch port of
|
||||
/// <c>ClientCombatSystem::ObjectIsAttackable @ 0x0056A600</c>.
|
||||
/// </summary>
|
||||
public static bool ObjectIsAttackable(
|
||||
uint playerId,
|
||||
ClientObject? player,
|
||||
uint targetId,
|
||||
ClientObject? target)
|
||||
{
|
||||
if (targetId == 0 || targetId == playerId)
|
||||
return true;
|
||||
|
||||
if (target is null || (target.Type & ItemType.Creature) == 0)
|
||||
return false;
|
||||
|
||||
uint targetFlags = target.PublicWeenieBitfield ?? 0u;
|
||||
if ((targetFlags & BfFreePkStatus) != 0)
|
||||
return true;
|
||||
|
||||
if (player is null)
|
||||
return false;
|
||||
|
||||
uint playerFlags = player.PublicWeenieBitfield ?? 0u;
|
||||
if ((playerFlags & BfFreePkStatus) != 0)
|
||||
return true;
|
||||
|
||||
if ((targetFlags & BfPlayer) != 0)
|
||||
{
|
||||
if ((targetFlags & BfPlayerKiller) != 0
|
||||
&& (playerFlags & BfPlayerKiller) != 0)
|
||||
return true;
|
||||
|
||||
return (targetFlags & BfPkLiteStatus) != 0
|
||||
&& (playerFlags & BfPkLiteStatus) != 0;
|
||||
}
|
||||
|
||||
if (target.PetOwnerId != 0)
|
||||
return false;
|
||||
|
||||
return (targetFlags & BfAttackable) != 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -182,6 +182,11 @@ public sealed class ClientObject
|
|||
public uint? TargetType { get; set; } // ITEM_TYPE mask for targeted-use compatibility
|
||||
/// <summary>Retail <c>PublicWeenieDesc._bitfield</c>; null until CreateObject supplies it.</summary>
|
||||
public uint? PublicWeenieBitfield { get; set; }
|
||||
/// <summary>
|
||||
/// Retail <c>PublicWeenieDesc._pet_owner</c>; zero means this creature is not a pet.
|
||||
/// CreateObject second-header flag <c>PWD2_Packed_PetOwner (0x8)</c> supplies it.
|
||||
/// </summary>
|
||||
public uint PetOwnerId { get; set; }
|
||||
/// <summary>Retail <c>PublicWeenieDesc._combatUse</c>; null when its header flag was absent.</summary>
|
||||
public byte? CombatUse { get; set; }
|
||||
/// <summary>Client-side trade state used by ItemHolder legality gates.</summary>
|
||||
|
|
@ -249,7 +254,8 @@ public readonly record struct WeenieData(
|
|||
byte? RadarBehavior = null,
|
||||
uint? PublicWeenieBitfield = null,
|
||||
byte? CombatUse = null,
|
||||
string? PluralName = null);
|
||||
string? PluralName = null,
|
||||
uint? PetOwnerId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Retail ITEM_USEABLE helpers (acclient.h:6478, ItemUses::* at 0x004fccd0).
|
||||
|
|
|
|||
|
|
@ -388,6 +388,7 @@ public sealed class ClientObjectTable
|
|||
if (d.Useability is { } use) obj.Useability = use;
|
||||
if (d.TargetType is { } targetType) obj.TargetType = targetType;
|
||||
if (d.PublicWeenieBitfield is { } bitfield) obj.PublicWeenieBitfield = bitfield;
|
||||
if (d.PetOwnerId is { } petOwnerId) obj.PetOwnerId = petOwnerId;
|
||||
if (d.CombatUse is { } combatUse) obj.CombatUse = combatUse;
|
||||
if (d.RadarBlipColor is { } radarBlipColor) obj.RadarBlipColor = radarBlipColor;
|
||||
if (d.RadarBehavior is { } radarBehavior) obj.RadarBehavior = radarBehavior;
|
||||
|
|
|
|||
|
|
@ -566,6 +566,29 @@ public sealed class CreateObjectTests
|
|||
Assert.Equal("Pyreal Scarabs", parsed.Value.PluralName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WeenieHeader_secondHeaderPetOwner_isPreservedAfterPrecedingFields()
|
||||
{
|
||||
// PublicWeenieDesc::UnPack @ 0x005AD7AC..0x005AD7F8:
|
||||
// MaterialType, CooldownId, CooldownDuration, then PetOwner.
|
||||
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
|
||||
guid: 0x50000024u,
|
||||
name: "Combat Pet",
|
||||
itemType: (uint)ItemType.Creature,
|
||||
objectDescriptionFlags: 0x04000000u,
|
||||
weenieFlags: 0x80000000u,
|
||||
weenieFlags2: 0x0000000Eu,
|
||||
materialType: 7u,
|
||||
cooldownId: 11u,
|
||||
cooldownDuration: 12.5,
|
||||
petOwnerId: 0x50000001u);
|
||||
|
||||
var parsed = CreateObject.TryParse(body);
|
||||
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(0x50000001u, parsed.Value.PetOwnerId);
|
||||
}
|
||||
|
||||
private static byte[] BuildMinimalCreateObjectWithWeenieHeader(
|
||||
uint guid,
|
||||
string name,
|
||||
|
|
@ -601,7 +624,11 @@ public sealed class CreateObjectTests
|
|||
byte? radarBlipColor = null,
|
||||
byte? radarBehavior = null,
|
||||
byte? combatUse = null,
|
||||
ushort movementSeq = 0)
|
||||
ushort movementSeq = 0,
|
||||
uint materialType = 0,
|
||||
uint cooldownId = 0,
|
||||
double cooldownDuration = 0,
|
||||
uint petOwnerId = 0)
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
WriteU32(bytes, CreateObject.Opcode);
|
||||
|
|
@ -691,6 +718,16 @@ public sealed class CreateObjectTests
|
|||
if ((weenieFlags & 0x10000000u) != 0) WriteU16(bytes, 0); // HookType u16
|
||||
if ((weenieFlags & 0x40000000u) != 0) WritePackedDword(bytes, iconOverlayId); // IconOverlay
|
||||
if ((weenieFlags2 & 0x00000001u) != 0) WritePackedDword(bytes, iconUnderlayId); // IconUnderlay
|
||||
if ((weenieFlags & 0x80000000u) != 0) WriteU32(bytes, materialType); // MaterialType
|
||||
if ((weenieFlags2 & 0x00000002u) != 0) WriteU32(bytes, cooldownId); // CooldownId
|
||||
if ((weenieFlags2 & 0x00000004u) != 0)
|
||||
{
|
||||
Span<byte> tmp = stackalloc byte[8];
|
||||
BinaryPrimitives.WriteDoubleLittleEndian(tmp, cooldownDuration);
|
||||
bytes.AddRange(tmp.ToArray());
|
||||
}
|
||||
if ((weenieFlags2 & 0x00000008u) != 0) WriteU32(bytes, petOwnerId); // PetOwner
|
||||
Align4(bytes);
|
||||
|
||||
return bytes.ToArray();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ public sealed class ObjectTableWiringTests
|
|||
ObjectDescriptionFlags = 0x20800200u,
|
||||
CombatUse = 2,
|
||||
PluralName = "Iron Swords",
|
||||
PetOwnerId = 0x50000001u,
|
||||
};
|
||||
|
||||
var d = ObjectTableWiring.ToWeenieData(spawn);
|
||||
|
|
@ -110,6 +111,7 @@ public sealed class ObjectTableWiringTests
|
|||
Assert.Equal(0x20800200u, d.PublicWeenieBitfield);
|
||||
Assert.Equal((byte)2, d.CombatUse);
|
||||
Assert.Equal("Iron Swords", d.PluralName);
|
||||
Assert.Equal(0x50000001u, d.PetOwnerId);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -39,6 +39,17 @@ public sealed class WorldSessionRadarTests
|
|||
Assert.Equal("Pyreal Scarabs", spawn.PluralName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToEntitySpawn_PreservesPetOwner()
|
||||
{
|
||||
var spawn = WorldSession.ToEntitySpawn(MinimalParsed() with
|
||||
{
|
||||
PetOwnerId = 0x50000002u,
|
||||
});
|
||||
|
||||
Assert.Equal(0x50000002u, spawn.PetOwnerId);
|
||||
}
|
||||
|
||||
private static CreateObject.Parsed MinimalParsed() => new(
|
||||
Guid: 0x50000001u,
|
||||
Position: null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Combat;
|
||||
|
||||
public sealed class SelectedObjectHealthPolicyTests
|
||||
{
|
||||
private const uint PlayerId = 0x50000001u;
|
||||
|
||||
private static ClientObject Obj(
|
||||
uint id,
|
||||
ItemType type = ItemType.Creature,
|
||||
uint flags = 0,
|
||||
uint petOwner = 0) => new()
|
||||
{
|
||||
ObjectId = id,
|
||||
Type = type,
|
||||
PublicWeenieBitfield = flags,
|
||||
PetOwnerId = petOwner,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Toolbar_PlayerAndPetShortCircuitAttackability()
|
||||
{
|
||||
var local = Obj(PlayerId, flags: SelectedObjectHealthPolicy.BfPlayer);
|
||||
var otherPlayer = Obj(0x50000002u, flags: SelectedObjectHealthPolicy.BfPlayer);
|
||||
var pet = Obj(0x50000003u, petOwner: PlayerId);
|
||||
|
||||
Assert.True(SelectedObjectHealthPolicy.ShouldQueryHealth(PlayerId, local, otherPlayer));
|
||||
Assert.True(SelectedObjectHealthPolicy.ShouldQueryHealth(PlayerId, local, pet));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Toolbar_SelfSelectionGetsHealthQuery()
|
||||
{
|
||||
var local = Obj(PlayerId, flags: SelectedObjectHealthPolicy.BfPlayer);
|
||||
|
||||
Assert.True(SelectedObjectHealthPolicy.ShouldQueryHealth(PlayerId, local, local));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Toolbar_AttackableCreatureTrue_FriendlyNpcAndAttackableDoorFalse()
|
||||
{
|
||||
var local = Obj(PlayerId, flags: SelectedObjectHealthPolicy.BfPlayer);
|
||||
var monster = Obj(0x50000010u, flags: SelectedObjectHealthPolicy.BfAttackable);
|
||||
var friendlyNpc = Obj(0x50000011u);
|
||||
var door = Obj(0x50000012u, ItemType.Misc, SelectedObjectHealthPolicy.BfAttackable);
|
||||
|
||||
Assert.True(SelectedObjectHealthPolicy.ShouldQueryHealth(PlayerId, local, monster));
|
||||
Assert.False(SelectedObjectHealthPolicy.ShouldQueryHealth(PlayerId, local, friendlyNpc));
|
||||
Assert.False(SelectedObjectHealthPolicy.ShouldQueryHealth(PlayerId, local, door));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjectIsAttackable_FreePkOnEitherCreatureSideReturnsTrue()
|
||||
{
|
||||
var normalPlayer = Obj(PlayerId, flags: SelectedObjectHealthPolicy.BfPlayer);
|
||||
var freePlayer = Obj(PlayerId,
|
||||
flags: SelectedObjectHealthPolicy.BfPlayer | SelectedObjectHealthPolicy.BfFreePkStatus);
|
||||
var normalCreature = Obj(0x50000020u);
|
||||
var freeCreature = Obj(0x50000021u, flags: SelectedObjectHealthPolicy.BfFreePkStatus);
|
||||
|
||||
Assert.True(SelectedObjectHealthPolicy.ObjectIsAttackable(
|
||||
PlayerId, normalPlayer, freeCreature.ObjectId, freeCreature));
|
||||
Assert.True(SelectedObjectHealthPolicy.ObjectIsAttackable(
|
||||
PlayerId, freePlayer, normalCreature.ObjectId, normalCreature));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjectIsAttackable_PlayerRequiresMatchingPkPool()
|
||||
{
|
||||
var pkPlayer = Obj(PlayerId,
|
||||
flags: SelectedObjectHealthPolicy.BfPlayer | SelectedObjectHealthPolicy.BfPlayerKiller);
|
||||
var pkTarget = Obj(0x50000030u,
|
||||
flags: SelectedObjectHealthPolicy.BfPlayer | SelectedObjectHealthPolicy.BfPlayerKiller);
|
||||
var pkLiteTarget = Obj(0x50000031u,
|
||||
flags: SelectedObjectHealthPolicy.BfPlayer | SelectedObjectHealthPolicy.BfPkLiteStatus);
|
||||
|
||||
Assert.True(SelectedObjectHealthPolicy.ObjectIsAttackable(
|
||||
PlayerId, pkPlayer, pkTarget.ObjectId, pkTarget));
|
||||
Assert.False(SelectedObjectHealthPolicy.ObjectIsAttackable(
|
||||
PlayerId, pkPlayer, pkLiteTarget.ObjectId, pkLiteTarget));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjectIsAttackable_ZeroOrSelfTrue_MissingPlayerBlocksNormalCreature()
|
||||
{
|
||||
var creature = Obj(0x50000040u, flags: SelectedObjectHealthPolicy.BfAttackable);
|
||||
|
||||
Assert.True(SelectedObjectHealthPolicy.ObjectIsAttackable(PlayerId, null, 0u, null));
|
||||
Assert.True(SelectedObjectHealthPolicy.ObjectIsAttackable(PlayerId, null, PlayerId, null));
|
||||
Assert.False(SelectedObjectHealthPolicy.ObjectIsAttackable(
|
||||
PlayerId, null, creature.ObjectId, creature));
|
||||
}
|
||||
}
|
||||
|
|
@ -204,6 +204,25 @@ public sealed class ClientObjectTableTests
|
|||
Assert.Equal(0x99u, d.ContainerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ingest_PetOwnerFromSecondHeader_IsPreserved()
|
||||
{
|
||||
var table = new ClientObjectTable();
|
||||
var data = new WeenieData(
|
||||
Guid: 0x50000010u, Name: "Combat Pet", Type: ItemType.Creature,
|
||||
WeenieClassId: 2, IconId: 0, IconOverlayId: 0, IconUnderlayId: 0, Effects: 0,
|
||||
Value: null, StackSize: null, StackSizeMax: null, Burden: null,
|
||||
ContainerId: null, WielderId: null, ValidLocations: null,
|
||||
CurrentWieldedLocation: null, Priority: null,
|
||||
ItemsCapacity: null, ContainersCapacity: null,
|
||||
Structure: null, MaxStructure: null, Workmanship: null,
|
||||
PetOwnerId: 0x50000001u);
|
||||
|
||||
ClientObject result = table.Ingest(data);
|
||||
|
||||
Assert.Equal(0x50000001u, result.PetOwnerId);
|
||||
}
|
||||
|
||||
private static WeenieData FullWeenie(uint guid, uint icon = 0x06001234u,
|
||||
string name = "Sword", ItemType type = ItemType.MeleeWeapon, uint effects = 0,
|
||||
int? value = 100, int? stack = 1, uint? container = null, uint wcid = 0xABCDu) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue