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; } }