using System.Numerics;
namespace AcDream.App.Rendering.Walk;
/// Retail view_vertex (stride 0x18): a screen point plus the
/// world-space plane of the edge that STARTS at it (edge k = verts k → k+1).
public struct WalkViewVertex
{
public Vector2 Point;
public WalkPlane Plane;
}
/// Retail view_poly: one view polygon's slice of the shared
/// vertex pool plus its screen bounds.
public readonly record struct WalkViewPoly(
int VertexCount, int VertexIndex, float XMin, float XMax, float YMin, float YMax);
/// Retail view_type: the poly list + shared vertex pool one
/// portal_view_type accumulates its views into.
public sealed class WalkViewSet
{
public readonly List Polys = new();
public readonly List Vertices = new();
public int VertexCountTotal;
}
///
/// Retail portal_view_type (0x48 bytes): one view-recursion slot on a
/// cell (or the PView's outside_view). Retail recycles slots and
/// resets exactly view_count/update_count/view_timestamp on push
/// (CEnvCell::curr_view_push @0x005a5090); this port models the same
/// counters over list storage — when is 0 the next
/// append clears the pools, matching retail's vertex-pool base reset.
///
public sealed class WalkPortalView
{
/// Per-portal portal_info flags (seen, inflag), sized by
/// PView::InitCell for the owning cell's portal count.
public WalkPortalFlags[] PortalFlags = [];
public readonly WalkViewSet View = new();
/// Max SQUARED cell-local distance to any in-view portal vertex
/// (PView::InitCell); the flood's todo-list distance key.
public float MaxInDistSquared;
public int ViewCount;
public bool CellViewDone;
public int ViewTimestamp;
public int UpdateCount;
/// curr_view_push's per-push counter reset.
public void ResetForPush()
{
ViewCount = 0;
UpdateCount = 0;
ViewTimestamp = 0;
}
}
public struct WalkPortalFlags
{
public bool Seen;
public bool InView;
}
///
/// Unprojects a screen point to a world-space eye ray direction —
/// retail PrimD3DRender::ScreenToViewTransform @0x0059aa40 (the live
/// newmethod==1 path of Render::copy_view's plane builder).
/// The exact matrix wiring lives with the camera module; the walk depends
/// only on this contract.
///
public interface IWalkRayCaster
{
Vector3 RayThrough(float screenX, float screenY);
}
///
/// Campaign FW1 — Render::copy_view @0x0054dfc0, ported from the
/// flood-read appendix report 3 (Ghidra-arbitrated). Appends ONE view
/// polygon to a : perspective-divides the
/// homogeneous screen points IN PLACE, prunes ~1 px duplicates and
/// collinear points (three closing wrap checks included), rejects fewer
/// than 3 survivors (returns false, dest untouched), caps at 31, stores the
/// point list plus a closing duplicate, computes bounds, and builds
/// per-edge WORLD planes N = normalize(cross(ray[k+1], ray[k])) — NEXT ×
/// CURRENT — with d = −dot(N, viewpoint).
///
public static class WalkCopyView
{
public const int MaxVertices = 31; // retail cap 0x1f
public const float DedupThreshold = 1f; // strict > 1 px
/// The null-source path: the full-viewport root quad
/// (0,H)(W,H)(W,0)(0,0) — used by Render::set_default_view and
/// PView::DrawInside's root view (the source count is ignored).
public static bool AppendFullViewportQuad(
WalkPortalView dest, IWalkRayCaster rays, Vector3 viewpoint,
float viewportWidth, float viewportHeight)
{
Span quad =
[
new(0f, viewportHeight, 0f, 1f),
new(viewportWidth, viewportHeight, 0f, 1f),
new(viewportWidth, 0f, 0f, 1f),
new(0f, 0f, 0f, 1f),
];
return Append(dest, quad, rays, viewpoint);
}
/// The point-source path. is mutated
/// (in-place perspective divide) exactly as retail mutates the shared
/// clip buffer; the buffer is consumed per portal, so the mutation never
/// leaks across calls.
public static bool Append(
WalkPortalView dest, Span points,
IWalkRayCaster rays, Vector3 viewpoint)
{
int npts = points.Length;
if (npts == 0) return false;
// ---- survivor marking (keep[] / last / stl / second bookkeeping) ----
Span keep = stackalloc bool[npts];
keep[0] = true;
int n = 1;
int last = 0;
int secondToLast = 0; // retail 'stl': index of the second-to-last kept corner
int second = 0; // retail local_220: index of the 2nd kept point
for (int i = 0; i < npts; i++)
{
ref WalkScreenPoint p = ref points[i];
if (p.W != 1f)
{
p.X /= p.W;
p.Y /= p.W;
p.W = 1f;
}
if (i == 0) continue;
bool distinct =
MathF.Abs(points[i].X - points[last].X) > DedupThreshold
|| MathF.Abs(points[i].Y - points[last].Y) > DedupThreshold;
keep[i] = distinct;
if (!distinct) continue;
if (n == 1)
{
n++;
second = i;
}
else
{
WalkScreenPoint pp = points[secondToLast];
WalkScreenPoint prev = points[last];
WalkScreenPoint cur = points[i];
float span = MathF.Max(MathF.Abs(pp.X - cur.X), MathF.Abs(pp.Y - cur.Y));
float cross = (pp.X - prev.X) * (prev.Y - cur.Y)
- (pp.Y - prev.Y) * (prev.X - cur.X);
if (MathF.Abs(cross) >= span)
{
n++;
secondToLast = last;
}
else
{
// prev was collinear: un-keep it; count unchanged (prev out, cur in).
keep[last] = false;
if (second == last) second = i;
}
}
last = i;
}
// ---- closing wrap checks against point 0 ----
WalkScreenPoint first = points[0];
bool lastDistinct =
MathF.Abs(first.X - points[last].X) > DedupThreshold
|| MathF.Abs(first.Y - points[last].Y) > DedupThreshold;
keep[last] = lastDistinct;
if (!lastDistinct)
{
n--;
last = secondToLast;
}
else
{
float span = MathF.Max(
MathF.Abs(points[secondToLast].X - first.X),
MathF.Abs(points[secondToLast].Y - first.Y));
float cross = (points[secondToLast].X - points[last].X) * (points[last].Y - first.Y)
- (points[last].X - first.X) * (points[secondToLast].Y - points[last].Y);
if (MathF.Abs(cross) < span)
{
keep[last] = false;
n--;
last = secondToLast;
}
}
secondToLast = last;
if (second > 0)
{
// Is point 0 itself collinear between the last corner and the second?
float span = MathF.Max(
MathF.Abs(points[secondToLast].X - points[second].X),
MathF.Abs(points[secondToLast].Y - points[second].Y));
float cross = (first.Y - points[second].Y) * (points[secondToLast].X - first.X)
- (first.X - points[second].X) * (points[secondToLast].Y - first.Y);
if (MathF.Abs(cross) < span)
{
n--;
keep[0] = false;
}
}
if (n < 3) return false; // REJECT: dest completely untouched
if (n > MaxVertices) n = MaxVertices; // cap 31 (corrupt overflow path unreachable ≤32 in)
// ---- append into the pool (view_count==0 resets the pool base) ----
WalkViewSet view = dest.View;
if (dest.ViewCount == 0)
{
view.Polys.Clear();
view.Vertices.Clear();
view.VertexCountTotal = 0;
}
int vbase = view.VertexCountTotal;
view.VertexCountTotal = vbase + n + 1;
int written = 0;
for (int i = 0; i < npts && written < n; i++)
{
if (!keep[i]) continue;
view.Vertices.Add(new WalkViewVertex
{
// Retail applies a REAL fabs on copy (harmless post-clip; preserved).
Point = new Vector2(MathF.Abs(points[i].X), MathF.Abs(points[i].Y)),
});
written++;
}
// Closing duplicate vertex (its plane slot is never consumed).
view.Vertices.Add(new WalkViewVertex { Point = view.Vertices[vbase].Point });
// ---- bounds over v[0..n-1] ----
float xmin, xmax, ymin, ymax;
Vector2 seed = view.Vertices[vbase + n - 1].Point;
xmin = xmax = seed.X;
ymin = ymax = seed.Y;
for (int k = n - 2; k >= 0; k--)
{
Vector2 pt = view.Vertices[vbase + k].Point;
if (pt.X < xmin) xmin = pt.X; else if (pt.X > xmax) xmax = pt.X;
if (pt.Y < ymin) ymin = pt.Y; else if (pt.Y > ymax) ymax = pt.Y;
}
view.Polys.Add(new WalkViewPoly(n, vbase, xmin, xmax, ymin, ymax));
// ---- per-edge world planes from unprojected rays ----
Span ray = stackalloc Vector3[n + 1];
for (int k = 0; k < n; k++)
{
Vector2 pt = view.Vertices[vbase + k].Point;
ray[k] = rays.RayThrough(pt.X, pt.Y);
}
ray[n] = ray[0];
for (int k = n - 1; k >= 0; k--)
{
Vector3 normal = Vector3.Cross(ray[k + 1], ray[k]); // NEXT × CURRENT
if (MathF.Abs(normal.X) >= WalkVisibilityMath.Epsilon
|| MathF.Abs(normal.Y) >= WalkVisibilityMath.Epsilon
|| MathF.Abs(normal.Z) >= WalkVisibilityMath.Epsilon)
{
normal *= 1f / MathF.Sqrt(
normal.X * normal.X + normal.Y * normal.Y + normal.Z * normal.Z);
}
// else: degenerate edge left tiny/unnormalized (retail behavior).
WalkViewVertex v = view.Vertices[vbase + k];
v.Plane = new WalkPlane(normal, -Vector3.Dot(normal, viewpoint));
view.Vertices[vbase + k] = v;
}
dest.ViewCount += 1;
return true;
}
}