feat(interaction): Slice 4 - equipped-child world picking
A click on a remote character's wielded weapon reported nothing. The picker was already correct: RetailSelectionScene publishes every drawn part under its own live-entity server GUID and RetailWorldPicker returns the weapon as the polygon winner. The failure was downstream eligibility - WorldSelectionQuery required TryGetInteractionEligibleRecord, whose _visible set admits LiveEntityProjectionKind.World only, so the winning hit was discarded. Retail has no such gate. Render::GfxObjUnderSelectionRay @ 0x0054C740 accumulates each hit under the drawn part's own physics-object id (CPhysicsPart::get_physobj_id @ 0x0050D490), and CPhysicsPart::Draw @ 0x0050D7A0 admits any drawn part whose physobj id is nonzero. An equipped item is a first-class CPhysicsObj with its own id and part array (CPhysicsObj::add_child @ 0x0050F870 via CSetup::GetHoldingLocation @ 0x005213F0). There is no parent redirection and no wielded-specific rule, so a click on a wielded weapon returns THE WEAPON'S GUID. PositionState.WIELDED is distinct from IN_CONTAINER (acclient.h:6802), so container suppression never hid a wielded selection either. LiveEntityRuntime gains two scoped predicates: TryGetAttachedProjectedRecord (a current Attached projection that is spatially projected) and TryGetPickEligibleRecord (that arm plus today's World visible-set arm, with the same WorldEntity.Id staleness recheck). TryGetInteractionEligibleRecord and the _visible set are deliberately NOT widened - they feed radar, auto-target, sticky/MoveTo establishment, and CombatAttackTargetSource, and retail's radar has no wielded blips. A regression test asserts an attached child stays out of that set while picking admits it. Marker anchoring had the twin problem. SmartBox::GetObjectBoundingBox @ 0x00452E20 pushes the picked object's OWN m_position - which for a child is the frame CPhysicsObj::UpdateChild @ 0x00512D50 recomposes each tick as Frame::combine(parent part frame, holding frame) - and CPartArray::GetSelectionSphere @ 0x00518B80 scales the authored sphere by that object's own part-array scale. acdream stores the PARENT's root in the child projection's Position/Rotation because the child's MeshRefs are parent-relative, which put the vivid brackets at the wielder's feet. The composed child root is already published per frame to EntityEffectPoseRegistry by EquippedChildRenderController.PublishChildPose, so selection now borrows it through an injected Func<uint, Matrix4x4?> wired in LivePresentationComposition beside the existing selection-sphere hook. There is no parent fallback: a child with no published composed root has no live frame this tick and no sphere. Its part-array scale comes from the spawn record, the same source EquippedChildRenderController.TryRealize reads, because an Attached WorldEntity carries the parent-derived pose rather than its own ObjScale. The sr_Use branch of RecvNotice_SmartBoxObjectFound @ 0x004E5AD0 guards ItemHolder::UseObject with `found->pwd._wielderID != SmartBox::player_id` at 0x004E5BE9 while still selecting and flashing. Equipped-child picking makes that click reachable, so the gate ships with it as IWorldSelectionQuery.IsWieldedByPlayer. CPhysicsObj::SetLighting @ 0x00511A80 is non-recursive, so the pulse lights the clicked object's own part array only - clicking a weapon never flashes its wielder. That follows from routing the pulse identity through the same predicate. RetailWorldPicker, RetailSelectionScene, WbDrawDispatcher, and EquippedChildRenderController are untouched, as are all wire and physics paths. The slice REMOVES an undocumented deviation (Attached projections excluded from pick eligibility versus retail's part-id pick) and introduces none, so no retail-divergence-register row is owed in either direction. Gates: dotnet build green; AcDream.App.Tests 3,951 passed / 3 skipped; complete Release solution 9,783 passed / 5 skipped; tools\run-connected-world-lifecycle-gate.ps1 RESULT=PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
9fdfe68c7f
commit
f6db964fd5
9 changed files with 570 additions and 25 deletions
|
|
@ -1178,7 +1178,77 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a top-level object that currently participates in picking,
|
||||
/// Resolves a current equipped-child projection whose composed frame is
|
||||
/// live. Retail installs an equipped item as a first-class
|
||||
/// <c>CPhysicsObj</c> with its own object id and part array
|
||||
/// (<c>CPhysicsObj::add_child @ 0x0050F870</c> via
|
||||
/// <c>CSetup::GetHoldingLocation @ 0x005213F0</c>), and
|
||||
/// <c>CPhysicsObj::UpdateChild @ 0x00512D50</c> recomposes
|
||||
/// <c>Frame::combine(parent part frame, holding frame)</c> into that child's
|
||||
/// own <c>m_position</c> every frame. An attached projection therefore has
|
||||
/// real world presence even though it is deliberately absent from the
|
||||
/// interaction/radar/auto-target visible set.
|
||||
/// </summary>
|
||||
public bool TryGetAttachedProjectedRecord(
|
||||
uint serverGuid,
|
||||
out LiveEntityRecord record)
|
||||
{
|
||||
if (_projections.TryGetCurrent(serverGuid, out LiveEntityRecord found)
|
||||
&& found.WorldEntity is not null
|
||||
&& found.ProjectionKind is LiveEntityProjectionKind.Attached
|
||||
&& found.IsSpatiallyProjected)
|
||||
{
|
||||
record = found;
|
||||
return true;
|
||||
}
|
||||
|
||||
record = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves an object that currently participates in mouse picking.
|
||||
/// Retail's only candidacy rule is "a drawn part whose owning physics
|
||||
/// object has a nonzero id" (<c>CPhysicsPart::Draw @ 0x0050D7A0</c> guards
|
||||
/// on <c>CPhysicsPart::get_physobj_id @ 0x0050D490</c>, and
|
||||
/// <c>Render::GfxObjUnderSelectionRay @ 0x0054C740</c> accumulates the
|
||||
/// hit under that id) — there is no parent redirection and no
|
||||
/// wielded-specific gate, so a click on a wielded weapon returns the
|
||||
/// weapon's own GUID. Picking therefore admits attached projections on top
|
||||
/// of the ordinary top-level visible set. This is deliberately NOT the
|
||||
/// interaction-eligible predicate: radar, auto-target, and
|
||||
/// MoveTo/Sticky establishment must stay wielded-item free.
|
||||
/// </summary>
|
||||
public bool TryGetPickEligibleRecord(
|
||||
uint serverGuid,
|
||||
out LiveEntityRecord record)
|
||||
=> TryGetInteractionEligibleRecord(serverGuid, out record)
|
||||
|| TryGetAttachedProjectedRecord(serverGuid, out record);
|
||||
|
||||
/// <summary>
|
||||
/// Pick eligibility bound to one logical incarnation. A stale published
|
||||
/// frame must never retarget a replacement which reused the server GUID.
|
||||
/// </summary>
|
||||
public bool TryGetPickEligibleRecord(
|
||||
uint serverGuid,
|
||||
uint localEntityId,
|
||||
out LiveEntityRecord record)
|
||||
{
|
||||
if (serverGuid != 0u
|
||||
&& localEntityId != 0u
|
||||
&& TryGetPickEligibleRecord(serverGuid, out LiveEntityRecord found)
|
||||
&& found.WorldEntity!.Id == localEntityId)
|
||||
{
|
||||
record = found;
|
||||
return true;
|
||||
}
|
||||
|
||||
record = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a top-level object that currently participates in
|
||||
/// targeting, radar, and wire-driven MoveTo/Sticky establishment.
|
||||
/// Pending, attached, and Hidden projections are intentionally excluded.
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue