diff --git a/docs/plans/2026-07-23-world-interaction-completion.md b/docs/plans/2026-07-23-world-interaction-completion.md index 36d4e371..6db49c58 100644 --- a/docs/plans/2026-07-23-world-interaction-completion.md +++ b/docs/plans/2026-07-23-world-interaction-completion.md @@ -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` 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.