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
117
tests/AcDream.App.Tests/Rendering/RetailSelectionAssetTests.cs
Normal file
117
tests/AcDream.App.Tests/Rendering/RetailSelectionAssetTests.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Selection;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.Core.Textures;
|
||||
using AcDream.Core.Selection;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Options;
|
||||
using Xunit.Sdk;
|
||||
using SysEnv = System.Environment;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class RetailSelectionAssetTests
|
||||
{
|
||||
[Fact]
|
||||
public void InstalledDat_ResolvesAllFourVividTargetCorners()
|
||||
{
|
||||
string datDir = ResolveDatDir();
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
var master = Assert.IsType<EnumIDMap>(
|
||||
dats.Get<EnumIDMap>((uint)dats.Portal.Header.MasterMapId));
|
||||
uint subMapDid = master.ClientEnumToID[0x10000009u];
|
||||
var subMap = Assert.IsType<EnumIDMap>(dats.Get<EnumIDMap>(subMapDid));
|
||||
|
||||
for (uint index = 0; index < 4; index++)
|
||||
{
|
||||
uint enumValue = 1u + index;
|
||||
uint did = RetailDataIdResolver.Resolve(dats, enumValue, 0x10000009u);
|
||||
Assert.True(did != 0u,
|
||||
$"enum {enumValue}; submap 0x{subMapDid:X8} keys: {string.Join(',', subMap.ClientEnumToID.Keys.Select(k => $"0x{k:X8}"))}");
|
||||
Assert.True(dats.Portal.TryGet<RenderSurface>(did, out var surface)
|
||||
|| dats.HighRes.TryGet<RenderSurface>(did, out surface),
|
||||
$"enum 0x{enumValue:X8} resolved 0x{did:X8}");
|
||||
Assert.NotNull(surface);
|
||||
Assert.Equal(0x06000000u, did & 0xFF000000u);
|
||||
Assert.Equal((12, 12), (surface.Width, surface.Height));
|
||||
|
||||
Palette? palette = surface.DefaultPaletteId != 0
|
||||
? dats.Get<Palette>(surface.DefaultPaletteId)
|
||||
: null;
|
||||
DecodedTexture decoded = SurfaceDecoder.DecodeRenderSurface(surface, palette);
|
||||
int coloredPixels = 0;
|
||||
for (int pixel = 0; pixel < decoded.Rgba8.Length; pixel += 4)
|
||||
{
|
||||
if (decoded.Rgba8[pixel + 3] == 0)
|
||||
continue;
|
||||
if (decoded.Rgba8[pixel] != decoded.Rgba8[pixel + 1]
|
||||
|| decoded.Rgba8[pixel] != decoded.Rgba8[pixel + 2])
|
||||
coloredPixels++;
|
||||
}
|
||||
Assert.True(coloredPixels == 0,
|
||||
$"corner {index + 1} has {coloredPixels} non-grayscale source pixels");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoorSelectionGeometry_UsesDrawingBspRootSphereAndDatPolygonOrder()
|
||||
{
|
||||
const uint doorGfxObjId = 0x010044B5u;
|
||||
string datDir = ResolveDatDir();
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
var gfx = Assert.IsType<GfxObj>(dats.Get<GfxObj>(doorGfxObjId));
|
||||
var cache = new RetailSelectionGeometryCache(dats, new object());
|
||||
|
||||
var selection = Assert.IsType<AcDream.Core.Selection.RetailSelectionMesh>(
|
||||
cache.Resolve(doorGfxObjId));
|
||||
|
||||
var root = Assert.IsType<DatReaderWriter.Types.DrawingBSPNode>(gfx.DrawingBSP.Root);
|
||||
Assert.Equal(root.BoundingSphere.Origin, selection.SphereCenter);
|
||||
Assert.Equal(root.BoundingSphere.Radius, selection.SphereRadius);
|
||||
Assert.Equal(gfx.Polygons.Count, selection.Polygons.Count);
|
||||
|
||||
var firstSource = gfx.Polygons.First().Value;
|
||||
var firstSelection = selection.Polygons[0];
|
||||
Assert.Equal((int)firstSource.SidesType == 0, firstSelection.SingleSided);
|
||||
Assert.Equal(firstSource.VertexIds.Count, firstSelection.Vertices.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DrawingSphereViewconeRejectsPartBehindCamera()
|
||||
{
|
||||
Matrix4x4 view = Matrix4x4.CreateLookAt(
|
||||
Vector3.Zero, -Vector3.UnitZ, Vector3.UnitY);
|
||||
Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(
|
||||
MathF.PI / 3f, 4f / 3f, 0.1f, 100f);
|
||||
FrustumPlanes frustum = FrustumPlanes.FromViewProjection(view * projection);
|
||||
var mesh = new RetailSelectionMesh(Vector3.Zero, 1f, []);
|
||||
|
||||
Assert.True(RetailSelectionScene.DrawingSphereIntersectsFrustum(
|
||||
mesh, Matrix4x4.CreateTranslation(0f, 0f, -5f), frustum));
|
||||
Assert.False(RetailSelectionScene.DrawingSphereIntersectsFrustum(
|
||||
mesh, Matrix4x4.CreateTranslation(0f, 0f, 5f), frustum));
|
||||
}
|
||||
|
||||
private static string ResolveDatDir()
|
||||
{
|
||||
string? configured = SysEnv.GetEnvironmentVariable("ACDREAM_DAT_DIR");
|
||||
if (!string.IsNullOrWhiteSpace(configured)
|
||||
&& File.Exists(Path.Combine(configured, "client_portal.dat")))
|
||||
return configured;
|
||||
|
||||
const string installed = @"C:\Turbine\Asheron's Call";
|
||||
if (File.Exists(Path.Combine(installed, "client_portal.dat")))
|
||||
return installed;
|
||||
|
||||
string conventional = Path.Combine(
|
||||
SysEnv.GetFolderPath(SysEnv.SpecialFolder.UserProfile),
|
||||
"Documents",
|
||||
"Asheron's Call");
|
||||
if (File.Exists(Path.Combine(conventional, "client_portal.dat")))
|
||||
return conventional;
|
||||
|
||||
throw SkipException.ForSkip("Installed client_portal.dat is required.");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.UI.Layout;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
public sealed class VividTargetIndicatorLayoutTests
|
||||
{
|
||||
private static readonly (float Width, float Height)[] RetailCorners =
|
||||
[(12f, 12f), (12f, 12f), (12f, 12f), (12f, 12f)];
|
||||
|
||||
[Fact]
|
||||
public void CornersSitOutsideSelectionSphereRectangle()
|
||||
{
|
||||
VividTargetLayout layout = VividTargetIndicatorController.ComputeLayout(
|
||||
new Vector2(100f, 80f),
|
||||
new Vector2(180f, 200f),
|
||||
new Vector2(800f, 600f),
|
||||
RetailCorners);
|
||||
|
||||
Assert.Equal(new Vector2(88f, 68f), layout.RootPosition);
|
||||
Assert.Equal(new Vector2(104f, 144f), layout.RootSize);
|
||||
Assert.Equal(Vector2.Zero, layout.CornerPositions[0]);
|
||||
Assert.Equal(new Vector2(92f, 0f), layout.CornerPositions[1]);
|
||||
Assert.Equal(new Vector2(92f, 132f), layout.CornerPositions[2]);
|
||||
Assert.Equal(new Vector2(0f, 132f), layout.CornerPositions[3]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WholeIndicatorClampsToRetailEightPixelViewportMargin()
|
||||
{
|
||||
VividTargetLayout layout = VividTargetIndicatorController.ComputeLayout(
|
||||
new Vector2(2f, 3f),
|
||||
new Vector2(62f, 83f),
|
||||
new Vector2(200f, 150f),
|
||||
RetailCorners);
|
||||
|
||||
Assert.Equal(new Vector2(8f, 8f), layout.RootPosition);
|
||||
Assert.Equal(new Vector2(66f, 87f), layout.RootSize);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue