fix(ui): retail target-cursor shape (pending over UI, world-object driven) + visible UseDone refusals

Visual gate round 2 (user, retail oracle): the target cursor over UI must
be the 0x27 four-arrows PENDING cursor — retail's UpdateCursorState
(0x00564630) keys valid/invalid solely off the SmartBox found object,
i.e. the WORLD entity under the cursor. And "can't heal myself" turned
out to be TWO stacked causes, both fixed:

- Cursor: UI hover no longer forces TargetInvalid (that arm was a
  non-retail invention) and the doll/status-bar hover providers are
  gone (UiElement.UseTargetGuidProvider deleted). Valid/invalid now
  come from (a) a hovered occupied item slot's own item, or (b) the
  world entity under the cursor via a new worldTargetProvider — the
  B.4b screen-rect picker extracted into GameWindow.PickWorldGuidAtCursor
  and shared by click + hover.
- Self-heal: the world picker always skipped the local player
  (skipServerGuid), so clicking your own toon in target mode could
  never acquire self. Target-use picks now include self (retail lets
  you kit-heal yourself by clicking your character); plain selection
  keeps the exclusion. The doll click still self-targets.
- Silent refusals: the [use-target] log proved the UseWithTarget action
  WAS sent (kit 0x00220008 / TargetType 0x10, target self) — ACE
  refused with WeenieError.YouArentTrainedInHealing (0x04FC) and we
  never parsed UseDone (0x01C7). Now dispatched + surfaced as a chat
  line via WeenieErrorText (interim subset map, register AP-74, #166
  ports the retail String-table lookup).

Full suite green (3,295).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-03 10:27:33 +02:00
parent 047410ccc9
commit 769ebef30d
10 changed files with 151 additions and 61 deletions

View file

@ -1844,7 +1844,12 @@ public sealed class GameWindow : IDisposable
sendWield: (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask),
sendDrop: item => _liveSession?.SendDropItem(item),
toast: text => _debugVm?.AddToast(text));
_cursorFeedbackController = new AcDream.App.UI.CursorFeedbackController(_itemInteractionController);
_cursorFeedbackController = new AcDream.App.UI.CursorFeedbackController(
_itemInteractionController,
// Retail UpdateCursorState (0x00564630) keys target-mode
// valid/invalid off the SmartBox found object — the world
// entity under the cursor, self included.
worldTargetProvider: () => PickWorldGuidAtCursor(includeSelf: true) ?? 0u);
_retailCursorManager = new RetailCursorManager(_dats!, _datLock);
_characterSheetProvider = new AcDream.App.UI.Layout.CharacterSheetProvider(
Objects, LocalPlayer,
@ -12375,26 +12380,27 @@ public sealed class GameWindow : IDisposable
// The inbound reply (SetState 0xF74B) lands via L.2g slice 1.
// ============================================================
private void PickAndStoreSelection(bool useImmediately)
/// <summary>
/// Shared world pick at the current cursor — the 2026-05-16
/// retail-faithful screen-rect picker (hit area = the target
/// indicator's rect via the shared ScreenProjection helper; the old
/// per-type radius/offset heuristics are retired). Used by the click
/// select path AND the target-mode cursor hover (retail
/// SmartBox::get_found_object_id analogue). <paramref name="includeSelf"/>:
/// item target-use may pick the LOCAL PLAYER (retail lets you kit-heal
/// yourself by clicking your own toon); plain selection never does.
/// </summary>
private uint? PickWorldGuidAtCursor(bool includeSelf)
{
if (_cameraController is null || _window is null) return;
if (_cameraController is null || _window is null) return null;
// 2026-05-16 — retail-faithful screen-rect picker. The hit area
// is the same screen-space rect the target indicator draws
// (computed via the shared AcDream.Core.Selection.ScreenProjection
// helper). Per user feedback 2026-05-16: clicking the indicator
// brackets — including the rect corners — must select the entity.
// The per-type radius/offset heuristics retired here (1.0/1.6/2.0
// m radii, 0.2/0.9/1.0/1.5 m vertical offsets, IsTallSceneryGuid)
// existed to make a 3D ray-sphere picker approximate the visible
// rect; the new picker doesn't need them.
var camera = _cameraController.Active;
var viewport = new System.Numerics.Vector2((float)_window.Size.X, (float)_window.Size.Y);
// Indoor walking Phase 1 #86 (2026-05-19): snapshot the currently-
// cached EnvCell physics so the picker can occlude entities behind
// walls. Snapshot is per-pick (one click), iteration is bounded
// by the streaming radius (~80 cells at radius 4).
// walls. Snapshot is per-pick (one click / one hover frame),
// iteration is bounded by the streaming radius (~80 cells at radius 4).
var loadedCellPhysics = new List<AcDream.Core.Physics.CellPhysics>();
foreach (var cellId in _physicsDataCache.CellStructIds)
{
@ -12402,12 +12408,12 @@ public sealed class GameWindow : IDisposable
if (cp is not null) loadedCellPhysics.Add(cp);
}
var picked = AcDream.Core.Selection.WorldPicker.Pick(
return AcDream.Core.Selection.WorldPicker.Pick(
mouseX: _lastMouseX, mouseY: _lastMouseY,
view: camera.View, projection: camera.Projection,
viewport: viewport,
candidates: _entitiesByServerGuid.Values,
skipServerGuid: _playerServerGuid,
skipServerGuid: includeSelf ? 0u : _playerServerGuid,
// Resolver: Setup's SelectionSphere is the ONLY input. If the
// entity's Setup didn't bake a SelectionSphere, return null —
// the picker skips it, which matches retail behaviour
@ -12429,6 +12435,16 @@ public sealed class GameWindow : IDisposable
? (origin, direction) =>
AcDream.Core.Selection.CellBspRayOccluder.NearestWallT(origin, direction, loadedCellPhysics)
: null);
}
private void PickAndStoreSelection(bool useImmediately)
{
if (_cameraController is null || _window is null) return;
// Target-use mode picks WITH self (retail: kit-heal yourself by
// clicking your own toon); plain selection keeps the self-exclusion.
var picked = PickWorldGuidAtCursor(
includeSelf: _itemInteractionController?.IsTargetModeActive == true);
if (picked is uint guid)
{