feat(selection): port retail polygon picking and vivid marker

Replace the projected Setup-sphere rectangle and independent physics-wall ray with retail's render-coupled picker: only visible server-object parts participate, each exact drawing sphere broad-phases the camera-eye ray, and first-in-DAT-order visual polygon hits globally outrank sphere fallbacks.

Replace the devtools-only procedural triangles with the retained gameplay VividTargetIndicator using retail client-enum surfaces 1..4, radar-blip colorization, Setup selection-sphere framing, and the exact eight-pixel viewport clamp.

Release build succeeds with zero warnings and all 5,886 tests pass with five intentional skips.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 21:32:51 +02:00
parent 0f82a08f0a
commit 146a963aeb
26 changed files with 1302 additions and 1340 deletions

View file

@ -0,0 +1,204 @@
using System.Numerics;
using AcDream.Core.Selection;
using AcDream.Core.Ui;
namespace AcDream.App.UI.Layout;
public readonly record struct VividTargetInfo(
Vector3 SelectionSphereCenter,
float SelectionSphereRadius,
uint ItemType,
uint ObjectDescriptionFlags);
public sealed record VividTargetRuntimeBindings(
SelectionState Selection,
Func<uint> PlayerGuid,
Func<bool> Enabled,
Func<uint, VividTargetInfo?> ResolveTarget,
Func<uint, bool> WasDrawn,
Func<(Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport)> Camera);
/// <summary>
/// Retained-UI port of <c>VividTargetIndicator @ 0x004F5CE0..0x004F6DF6</c>.
/// The four corner surfaces are client-enum category 0x10000009 values 1..4, colorized
/// with the selected object's retail radar-blip color and placed around the
/// Setup selection sphere's SmartBox screen rectangle.
/// </summary>
public sealed class VividTargetIndicatorController
{
private const uint ClientEnumCategory = 0x10000009u;
private const uint FirstSourceImageEnum = 1u;
private const float ViewportMargin = 8f;
private readonly UiPanel _root;
private readonly UiTextureElement[] _corners;
private readonly VividTargetRuntimeBindings _bindings;
private readonly (float Width, float Height)[] _sizes;
private VividTargetIndicatorController(
UiPanel root,
UiTextureElement[] corners,
(float Width, float Height)[] sizes,
VividTargetRuntimeBindings bindings)
{
_root = root;
_corners = corners;
_sizes = sizes;
_bindings = bindings;
}
public static VividTargetIndicatorController? Mount(
UiRoot host,
RetailUiAssets assets,
VividTargetRuntimeBindings bindings)
{
var root = new UiPanel
{
Name = "VividTargetIndicator",
BackgroundColor = Vector4.Zero,
BorderColor = Vector4.Zero,
ClickThrough = true,
Visible = false,
ZOrder = -10_000,
Anchors = AnchorEdges.None,
};
var corners = new UiTextureElement[4];
var sizes = new (float Width, float Height)[4];
for (uint i = 0; i < 4; i++)
{
uint did;
lock (assets.DatLock)
did = RetailDataIdResolver.Resolve(
assets.Dats,
enumValue: FirstSourceImageEnum + i,
enumCategory: ClientEnumCategory);
if (did == 0u)
return null;
var resolved = assets.ResolveSprite(did);
if (resolved.Texture == 0u || resolved.Width <= 0 || resolved.Height <= 0)
return null;
sizes[i] = (resolved.Width, resolved.Height);
corners[i] = new UiTextureElement
{
Name = $"VividTargetCorner{i + 1}",
Texture = resolved.Texture,
Width = resolved.Width,
Height = resolved.Height,
ClickThrough = true,
Anchors = AnchorEdges.None,
};
root.AddChild(corners[i]);
}
host.AddChild(root);
return new VividTargetIndicatorController(root, corners, sizes, bindings);
}
public void Tick()
{
if (!_bindings.Enabled()
|| _bindings.Selection.SelectedObjectId is not uint guid
|| guid == 0u
|| guid == _bindings.PlayerGuid()
|| !_bindings.WasDrawn(guid)
|| _bindings.ResolveTarget(guid) is not VividTargetInfo target)
{
_root.Visible = false;
return;
}
var camera = _bindings.Camera();
if (!ScreenProjection.TryProjectSphereToScreenRect(
target.SelectionSphereCenter,
target.SelectionSphereRadius,
camera.View,
camera.Projection,
camera.Viewport,
out Vector2 rectMin,
out Vector2 rectMax,
out _,
minSidePixels: 0f))
{
_root.Visible = false;
return;
}
VividTargetLayout layout = ComputeLayout(
rectMin, rectMax, camera.Viewport, _sizes);
_root.Left = layout.RootPosition.X;
_root.Top = layout.RootPosition.Y;
_root.Width = layout.RootSize.X;
_root.Height = layout.RootSize.Y;
for (int i = 0; i < _corners.Length; i++)
SetCorner(i, layout.CornerPositions[i].X, layout.CornerPositions[i].Y);
RadarBlipColors.Rgba color = RadarBlipColors.For(
target.ItemType, target.ObjectDescriptionFlags);
var tint = new Vector4(color.Red, color.Green, color.Blue, color.Alpha);
for (int i = 0; i < _corners.Length; i++)
_corners[i].Tint = tint;
_root.Visible = true;
}
private void SetCorner(int index, float left, float top)
{
_corners[index].Left = left;
_corners[index].Top = top;
}
internal static VividTargetLayout ComputeLayout(
Vector2 rectMin,
Vector2 rectMax,
Vector2 viewport,
IReadOnlyList<(float Width, float Height)> sizes)
{
if (sizes.Count != 4)
throw new ArgumentException("Retail VividTargetIndicator has exactly four corners.", nameof(sizes));
// VividTargetIndicator::OnDraw @ 0x004F69C8..0x004F6A99 reads
// corner[1]'s dimensions, expands the SmartBox rectangle by one
// corner on the top/left, then clamps each rectangle edge separately.
// Partially off-screen targets therefore shrink the assembled region;
// retail does not translate the whole un-clipped box back on-screen.
float cornerWidth = sizes[0].Width;
float cornerHeight = sizes[0].Height;
float outerLeft = rectMin.X - cornerWidth;
float outerTop = rectMin.Y - cornerHeight;
float outerRight = rectMax.X;
float outerBottom = rectMax.Y;
if (outerLeft > outerRight) outerLeft = outerRight - 1f;
if (outerTop > outerBottom) outerTop = outerBottom - 1f;
outerLeft = MathF.Max(outerLeft, ViewportMargin);
outerTop = MathF.Max(outerTop, ViewportMargin);
outerRight = MathF.Max(outerRight, cornerWidth + ViewportMargin);
outerBottom = MathF.Max(outerBottom, cornerHeight + ViewportMargin);
float maximumRight = viewport.X - cornerWidth - ViewportMargin;
float maximumBottom = viewport.Y - cornerHeight - ViewportMargin;
outerLeft = MathF.Min(outerLeft, maximumRight - cornerWidth);
outerTop = MathF.Min(outerTop, maximumBottom - cornerHeight);
outerRight = MathF.Min(outerRight, maximumRight);
outerBottom = MathF.Min(outerBottom, maximumBottom);
Vector2 rootSize = new(
MathF.Max(1f, outerRight - outerLeft + cornerWidth),
MathF.Max(1f, outerBottom - outerTop + cornerHeight));
return new VividTargetLayout(
new Vector2(outerLeft, outerTop),
rootSize,
[
Vector2.Zero,
new Vector2(rootSize.X - sizes[1].Width, 0f),
new Vector2(rootSize.X - sizes[2].Width, rootSize.Y - sizes[2].Height),
new Vector2(0f, rootSize.Y - sizes[3].Height),
]);
}
}
internal readonly record struct VividTargetLayout(
Vector2 RootPosition,
Vector2 RootSize,
IReadOnlyList<Vector2> CornerPositions);