docs(interaction): Slice 4 spec - retail equipped-child picking research

The named-retail oracle settles the child-vs-parent question: retail's pick records part->physobj->id (CPhysicsPart::Draw 0x0050D7A0, GfxObjUnderSelectionRay 0x0054C740), equipped children are first-class CPhysicsObjs whose m_position IS the composed hold frame (add_child 0x0050F870, UpdateChild 0x00512D50), so a click on a wielded weapon returns the weapon's own guid with no parent redirection and no wielded-specific gate. Selection, the non-recursive click flash (SetLighting 0x00511A80), and the vivid brackets all anchor to the picked child; only sr_Use on your OWN wielded item is suppressed (0x004E5BE9).

The gap analysis found acdream's picker already correct - equipped children publish selection parts under their own guid and already win the ray test. The failure is downstream eligibility: PickAt requires the World-kind-only interaction set, so the winning hit is discarded. The slice is therefore a scoped pick-eligibility predicate plus a marker anchor sourced from the already-published child root pose - deliberately NOT widening the interaction/radar/auto-target set, which retail also keeps free of wielded items.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-29 18:10:05 +02:00
parent 91d1d0d6f4
commit 9fdfe68c7f

View file

@ -404,3 +404,120 @@ Named retail references and executable pseudocode are recorded in
- Assess an owned inscribable weapon. Edit and clear its inscription by - Assess an owned inscribable weapon. Edit and clear its inscription by
clicking elsewhere, then reassess it. An item authored by another player is clicking elsewhere, then reassess it. An item authored by another player is
read-only and reports the exact retail permission line when clicked. read-only and reports the exact retail permission line when clicked.
## Slice 4 — equipped-child world picking
**Status:** research complete 2026-07-29 (named-retail oracle + gap analysis);
implementation next. Owner shape per the program table: pure
world-query/picking policy plus presentation anchor. No wire, physics,
renderer, or `EquippedChildRenderController` changes.
### The retail mechanism
Retail picking is render-coupled, not a scene-graph ray walk. A click arms a
per-frame selection cursor (`UIElement_SmartBoxWrapper::MouseDown @
0x004E5700` sets a `SearchReason` — sr_Select/sr_Examine/sr_Use/sr_TargetedUse,
acclient.h:6789 — then `SmartBox::find_object @ 0x00451C60` sets
`Render::set_selection_cursor @ 0x0054B750`). During the frame,
`Render::update_viewpoint @ 0x0054CDD0` builds `selection_ray` via
`Render::pick_ray @ 0x0054B610`, and EVERY drawn part accumulates hits in
`Render::GfxObjUnderSelectionRay @ 0x0054C740`: drawing-sphere test, then
per-polygon tests when enabled, keeping the closest — with the polygon winner
outranking a sphere-only winner (`GetMouseSelectionObjectID @ 0x0054C950`,
read at `SmartBox::DrawNoBlit @ 0x00454C20`).
**The child-vs-parent answer:** each hit records `part->physobj->id`
(`CPhysicsPart::get_physobj_id @ 0x0050D490`), and a part is only a candidate
when `part->physobj->id != 0` (`CPhysicsPart::Draw @ 0x0050D7A0`). Equipped
children are first-class `CPhysicsObj`s with their own ids and part arrays
(`CPhysicsObj::add_child @ 0x0050F870` via `CSetup::GetHoldingLocation @
0x005213F0`; `CPhysicsObj::UpdateChild @ 0x00512D50` composes
`Frame::combine(parent_part_frame, hold_frame)` into the child's own
`m_position` every frame). So a click on a wielded weapon returns THE WEAPON'S
GUID — there is no parent redirection in the path, and no ethereal or
wielded-specific gate: the only candidacy rule is "drawn part with a nonzero
physobj id".
Post-pick (`RecvNotice_SmartBoxObjectFound @ 0x004E5AD0`): the id must exist in
the weenie table; selection is set to the picked id itself
(`ACCWeenieObject::SetSelectedObject @ 0x0058C2E0`); the click flash
(`CPhysicsObj::SetLighting @ 0x00511A80`) is non-recursive — it lights that
object's own part array ONLY, so clicking a weapon flashes the weapon and
clicking a creature does not flash its weapon; the vivid brackets
(`VividTargetIndicator @ 0x004F5CE0`) derive from the selected object's own
selection sphere at its own position, which for a child IS the hand frame.
`sr_Use` on an object whose `_wielderID == player_id` is suppressed
(0x004E5BE9) while selection still happens; `sr_Examine` examines the child id
directly. `PositionState.WIELDED` is distinct from `IN_CONTAINER`
(acclient.h:6802), so container suppression never hides a wielded selection.
### The gap in acdream (the picker is already right)
Equipped children are already live entities with their own `ServerGuid`
(`EquippedChildRenderController.TryRealize`, :448-617) and every draw path
already publishes their selection parts under that guid
(`RetailSelectionScene.AddVisiblePart`, which only skips `serverGuid == 0`).
`RetailWorldPicker.Pick` therefore already returns the weapon as the polygon
winner. The failure is entirely downstream: `WorldSelectionQuery.PickAt`
(:137-154) requires `TryGetInteractionEligibleRecord`, whose `_visible` set
admits `LiveEntityProjectionKind.World` only (`LiveEntityRuntime.cs:1183`
excludes Pending/Attached/Hidden by design), so the winning hit is discarded
and the click reports nothing. Retail would have succeeded. Marker anchoring
has the twin problem: `ResolveVividTargetInfo` (:262-286) gates on the
World-only `TryGetSpatiallyProjectedRecord`, and `TryGetSelectionSphere`
(:293-318) anchors at `entity.Position/Rotation`, which for an attached child
is deliberately the PARENT's root pose (`ApplyParentWorldPose`, :651-658) —
brackets at the wielder's feet. The child's true composed root
(`pose.RootLocal * parentWorld`, the exact `Frame::combine` equivalent) is
already published per frame to `EntityEffectPoseRegistry` (`PublishChildPose`,
:632-644; `TryGetRootPose` :195) and is what the vfx anchors already use.
### Slice plan
1. **Pick eligibility for attached projections.** Add a scoped
`LiveEntityRuntime.TryGetPickEligibleRecord(serverGuid, localEntityId)`
accepting `World` (today's semantics) OR `Attached` (with the same
`IsSpatiallyProjected` + `WorldEntity.Id == localEntityId` staleness
recheck). Consume it in `WorldSelectionQuery.PickAt`, the lighting-pulse
identity paths, and `TryGetInteractionTarget`. **Do NOT widen
`TryGetInteractionEligibleRecord`/`_visible`** — it feeds radar,
auto-target, sticky/MoveTo establishment and `CombatAttackTargetSource`,
none of which retail lets wielded items enter (retail's radar has no
wielded blips).
2. **Marker + sphere anchor.** Branch `ResolveVividTargetInfo` onto the new
predicate, and for Attached records transform the Setup selection sphere by
`EntityEffectPoseRegistry.TryGetRootPose(localId)` (injected as a
`Func<uint, Matrix4x4?>` beside the existing selection-sphere hook) instead
of the parent-derived `entity.Position/Rotation`.
3. **Own-wielded Use gate.** In the use-immediately path, skip the Activate
enqueue when the picked object's `WielderId == playerGuid` (selection and
flash still occur) — the 0x004E5BE9 parity. If deferred, it owes an AP row.
4. **Files:** `LiveEntityRuntime.cs` (predicate), `WorldSelectionQuery.cs`,
`SelectionInteractionController.cs`, plus composition wiring for the
root-pose hook. Untouched: `RetailWorldPicker`, `RetailSelectionScene`,
`WbDrawDispatcher`, `EquippedChildRenderController`.
5. **Conformance tests** (harnesses exist in
`tests/AcDream.App.Tests/Interaction/WorldSelectionQueryTests.cs` and
`Rendering/RetailSelectionSceneTests.cs`): child part closest → resolves to
the CHILD guid; stale/withdrawn child record → null, never the parent;
marker uses the pose-registry root, not the parent root; marker suppressed
for the player's own wielded child, shown for a remote's; lighting pulse
lights the child identity only; double-click Use suppressed on own wielded.
6. **Visual gate (user, two-client):** click a remote character's wielded
weapon — selection names the weapon, the flash lights only the weapon, the
vivid brackets track the weapon through the wielder's animation (hand, not
feet), right-click opens Slice 3's examination window on the weapon, radar
shows no weapon blip, and double-clicking your OWN weapon does not fire a
Use.
### Notes
- Slice 3 dependency verified: `SelectionState.Select` stores any nonzero guid
and `RequestAppraisal` has no eligibility gate, so the examination window
works on a picked child unmodified once the pick resolves.
- Divergence register: this slice ADDS no row — it removes an undocumented
deviation (Attached exclusion from pick eligibility versus retail's
part-id pick).
- Existing architectural divergence, unchanged by this slice: retail re-arms
the pick every frame for hover/tooltips (`sr_MouseOver`); acdream picks on
demand per click against the last published frame, with an identity recheck.