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
clicking elsewhere, then reassess it. An item authored by another player is
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.