using System; using System.Collections.Generic; using AcDream.Core.Content; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; namespace AcDream.Core.Meshing; /// /// OH2/S1 chunk B (docs/research/2026-09-01-overhaul/oh2-cellstruct-surface-contract.md /// §9 item 5): retires this type's former render-production role. The old /// Build method carried its own NoPos-based "skip untextured cell /// geometry" approximation (AP-234, retired) — a second, divergent /// interpretation of the retail CellStruct surface/subset construction /// algorithm alongside MeshExtractor.PrepareCellStructMeshData, which /// is now the ONE production interpretation (contract §9 item 5: "Core and /// Content do not retain divergent CellStruct interpretations"). This class /// now exposes only : the exact retail /// predicate for whether a CellStruct contributes at least one drawable /// subset, needed by the streaming build job /// (AcDream.App.Streaming.LandblockBuildFactory) to decide whether a /// cell's shell has drawable geometry before it registers the cell — a /// question answerable without building or retaining any geometry. /// public static class CellMesh { /// /// True iff at least one of this CellStruct's polygon construction /// candidates (, /// ported from D3DPolyRender::ConstructMesh @0x0059DFA0, contract /// §3.4) targets a surface slot whose resolved Surface.Type is /// TEXTURED ( /// is false) — retail's built-EnvCell draw admission /// (Surface.Type & (BASE1_IMAGE|BASE1_CLIPMAP)) != 0 /// (RenderDeviceD3D::DrawEnvCell @0x0059F170 → /// D3DPolyRender::DrawMesh(..., arg4=1) @0x0059D4A0, contract §4), /// evaluated without resolving the surface's texture dependency chain. /// It is therefore a conservative superset of what /// MeshExtractor.PrepareCellStructMeshData actually emits: the /// extractor additionally drops a textured slot whose SurfaceTexture, /// RenderSurface, palette, or pixel format cannot be resolved (an /// extractor-side quarantine outcome, logged there), which this predicate /// still reports as drawable. Missing texture data is a DAT defect, not a /// retail admission decision, so the predicate follows the admission rule. /// /// /// Side candidates come only from CPolygon::sides_type /// (contract §3.4); NoPos/NoNeg play no role in this /// predicate — they mean "this side's UV-index array is absent" /// (contract §2.3/§3.5), which cannot change whether a subset draws, /// only what its texture coordinates are. This predicate therefore /// never reads a polygon's UV-index arrays. /// /// /// The EnvCell whose ordered surface-override array /// () resolves each candidate's source /// surface-array slot to a qualified Surface DAT id, exactly like /// MeshExtractor.PrepareCellStructMeshData's /// surfaceOverrides parameter. /// /// The CellStruct containing the polygon geometry. /// DAT object source used to resolve each candidate's surface Surface.Type. public static bool HasDrawableGeometry(EnvCell envCell, CellStruct cellStruct, IDatObjectSource dats) { // One resolve per distinct surface slot, not per polygon: a slot's // textured-ness is a per-slot fact (contract §3.2's per-surface // mask/type), and re-resolving the same Surface DAT record for // every polygon that references it would be wasted DAT I/O on the // streaming worker thread that calls this predicate. var slotIsTextured = new Dictionary(); bool SlotIsTextured(int slot) { if (slotIsTextured.TryGetValue(slot, out bool cached)) return cached; bool textured = false; if (slot >= 0 && slot < envCell.Surfaces.Count) { uint surfaceId = 0x08000000u | envCell.Surfaces[slot]; if (dats.Get(surfaceId) is { } surface) textured = !RetailUntexturedSurfacePolicy.IsUntextured(surface.Type); } slotIsTextured[slot] = textured; return textured; } foreach (var poly in cellStruct.Polygons.Values) { // Same degenerate-fan gate as MeshExtractor.PrepareCellStructMeshData. if (poly.VertexIds.Count < 3) continue; ReadOnlySpan candidates = CellStructSideCandidates.GetCandidates((int)poly.SidesType); foreach (var candidate in candidates) { short surfaceIdxRaw = candidate.SurfaceSlot == CellStructPolygonSurfaceSide.Positive ? poly.PosSurface : poly.NegSurface; if (surfaceIdxRaw < 0) continue; if (SlotIsTextured(surfaceIdxRaw)) return true; } } return false; } }