diff --git a/docs/plans/2026-07-23-world-interaction-completion.md b/docs/plans/2026-07-23-world-interaction-completion.md
index 4598c6b9..ba8d289d 100644
--- a/docs/plans/2026-07-23-world-interaction-completion.md
+++ b/docs/plans/2026-07-23-world-interaction-completion.md
@@ -422,6 +422,26 @@ The own-wielded `sr_Use` gate (`0x004E5BE9`) ships through the new
complete Release solution 9,783/5 skips, connected world-lifecycle gate
`RESULT=PASS`.
+**Correction 2026-07-29 (Opus review finding F1, HIGH).** Making a remote's
+wielded weapon selectable made the pickup chain reachable end to end for the
+first time, and acdream had never ported
+`ItemHolder::AttemptToPlaceInContainer_IsItemLegal @ 0x005870C0`'s arm at
+`0x005872B7` — `!ACCWeenieObject::IsOwnedByPlayer(item) && item->pwd._location
+!= 0` — so `SelectionPickUp` on another character's weapon installed a real
+approach and a wire request the server rejects. That arm now ships, with
+retail's own notice (`0x007e2228`) and retail's placement ahead of every other
+`AttemptToPlaceInContainer @ 0x00588140` stage: one local message, no movement,
+no request. The player's own wielded item is `IsOwnedByPlayer`, so it passes
+the arm and takes retail's `PositionState.WIELDED` route —
+`ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680` records
+`IR_PUT_IN_CONTAINER`, not `IR_PICK_UP` — dispatching its container transfer
+immediately with no approach. `TryGetApproach` now refuses attached children
+outright for the same reason, so no approach can anchor on a wielder's root.
+Picking, selection, examination, and the marker anchor are unchanged. The
+slice therefore introduces no divergence, contrary to what `f6db964f`'s message
+claimed; no register row is owed. Gates: App tests 3,960/3 skips, complete
+Release solution 9,792/5 skips, connected world-lifecycle gate `RESULT=PASS`.
+
### The retail mechanism
Retail picking is render-coupled, not a scene-graph ray walk. A click arms a
diff --git a/src/AcDream.App/Interaction/SelectionInteractionController.cs b/src/AcDream.App/Interaction/SelectionInteractionController.cs
index 534fd46f..9c788f91 100644
--- a/src/AcDream.App/Interaction/SelectionInteractionController.cs
+++ b/src/AcDream.App/Interaction/SelectionInteractionController.cs
@@ -255,7 +255,17 @@ internal sealed class SelectionInteractionController
// current-ground-object contents straight to
// ItemHolder::AttemptToPlaceInContainer @ 0x00588140. A corpse or
// chest child has no independent 3-D projection to approach.
- if (_items.IsInCurrentGroundObject(itemGuid))
+ //
+ // ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680 records the
+ // IR_PICK_UP world request only for PositionState.IN_3D_VIEW and
+ // treats WIELDED as a plain IR_PUT_IN_CONTAINER transfer, so the
+ // player's own wielded item is unwielded in place with no approach.
+ // The ownership conjunct keeps this shortcut behind IsItemLegal's
+ // 0x005872B7 arm, which ValidatePickupTarget enforces below: a wielded
+ // item that is not the player's must never reach a wire request.
+ if (_items.IsInCurrentGroundObject(itemGuid)
+ || (_query.IsWieldedPositionState(itemGuid)
+ && _items.IsOwnedByPlayer(itemGuid)))
{
var contained = new RuntimePendingPickup(
Token: 0u,
@@ -505,6 +515,20 @@ internal sealed class SelectionInteractionController
}
}
+ ///
+ /// ItemHolder::AttemptToPlaceInContainer @ 0x00588140 runs
+ /// AttemptToPlaceInContainer_IsItemLegal @ 0x005870C0 first, at
+ /// 0x00588173 — ahead of container legality, auto-merge, the
+ /// container walk, and the only CM_Inventory::Event_PutItemInContainer
+ /// emitter (ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680).
+ /// A rejection is therefore one local
+ /// ECM_UI::SendNotice_DisplayStringInfo(0x1a, …) and nothing else:
+ /// no wire request and no movement.
+ /// CPlayerSystem::PlaceInBackpack @ 0x0055D8C0 then withdraws the
+ /// waiting slot it had published (SetWaitingState(obj, 0) plus
+ /// CM_Item::SendNotice_EndPendingInPlayer at 0x0055D918),
+ /// which is what a false return drives here.
+ ///
private bool ValidatePickupTarget(uint serverGuid, bool showToast)
{
if (_query.IsCreature(serverGuid))
@@ -513,6 +537,24 @@ internal sealed class SelectionInteractionController
_toast?.Invoke(RetailMessages.CannotPickUpCreatures);
return false;
}
+ // IsItemLegal's arm at 0x005872B7 rejects
+ // `!ACCWeenieObject::IsOwnedByPlayer(item) && item->pwd._location != 0`
+ // with the notice at 0x005872DB. Retail runs it after the stuck arm at
+ // 0x00587240; the two are disjoint, because the stuck arm fires only
+ // when _containerID and _wielderID are both zero while this one needs a
+ // wielded _location, so acdream's fused stuck/type predicate may follow.
+ // Equipped-child picking made a remote character's wielded item
+ // selectable, which is what makes this arm reachable.
+ if (_query.IsWieldedPositionState(serverGuid)
+ && !_items.IsOwnedByPlayer(serverGuid))
+ {
+ if (showToast)
+ {
+ _toast?.Invoke(RetailMessages.BeingWieldedBySomeoneElse(
+ _query.Describe(serverGuid)));
+ }
+ return false;
+ }
if (_query.IsPickupable(serverGuid))
return true;
if (showToast)
diff --git a/src/AcDream.App/Interaction/WorldSelectionQuery.cs b/src/AcDream.App/Interaction/WorldSelectionQuery.cs
index efa9b877..08375d88 100644
--- a/src/AcDream.App/Interaction/WorldSelectionQuery.cs
+++ b/src/AcDream.App/Interaction/WorldSelectionQuery.cs
@@ -48,6 +48,7 @@ internal interface IWorldSelectionQuery
bool IsUseable(uint serverGuid);
bool IsPickupable(uint serverGuid);
bool IsWieldedByPlayer(uint serverGuid);
+ bool IsWieldedPositionState(uint serverGuid);
bool TryGetApproach(uint serverGuid, out InteractionApproach approach);
Vector3? GetCombatCameraTargetPoint(uint serverGuid);
}
@@ -388,6 +389,27 @@ internal sealed class WorldSelectionQuery
&& item.WielderId == playerGuid;
}
+ ///
+ /// ACCWeenieObject::DeterminePositionState @ 0x0058BE70 resolves
+ /// PositionState.WIELDED (acclient.h:6802) as a zero
+ /// pwd._containerID with a nonzero pwd._location;
+ /// IN_CONTAINER wins when both are set.
+ /// ClientObject.CurrentlyEquippedLocation is acdream's projection of
+ /// pwd._location (the CurrentWieldedLocation PublicWeenieDesc
+ /// field, acclient.h:37175).
+ ///
+ ///
+ /// Two retail gates read this state. The pickup legality arm at
+ /// 0x005872B7 pairs it with ownership, and
+ /// ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680 records the
+ /// IR_PICK_UP world request only for IN_3D_VIEW, treating
+ /// WIELDED as a plain IR_PUT_IN_CONTAINER transfer.
+ ///
+ public bool IsWieldedPositionState(uint serverGuid)
+ => _objects.Get(serverGuid) is { } item
+ && item.ContainerId == 0u
+ && item.CurrentlyEquippedLocation != EquipMask.None;
+
/// ItemHolder::DetermineUseResult @ 0x00588460 pickup gate.
public bool IsPickupable(uint serverGuid)
{
@@ -399,11 +421,24 @@ internal sealed class WorldSelectionQuery
return ((spawn.ItemType ?? 0u) & SmallItemMask) != 0u;
}
+ ///
+ /// Builds the world-pickup approach. Only retail's
+ /// PositionState.IN_3D_VIEW objects have one:
+ /// ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680 records
+ /// IR_PICK_UP for that state alone, and an attached child is by
+ /// construction not an independent 3-D object — its bookkeeping
+ /// WorldEntity.Position carries the PARENT's composed root
+ /// (EquippedChildRenderController.ApplyParentWorldPose), not the
+ /// child frame CPhysicsObj::UpdateChild @ 0x00512D50 composes.
+ /// Refusing the attached case keeps an approach from ever anchoring on a
+ /// wielder; wielded items reach their container transfer without one.
+ ///
public bool TryGetApproach(
uint serverGuid,
out InteractionApproach approach)
{
if (_playerPose() is not { } player
+ || _liveEntities.TryGetAttachedProjectedRecord(serverGuid, out _)
|| !TryGetInteractionTarget(serverGuid, out WorldInteractionTarget target))
{
approach = default;
diff --git a/src/AcDream.Core/Ui/RetailMessages.cs b/src/AcDream.Core/Ui/RetailMessages.cs
index 5689705d..dbc7871a 100644
--- a/src/AcDream.Core/Ui/RetailMessages.cs
+++ b/src/AcDream.Core/Ui/RetailMessages.cs
@@ -55,6 +55,27 @@ public static class RetailMessages
///
public const string CannotPickUpCreatures = "You cannot pick up creatures!";
+ ///
+ /// Retail: "The %s is being wielded by someone else!".
+ /// Data: 0x007e2228 (line 1033029). Runtime sprintf at
+ /// 0x005872db (line 401606) inside
+ /// ItemHolder::AttemptToPlaceInContainer_IsItemLegal @ 0x005870c0,
+ /// on the reject arm at 0x005872b7 that fires when
+ /// !ACCWeenieObject::IsOwnedByPlayer(item) &&
+ /// item->pwd._location != 0. Shown when the player tries to pick up
+ /// an item another character is wielding.
+ ///
+ ///
+ /// Not to be confused with the punctuation-free
+ /// "The %s is being wielded by someone else" at 0x007cd350,
+ /// which belongs to the wield/wear notice block and is emitted from a
+ /// different function at 0x00560aef. The pickup gate's own literal
+ /// block runs 0x007e21f0–0x007e234c, one string per
+ /// IsItemLegal arm, in reverse code order.
+ ///
+ public static string BeingWieldedBySomeoneElse(string entityName)
+ => $"The {entityName} is being wielded by someone else!";
+
///
/// Retail: "Cannot be used with %s".
/// Data: 0x007cc834 (line 1024669). Runtime sprintf at
diff --git a/tests/AcDream.App.Tests/Combat/CombatCameraTargetSourceTests.cs b/tests/AcDream.App.Tests/Combat/CombatCameraTargetSourceTests.cs
index 24f89987..8c0bc9e1 100644
--- a/tests/AcDream.App.Tests/Combat/CombatCameraTargetSourceTests.cs
+++ b/tests/AcDream.App.Tests/Combat/CombatCameraTargetSourceTests.cs
@@ -61,6 +61,7 @@ public sealed class CombatCameraTargetSourceTests
public bool IsUseable(uint serverGuid) => false;
public bool IsPickupable(uint serverGuid) => false;
public bool IsWieldedByPlayer(uint serverGuid) => false;
+ public bool IsWieldedPositionState(uint serverGuid) => false;
public bool TryGetApproach(uint serverGuid, out InteractionApproach approach)
{
approach = default;
diff --git a/tests/AcDream.App.Tests/Interaction/SelectionInteractionControllerTests.cs b/tests/AcDream.App.Tests/Interaction/SelectionInteractionControllerTests.cs
index 5f3e838a..1bccc07f 100644
--- a/tests/AcDream.App.Tests/Interaction/SelectionInteractionControllerTests.cs
+++ b/tests/AcDream.App.Tests/Interaction/SelectionInteractionControllerTests.cs
@@ -18,6 +18,10 @@ public sealed class SelectionInteractionControllerTests
private const uint Player = 0x5000_0001u;
private const uint Target = 0x7000_0001u;
private const uint GroundContainer = 0x7000_0010u;
+ private const uint Wielder = 0x7000_0100u;
+ private const uint RemoteWeapon = 0x7000_0101u;
+ private const uint OwnWeapon = 0x7000_0102u;
+ private const uint GroundWeapon = 0x7000_0103u;
private sealed class Query : IWorldSelectionQuery
{
@@ -29,6 +33,13 @@ public sealed class SelectionInteractionControllerTests
public bool Useable { get; set; } = true;
public bool Pickupable { get; set; } = true;
public bool WieldedByPlayer { get; set; }
+ ///
+ /// Guids whose ACCWeenieObject::DeterminePositionState @
+ /// 0x0058BE70 result is PositionState.WIELDED. Ownership is
+ /// NOT faked — the controller resolves it through the real
+ /// .
+ ///
+ public HashSet WieldedPositionStates { get; } = new();
public bool CaptureIdentity { get; set; } = true;
public uint LocalEntityId { get; set; } = 101u;
public ClosestCombatTarget? Closest { get; set; }
@@ -59,6 +70,8 @@ public sealed class SelectionInteractionControllerTests
public bool IsUseable(uint serverGuid) => Useable;
public bool IsPickupable(uint serverGuid) => Pickupable;
public bool IsWieldedByPlayer(uint serverGuid) => WieldedByPlayer;
+ public bool IsWieldedPositionState(uint serverGuid)
+ => WieldedPositionStates.Contains(serverGuid);
public Vector3? GetCombatCameraTargetPoint(uint serverGuid) => null;
public bool TryGetApproach(uint serverGuid, out InteractionApproach approach)
@@ -194,19 +207,22 @@ public sealed class SelectionInteractionControllerTests
Items.PendingBackpackPlacementCancelled += CancelledPlacements.Add;
}
- public void SetApproach(bool closeRange, uint localEntityId = 101u)
+ public void SetApproach(
+ bool closeRange,
+ uint localEntityId = 101u,
+ uint serverGuid = Target)
{
var entity = new WorldEntity
{
Id = localEntityId,
- ServerGuid = Target,
+ ServerGuid = serverGuid,
SourceGfxObjOrSetupId = 0x0200_0001u,
Position = new Vector3(5f, 0f, 0f),
Rotation = Quaternion.Identity,
MeshRefs = [],
};
Query.Approach = new InteractionApproach(
- new WorldInteractionTarget(Target, localEntityId, entity),
+ new WorldInteractionTarget(serverGuid, localEntityId, entity),
new PlayerInteractionPose(0x0101_0001u, Vector3.Zero),
0.6f,
closeRange,
@@ -746,6 +762,181 @@ public sealed class SelectionInteractionControllerTests
Assert.Empty(h.Transport.Uses);
}
+ ///
+ /// Registers a wielded weapon exactly as ClientObjectTable.Ingest
+ /// projects a CreateObject PublicWeenieDesc that carries Wielder plus
+ /// CurrentWieldedLocation. Ownership is resolved by the real
+ /// ACCWeenieObject::IsOwnedByObject @ 0x0058CEB0 port, not a fake.
+ ///
+ private static void AddWieldedWeapon(Harness h, uint guid, uint wielderId)
+ {
+ h.Objects.AddOrUpdate(new ClientObject
+ {
+ ObjectId = guid,
+ Name = $"Weapon {guid:X8}",
+ Type = ItemType.MeleeWeapon,
+ WielderId = wielderId,
+ CurrentlyEquippedLocation = EquipMask.MeleeWeapon,
+ });
+ h.Query.WieldedPositionStates.Add(guid);
+ }
+
+ ///
+ /// ItemHolder::AttemptToPlaceInContainer_IsItemLegal @ 0x005870C0
+ /// rejects !IsOwnedByPlayer(item) && item->pwd._location != 0
+ /// at 0x005872B7 with the notice at 0x005872DB, and it runs
+ /// at 0x00588173 before every other stage of
+ /// AttemptToPlaceInContainer @ 0x00588140. So: one local message,
+ /// no approach, no wire request, no waiting slot.
+ ///
+ [Fact]
+ public void PickupOfARemotesWieldedItemIsRejectedLocallyWithNoMovementOrRequest()
+ {
+ var h = new Harness();
+ h.Objects.AddOrUpdate(new ClientObject
+ {
+ ObjectId = Wielder,
+ Name = "Remote",
+ Type = ItemType.Creature,
+ });
+ AddWieldedWeapon(h, RemoteWeapon, wielderId: Wielder);
+ h.SetApproach(closeRange: false, serverGuid: RemoteWeapon);
+ h.Selection.Select(RemoteWeapon, SelectionChangeSource.World);
+
+ Assert.True(h.Controller.HandleInputAction(InputAction.SelectionPickUp));
+ h.Controller.DrainOutbound();
+
+ Assert.Contains(
+ // Query.Describe is the fake's ACCWeenieObject::GetObjectNameWide.
+ $"The Target {RemoteWeapon:X8} is being wielded by someone else!",
+ h.Toasts);
+ Assert.Empty(h.Transport.Pickups);
+ Assert.Empty(h.Transport.Uses);
+ Assert.Empty(h.Movement.Approaches);
+ Assert.Empty(h.PendingPlacements);
+ }
+
+ ///
+ /// The same gate must hold on the direct
+ /// CPlayerSystem::PlaceInBackpack @ 0x0055D8C0 entry point, whose
+ /// published waiting slot retail withdraws at 0x0055D918
+ /// (SetWaitingState(obj, 0) + SendNotice_EndPendingInPlayer).
+ ///
+ [Fact]
+ public void DirectBackpackPlacementOfARemotesWieldedItemWithdrawsItsWaitingSlot()
+ {
+ var h = new Harness();
+ AddWieldedWeapon(h, RemoteWeapon, wielderId: Wielder);
+ h.SetApproach(closeRange: true, serverGuid: RemoteWeapon);
+
+ Assert.True(h.Items.PlaceWorldItemInBackpack(RemoteWeapon));
+
+ Assert.Contains(
+ // Query.Describe is the fake's ACCWeenieObject::GetObjectNameWide.
+ $"The Target {RemoteWeapon:X8} is being wielded by someone else!",
+ h.Toasts);
+ Assert.Empty(h.Transport.Pickups);
+ Assert.Empty(h.Movement.Approaches);
+ Assert.Single(h.PendingPlacements);
+ Assert.Equal(
+ new[] { RemoteWeapon },
+ h.CancelledPlacements.Select(p => p.ItemId));
+ }
+
+ ///
+ /// Slice 4 non-regression: the rejected item stays fully selectable and
+ /// examinable. Retail's sr_Select/sr_Examine branches of
+ /// RecvNotice_SmartBoxObjectFound @ 0x004E5AD0 never consult
+ /// IsItemLegal.
+ ///
+ [Fact]
+ public void ARemotesWieldedItemStaysSelectableAndExaminable()
+ {
+ var h = new Harness();
+ AddWieldedWeapon(h, RemoteWeapon, wielderId: Wielder);
+ h.Query.Picked = RemoteWeapon;
+
+ Assert.True(h.Controller.HandleInputAction(InputAction.SelectRight));
+
+ Assert.Equal(RemoteWeapon, h.Selection.SelectedObjectId);
+ Assert.Equal(new[] { "pick", "pulse", "examine" }, h.Query.Events);
+ Assert.Equal(new[] { RemoteWeapon }, h.Examines);
+ }
+
+ ///
+ /// Positive control: the identical ItemType lying in the world is not in
+ /// PositionState.WIELDED (DeterminePositionState @ 0x0058BE70
+ /// needs a nonzero pwd._location) and picks up normally.
+ ///
+ [Fact]
+ public void AGroundItemOfTheSameTypeStillPicksUpNormally()
+ {
+ var h = new Harness();
+ h.Objects.AddOrUpdate(new ClientObject
+ {
+ ObjectId = GroundWeapon,
+ Name = "Ground weapon",
+ Type = ItemType.MeleeWeapon,
+ });
+ h.SetApproach(closeRange: false, serverGuid: GroundWeapon);
+ h.Selection.Select(GroundWeapon, SelectionChangeSource.World);
+
+ Assert.True(h.Controller.HandleInputAction(InputAction.SelectionPickUp));
+ h.Controller.DrainOutbound();
+
+ Assert.Equal(new[] { (GroundWeapon, Player, 0) }, h.Transport.Pickups);
+ Assert.Single(h.Movement.Approaches);
+ Assert.Empty(h.CancelledPlacements);
+ Assert.DoesNotContain(h.Toasts, text => text.Contains("wielded"));
+ }
+
+ ///
+ /// The player's OWN wielded item is IsOwnedByPlayer, so
+ /// IsItemLegal's 0x005872B7 arm passes it. Its
+ /// PositionState is WIELDED rather than IN_3D_VIEW, so
+ /// ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680 records
+ /// IR_PUT_IN_CONTAINER instead of IR_PICK_UP: the transfer
+ /// goes out immediately, with no world approach.
+ ///
+ [Fact]
+ public void PickupOfThePlayersOwnWieldedItemTransfersImmediatelyWithoutApproach()
+ {
+ var h = new Harness();
+ AddWieldedWeapon(h, OwnWeapon, wielderId: Player);
+ h.SetApproach(closeRange: true, serverGuid: OwnWeapon);
+ h.Selection.Select(OwnWeapon, SelectionChangeSource.World);
+
+ Assert.True(h.Controller.HandleInputAction(InputAction.SelectionPickUp));
+ h.Controller.DrainOutbound();
+
+ Assert.Equal(new[] { (OwnWeapon, Player, 0) }, h.Transport.Pickups);
+ Assert.Empty(h.Movement.Approaches);
+ Assert.Empty(h.CancelledPlacements);
+ Assert.DoesNotContain(h.Toasts, text => text.Contains("wielded"));
+ }
+
+ ///
+ /// The WIELDED no-approach shortcut must never outrun the legality gate:
+ /// ownership alone decides which of the two wielded items may transfer.
+ ///
+ [Fact]
+ public void TheWieldedShortcutIsGatedOnOwnershipNotOnWieldedStateAlone()
+ {
+ var h = new Harness();
+ AddWieldedWeapon(h, RemoteWeapon, wielderId: Wielder);
+ AddWieldedWeapon(h, OwnWeapon, wielderId: Player);
+
+ h.SetApproach(closeRange: true, serverGuid: RemoteWeapon);
+ Assert.True(h.Items.PlaceWorldItemInBackpack(RemoteWeapon));
+ Assert.Empty(h.Transport.Pickups);
+
+ h.SetApproach(closeRange: true, serverGuid: OwnWeapon);
+ Assert.True(h.Items.PlaceWorldItemInBackpack(OwnWeapon));
+
+ Assert.Equal(new[] { (OwnWeapon, Player, 0) }, h.Transport.Pickups);
+ Assert.Empty(h.Movement.Approaches);
+ }
+
[Fact]
public void DragReleasePulsesTheDropTargetWithoutChangingSelection()
{
diff --git a/tests/AcDream.App.Tests/Interaction/WorldSelectionQueryTests.cs b/tests/AcDream.App.Tests/Interaction/WorldSelectionQueryTests.cs
index 633c2f02..e1c4a0b6 100644
--- a/tests/AcDream.App.Tests/Interaction/WorldSelectionQueryTests.cs
+++ b/tests/AcDream.App.Tests/Interaction/WorldSelectionQueryTests.cs
@@ -124,7 +124,8 @@ public sealed class WorldSelectionQueryTests
ItemType type,
uint wielderId,
Matrix4x4? childRoot = null,
- float? objScale = null)
+ float? objScale = null,
+ EquipMask equippedLocation = EquipMask.MeleeWeapon)
{
WorldSession.EntitySpawn spawn = Spawn(guid, 1) with
{
@@ -148,6 +149,10 @@ public sealed class WorldSelectionQueryTests
Name = $"Object {guid:X8}",
Type = type,
WielderId = wielderId,
+ // CreateObject's PublicWeenieDesc carries CurrentWieldedLocation
+ // alongside Wielder for every equipped child; retail's
+ // pwd._location is what DeterminePositionState reads.
+ CurrentlyEquippedLocation = equippedLocation,
});
if (childRoot is { } root)
ChildRoots[entity.Id] = root;
@@ -743,6 +748,127 @@ public sealed class WorldSelectionQueryTests
Assert.Equal(4f, radius);
}
+ ///
+ /// ACCWeenieObject::DeterminePositionState @ 0x0058BE70:
+ /// BEING_REMOVED, else IN_CONTAINER when pwd._containerID != 0, else
+ /// pwd._location != 0 selects WIELDED over IN_3D_VIEW. Both the
+ /// pickup-legality arm at 0x005872B7 and the IR_PICK_UP-vs-
+ /// IR_PUT_IN_CONTAINER split in
+ /// ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680 read it.
+ ///
+ [Fact]
+ public void WieldedPositionStateFollowsRetailsContainerThenLocationOrder()
+ {
+ var h = new Harness();
+ const uint ground = 0x7000_0030u;
+ const uint stowed = 0x7000_0031u;
+ h.Add(ground, Vector3.UnitX, ItemType.MeleeWeapon);
+ h.Add(
+ Wielder,
+ new Vector3(0f, 0f, -10f),
+ ItemType.Creature,
+ SelectedObjectHealthPolicy.BfAttackable);
+ h.AddAttached(
+ RemoteWeapon,
+ new Vector3(0f, 0f, -10f),
+ ItemType.MeleeWeapon,
+ wielderId: Wielder,
+ childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f));
+ h.AddAttached(
+ OwnWeapon,
+ Vector3.Zero,
+ ItemType.MeleeWeapon,
+ wielderId: Player,
+ childRoot: Matrix4x4.CreateTranslation(0f, 0.5f, 1f));
+ h.Objects.AddOrUpdate(new ClientObject
+ {
+ ObjectId = stowed,
+ Name = "Stowed weapon",
+ Type = ItemType.MeleeWeapon,
+ ContainerId = Player,
+ CurrentlyEquippedLocation = EquipMask.MeleeWeapon,
+ });
+
+ Assert.True(h.Query.IsWieldedPositionState(RemoteWeapon));
+ Assert.True(h.Query.IsWieldedPositionState(OwnWeapon));
+ Assert.False(h.Query.IsWieldedPositionState(ground));
+ // IN_CONTAINER wins over a stale wielded location.
+ Assert.False(h.Query.IsWieldedPositionState(stowed));
+ // Unknown weenie: retail's GetWeenieObject miss rejects at 0x005870DC.
+ Assert.False(h.Query.IsWieldedPositionState(0x7000_00FFu));
+ }
+
+ ///
+ /// The wielded-item pickup gate lives in
+ /// ItemHolder::AttemptToPlaceInContainer_IsItemLegal @ 0x005870C0,
+ /// NOT in the pick. Retail's pick is render-coupled and consults no
+ /// ownership or position state, so Slice 4's selection behavior stands.
+ ///
+ [Fact]
+ public void PickAndSelectionStayOpenOnARemotesWieldedItemTheGateWillRefuse()
+ {
+ var h = new Harness();
+ WorldEntity wielder = h.Add(
+ Wielder,
+ new Vector3(0f, 0f, -10f),
+ ItemType.Creature,
+ SelectedObjectHealthPolicy.BfAttackable);
+ WorldEntity weapon = h.AddAttached(
+ RemoteWeapon,
+ wielder.Position,
+ ItemType.MeleeWeapon,
+ wielderId: Wielder,
+ childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f));
+ h.PublishParts(
+ (wielder, wielder.Position),
+ (weapon, new Vector3(0f, 0f, -5f)));
+
+ Assert.Equal(RemoteWeapon, h.Query.PickAtCursor(includeSelf: false));
+ Assert.True(h.Query.TryCaptureIdentity(RemoteWeapon, out uint localId));
+ Assert.Equal(weapon.Id, localId);
+ Assert.True(h.Query.IsCurrent(RemoteWeapon, weapon.Id));
+ Assert.True(h.Query.TryGetInteractionTarget(RemoteWeapon, out _));
+ Assert.NotNull(h.Query.ResolveVividTargetInfo(RemoteWeapon));
+ Assert.Equal($"Object {RemoteWeapon:X8}", h.Query.Describe(RemoteWeapon));
+
+ // The pick predicates are untouched: the retail gate is the wielded
+ // position state combined with ownership, not pickability.
+ Assert.True(h.Query.IsPickupable(RemoteWeapon));
+ Assert.True(h.Query.IsWieldedPositionState(RemoteWeapon));
+ Assert.False(h.Objects.IsOwnedByObject(RemoteWeapon, Player));
+ }
+
+ ///
+ /// The world-pickup approach belongs to PositionState.IN_3D_VIEW
+ /// alone (ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680).
+ /// An attached child's bookkeeping pose is its wielder's root, so an
+ /// approach built from it would walk the player to the wielder.
+ ///
+ [Fact]
+ public void ApproachRefusesAnAttachedChildRatherThanAnchoringOnItsWielder()
+ {
+ var h = new Harness();
+ const uint ground = 0x7000_0032u;
+ var wielderRoot = new Vector3(20f, 0f, 0f);
+ h.Add(ground, new Vector3(3f, 0f, 0f), ItemType.MeleeWeapon);
+ h.Add(
+ Wielder,
+ wielderRoot,
+ ItemType.Creature,
+ SelectedObjectHealthPolicy.BfAttackable);
+ h.AddAttached(
+ RemoteWeapon,
+ wielderRoot,
+ ItemType.MeleeWeapon,
+ wielderId: Wielder,
+ childRoot: Matrix4x4.CreateTranslation(20f, 0f, 1.2f));
+
+ Assert.False(h.Query.TryGetApproach(RemoteWeapon, out _));
+ // The same call still succeeds for a genuine 3-D-view item.
+ Assert.True(h.Query.TryGetApproach(ground, out InteractionApproach loose));
+ Assert.Equal(ground, loose.Target.ServerGuid);
+ }
+
private static SelectionCameraSnapshot Camera()
{
Matrix4x4 view = Matrix4x4.CreateLookAt(
diff --git a/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs b/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs
index dc484e14..3697c088 100644
--- a/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs
+++ b/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs
@@ -1292,6 +1292,7 @@ public sealed class CurrentGameRuntimeAdapterTests
public bool IsUseable(uint serverGuid) => serverGuid == target;
public bool IsPickupable(uint serverGuid) => false;
public bool IsWieldedByPlayer(uint serverGuid) => false;
+ public bool IsWieldedPositionState(uint serverGuid) => false;
public bool TryGetApproach(
uint serverGuid,