using System.Numerics; using AcDream.App.Rendering.Scene; namespace AcDream.App.Rendering.Walk; /// /// Campaign FW3.2b-2: the production over /// the retained scene () and the FW3.1 /// . Rebuilt facts per frame via /// : /// /// /// Cell statics — on /// demand, one pooled array per distinct cell per frame (a cell can be /// visited once by the root flood OR once per admitting look-in portal; the /// per-frame cache keeps the copy single). /// Outdoor statics — ONE /// sweep bucketed by landscape cell id /// ((lb & 0xFFFF0000) | (cellX*8 + cellY + 1) from the record's /// world position — the same encoding /// computes), EXCLUDING building shells (they draw at their building's own /// shell turn, retail CPhysicsPart::Draw(parts, 0) @0x0059f331, not /// at the cell's DrawObjCell turn). /// Building shells — the same sweep's IsBuildingShell records /// bucketed by Source.BuildingShellAnchorCellId; a /// maps to its anchor via its first /// non-exit portal's destination (the SAME rule /// LandblockLoader used to author the anchor). /// /// /// The tuple landblock id handed to the classifier is the frame's player /// landblock — the packed path's own convention for every /// RenderFrameEntityDrawRequest. /// internal sealed class WalkProductionWorldData : IWalkFrameWorldData { private readonly WalkBuildingRegistry _buildings; private RenderSceneQuery _scene; private uint _tupleLandblockId; private readonly Dictionary _cellCache = new(); private readonly Dictionary> _outdoorByCell = new(); private readonly Dictionary> _shellsByAnchor = new(); private readonly Dictionary _outdoorMaterialized = new(); private readonly Dictionary _shellMaterialized = new(); private RenderProjectionRecord[] _sweepScratch = new RenderProjectionRecord[1024]; private RenderProjectionRecord[] _cellScratch = new RenderProjectionRecord[256]; internal WalkProductionWorldData(WalkBuildingRegistry buildings) { _buildings = buildings ?? throw new ArgumentNullException(nameof(buildings)); } /// Rebuilds the frame's outdoor/shell buckets and clears the /// per-cell cache. Call once per frame before the driver runs. internal void BeginFrame(RenderSceneQuery scene, uint tupleLandblockId) { _scene = scene; _tupleLandblockId = tupleLandblockId; _cellCache.Clear(); _outdoorMaterialized.Clear(); _shellMaterialized.Clear(); foreach (List bucket in _outdoorByCell.Values) bucket.Clear(); foreach (List bucket in _shellsByAnchor.Values) bucket.Clear(); int count; while (true) { count = _scene.CopyIndexTo(RenderSceneIndex.OutdoorStatic, _sweepScratch); if (count < _sweepScratch.Length) break; _sweepScratch = new RenderProjectionRecord[_sweepScratch.Length * 2]; } for (int i = 0; i < count; i++) { ref readonly RenderProjectionRecord record = ref _sweepScratch[i]; if (record.EntityPayload.IsBuildingShell) { uint anchor = record.Source.BuildingShellAnchorCellId; if (!_shellsByAnchor.TryGetValue(anchor, out List? shells)) _shellsByAnchor[anchor] = shells = new List(); shells.Add(record); continue; } uint cellId = LandscapeCellId(record.Transform.Position); if (!_outdoorByCell.TryGetValue(cellId, out List? bucket)) _outdoorByCell[cellId] = bucket = new List(); bucket.Add(record); } } /// The landscape cell owning a world position — retail's /// 24 m cell grid inside the 192 m landblock, the same /// (lb & 0xFFFF0000) | (cellX*8 + cellY + 1) encoding the walk's /// landscape turn emits. internal static uint LandscapeCellId(Vector3 worldPosition) { int lbX = (int)MathF.Floor(worldPosition.X / 192f); int lbY = (int)MathF.Floor(worldPosition.Y / 192f); float localX = worldPosition.X - lbX * 192f; float localY = worldPosition.Y - lbY * 192f; int cellX = Math.Clamp((int)(localX / 24f), 0, 7); int cellY = Math.Clamp((int)(localY / 24f), 0, 7); uint landblock = ((uint)(byte)lbX << 24) | ((uint)(byte)lbY << 16); return landblock | (uint)(cellX * 8 + cellY + 1); } public WalkFrameStaticRecords GetCellStatics(uint cellId) { if (_cellCache.TryGetValue(cellId, out WalkFrameStaticRecords cached)) return cached; int count; while (true) { count = _scene.CopyCellStaticsTo(cellId, _cellScratch); if (count < _cellScratch.Length) break; _cellScratch = new RenderProjectionRecord[_cellScratch.Length * 2]; } WalkFrameStaticRecords records = count == 0 ? WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId } : new WalkFrameStaticRecords(_cellScratch[..count], _tupleLandblockId); _cellCache[cellId] = records; return records; } public WalkFrameStaticRecords GetOutdoorStatics(uint cellId) { if (_outdoorMaterialized.TryGetValue(cellId, out WalkFrameStaticRecords cached)) return cached; WalkFrameStaticRecords records = _outdoorByCell.TryGetValue(cellId, out List? bucket) && bucket.Count > 0 ? new WalkFrameStaticRecords([.. bucket], _tupleLandblockId) : WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId }; _outdoorMaterialized[cellId] = records; return records; } public WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building) { uint anchor = AnchorCellId(building); if (anchor == 0) return WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId }; if (_shellMaterialized.TryGetValue(anchor, out WalkFrameStaticRecords cached)) return cached; WalkFrameStaticRecords records = _shellsByAnchor.TryGetValue(anchor, out List? shells) && shells.Count > 0 ? new WalkFrameStaticRecords([.. shells], _tupleLandblockId) : WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId }; _shellMaterialized[anchor] = records; return records; } /// The building's authored shell anchor: its first non-exit /// portal's destination cell — the SAME rule LandblockLoader used /// when it stamped BuildingShellAnchorCellId on the shell entity. internal static uint AnchorCellId(WalkBuilding building) { foreach (ref readonly WalkBldPortal portal in building.Portals.AsSpan()) { if (portal.OtherCellId != 0xFFFFFFFFu) return portal.OtherCellId; } return 0; } public Matrix4x4 GetBuildingWorldTransform(WalkBuilding building) { if (!_buildings.TryGetEntry(building, out WalkBuildingFactory.Entry? entry)) { throw new InvalidOperationException( $"walk building 0x{building.PositionCellId:X8} has no committed registry entry"); } return entry.WorldTransform; } }