acdream/src/AcDream.Core/Ui/RetailMessages.cs
Erik 0ccbb4e52c fix(interaction): port retail's wielded-item pickup rejection (Slice 4 F1)
Slice 4 made a remote character's wielded weapon selectable, which made the
pickup chain reachable end to end for the first time: SelectionPickUp on
another player's weapon captured identity, passed ValidatePickupTarget (which
checked only the Stuck flag and the small-item mask, and a MeleeWeapon clears
both), installed a real non-autonomous approach through
PlayerInteractionMovementSink, and then sent a pickup request the server
rejects. Retail does none of that.

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). IsItemLegal's arm at
0x005872B7 rejects `!ACCWeenieObject::IsOwnedByPlayer(item) &&
item->pwd._location != 0` with one local
ECM_UI::SendNotice_DisplayStringInfo(0x1a, ...), and
CPlayerSystem::PlaceInBackpack @ 0x0055D8C0 then withdraws the waiting slot it
had published (SetWaitingState(obj, 0) + SendNotice_EndPendingInPlayer at
0x0055D918). No request, no movement. acdream had never ported that arm; it
was harmless while wielded children were unpickable and stopped being harmless
at f6db964f.

The notice is data_7e2228, "The %s is being wielded by someone else!" -- WITH
the exclamation mark. IsItemLegal's six strings occupy one contiguous literal
block, 0x007e21f0 through 0x007e234c, one per arm in reverse code order, and
the two neighbours already ported here (0x007e227c "The %s cannot be picked
up!" at 0x00587264, 0x007e22b4 "You cannot pick up creatures!" at 0x005871f4)
pin it. The punctuation-free 0x007cd350 variant belongs to the wield/wear
block and is emitted from a different function at 0x00560aef.

pwd._location is the PublicWeenieDesc CurrentWieldedLocation field
(acclient.h:37175), which acdream projects as
ClientObject.CurrentlyEquippedLocation, and ACCWeenieObject::IsOwnedByPlayer
@ 0x0058D160 is IsOwnedByObject(this, player_id) -- already ported as
ClientObjectTable.IsOwnedByObject @ 0x0058CEB0 and reached here through the
existing ItemInteractionController.IsOwnedByPlayer. The arm reads pwd._location
verbatim rather than adding a WielderId belt-and-braces test, because retail's
predicate is the thing being ported.

The player's OWN wielded item is IsOwnedByPlayer, so retail passes it and takes
a different route. ACCWeenieObject::DeterminePositionState @ 0x0058BE70 gives
it PositionState.WIELDED (acclient.h:6802) rather than IN_3D_VIEW, and
UIAttemptPutInContainer records IR_PICK_UP only for IN_3D_VIEW, treating
WIELDED and IN_CONTAINER alike as a plain IR_PUT_IN_CONTAINER transfer. So an
own-wielded item is unwielded in place: the request goes out immediately with
no approach, joining the existing current-ground-object shortcut. The shortcut
carries an ownership conjunct so it can never outrun the 0x005872B7 gate.

TryGetApproach now refuses attached children outright, for the same
IN_3D_VIEW reason. An Attached projection's bookkeeping WorldEntity.Position
carries the PARENT's composed root (EquippedChildRenderController
.ApplyParentWorldPose), not the child frame CPhysicsObj::UpdateChild @
0x00512D50 composes, so an approach built from it walked toward the wielder.
Slice 4 de-parented the marker anchor but left this one parent-derived; no
approach can anchor on a wielder now.

The pick predicates are deliberately untouched. Picking, selecting, examining,
lighting-pulse identity, and the vivid-marker anchor on a remote's wielded
weapon all behave exactly as Slice 4 shipped them -- retail's sr_Select and
sr_Examine branches of RecvNotice_SmartBoxObjectFound @ 0x004E5AD0 never
consult IsItemLegal. The gate is the transaction, not the pick.

f6db964f's message asserted the slice introduced no deviation and owed no
retail-divergence-register row. That was wrong: the unported 0x005872B7 arm
was a deviation it made reachable. This commit ports the arm in full, matches
retail on the own-wielded path, and removes the parent-derived approach
anchor, so the record is corrected here and no register row is owed.

Gates: dotnet build green; AcDream.App.Tests 3,960 passed / 3 skipped;
complete Release solution 9,792 passed / 5 skipped;
tools\run-connected-world-lifecycle-gate.ps1 -SkipBuild RESULT=PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 19:09:38 +02:00

126 lines
6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

namespace AcDream.Core.Ui;
/// <summary>
/// Verbatim ports of retail UI message strings. Centralised here so
/// future retail-faithful refinements only need to touch one file —
/// and so the call sites stay readable at the interaction layer.
///
/// <para>
/// String text is byte-identical with retail. Each helper cites the
/// retail DAT data address + the runtime use site in the named decomp
/// at <c>docs/research/named-retail/acclient_2013_pseudo_c.txt</c>.
/// </para>
///
/// <para>
/// Pattern mirrors <see cref="RadarBlipColors"/> — typed port of a
/// retail-UI primitive (<c>gmRadarUI::GetBlipColor</c> at
/// <c>0x004d76f0</c>). Add new strings here as we encounter them.
/// </para>
///
/// <para>
/// Members may be added BEFORE their first call site exists — retail
/// strings are a fixed inventory we know we'll need as we port more
/// features. Each member's doc-comment cites its retail anchor +
/// describes the scenario that'll consume it. Removing dead members
/// without a port is fine; we re-grep the decomp.
/// </para>
/// </summary>
public static class RetailMessages
{
/// <summary>
/// Retail: <c>"The %s cannot be used"</c>.
/// Data: <c>0x007e2a70</c> (line 1033115). Runtime sprintf at
/// <c>0x00588ea4</c> (line 403095) inside <c>ItemHolder::UseObject</c>'s
/// IsUseable==0 fallthrough branch. Shown when the player triggers
/// Use on an entity whose useability is USEABLE_UNDEF/USEABLE_NO.
/// </summary>
public static string CannotBeUsed(string entityName)
=> $"The {entityName} cannot be used";
/// <summary>
/// Retail: <c>"The %s can't be picked up!"</c>.
/// Runtime sprintf at <c>0x00587353</c> (line 401589) inside the
/// pickup-flow handler. Shown when the player triggers a pickup on
/// an entity that lacks USEABLE_REMOTE / isn't a small-item type.
/// </summary>
public static string CantBePickedUp(string entityName)
=> $"The {entityName} can't be picked up!";
/// <summary>
/// Retail: <c>"You cannot pick up creatures!"</c>.
/// Data: <c>0x007e22b4</c> (line 1033034). Runtime use at
/// <c>0x005871f4</c> (line 401642) inside the same pickup-flow
/// handler. Shown when the player triggers a pickup on a Creature
/// ItemType (NPCs, monsters, other players).
/// </summary>
public const string CannotPickUpCreatures = "You cannot pick up creatures!";
/// <summary>
/// Retail: <c>"The %s is being wielded by someone else!"</c>.
/// Data: <c>0x007e2228</c> (line 1033029). Runtime sprintf at
/// <c>0x005872db</c> (line 401606) inside
/// <c>ItemHolder::AttemptToPlaceInContainer_IsItemLegal @ 0x005870c0</c>,
/// on the reject arm at <c>0x005872b7</c> that fires when
/// <c>!ACCWeenieObject::IsOwnedByPlayer(item) &amp;&amp;
/// item-&gt;pwd._location != 0</c>. Shown when the player tries to pick up
/// an item another character is wielding.
/// </summary>
/// <remarks>
/// Not to be confused with the punctuation-free
/// <c>"The %s is being wielded by someone else"</c> at <c>0x007cd350</c>,
/// which belongs to the wield/wear notice block and is emitted from a
/// different function at <c>0x00560aef</c>. The pickup gate's own literal
/// block runs <c>0x007e21f0</c><c>0x007e234c</c>, one string per
/// <c>IsItemLegal</c> arm, in reverse code order.
/// </remarks>
public static string BeingWieldedBySomeoneElse(string entityName)
=> $"The {entityName} is being wielded by someone else!";
/// <summary>
/// Retail: <c>"Cannot be used with %s"</c>.
/// Data: <c>0x007cc834</c> (line 1024669). Runtime sprintf at
/// <c>0x0055ee0e</c> (line 363413). Shown when the player tries
/// a two-target Use (e.g., key on lock, lockpick on chest) and
/// the combination is invalid for the source item. The <c>%s</c>
/// is the TARGET entity name. No call site yet — wired in when
/// the two-target Use flow ships.
/// </summary>
public static string CannotBeUsedWith(string targetName)
=> $"Cannot be used with {targetName}";
/// <summary>
/// Retail: <c>"The %s cannot be picked up!"</c>. FORMAL variant.
/// Data: <c>0x007e227c</c> (line 1033033). Runtime sprintf at
/// <c>0x00587264</c> (line 401623). Distinct from
/// <see cref="CantBePickedUp(string)"/> — retail has TWO pickup-
/// reject strings (formal "cannot" + informal "can't"); they
/// fire from different code paths inside the pickup handler.
/// Use whichever the corresponding caller's retail path uses.
/// No call site yet — wired in when the formal-pickup-reject
/// path ships (probably a server-side rejection message).
/// </summary>
public static string CannotBePickedUp(string entityName)
=> $"The {entityName} cannot be picked up!";
/// <summary>
/// Retail: <c>"The %s cannot be used while on a hook, use the
/// '@house hooks on' command to make the hook openable.\n"</c>.
/// Data: <c>0x007d1f68</c> (line 1029591). Shown when the player
/// tries to Use a hooked-up item with the house's "hooks off"
/// preference set. Trailing newline matches retail. No call
/// site yet — wired in when the housing system ships.
/// </summary>
public static string CannotBeUsedWhileOnHook_HooksOff(string entityName)
=> $"The {entityName} cannot be used while on a hook, use the '@house hooks on' command to make the hook openable.\n";
/// <summary>
/// Retail: <c>"The %s cannot be used while on a hook and only
/// the owner may open the hook.\n"</c>.
/// Data: <c>0x007d5f30</c> (line 1030063). Shown when a non-owner
/// tries to Use a hooked-up item in someone else's house.
/// Trailing newline matches retail. No call site yet — wired in
/// when the housing system ships.
/// </summary>
public static string CannotBeUsedWhileOnHook_NotOwner(string entityName)
=> $"The {entityName} cannot be used while on a hook and only the owner may open the hook.\n";
}