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:
parent
91d1d0d6f4
commit
9fdfe68c7f
1 changed files with 117 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue