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:
parent
0f82a08f0a
commit
146a963aeb
26 changed files with 1302 additions and 1340 deletions
204
src/AcDream.App/UI/Layout/VividTargetIndicatorController.cs
Normal file
204
src/AcDream.App/UI/Layout/VividTargetIndicatorController.cs
Normal 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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue