From abcfb554188c24a44de2f151adc5f4af0e7a0e78 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 10 Apr 2026 21:45:02 +0200 Subject: [PATCH] feat(app): walk interior EnvCells for in-building static objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the "foundry statue still missing" user feedback after Phase 2c. Diagnostic spike confirmed the statue is not a Stab on LandBlockInfo.Objects, not a Building on LandBlockInfo.Buildings, and not a hierarchical Setup part — the highest entity on Holtburg center was at Z=104 with no entity above the foundry cluster. Root cause per LandBlockInfo.NumCells's own doc comment: interior cells live at dat id 0xAAAA0100 + N where AAAA is the landblock id high word and N runs from 0 to NumCells-1. Each EnvCell has a StaticObjects list (List) holding in-building decorations — statues, furniture, lamps, crates, rugs, and the like. We weren't loading any of it. GameWindow.OnLoad now iterates each landblock's LandBlockInfo.NumCells, loads each EnvCell at the canonical id, walks its StaticObjects, and hydrates each as a WorldEntity. Position is composed as: worldPos = landblockOffset + cellOrigin + (cellRotation * stabLocal) worldRot = cellRotation * stabRotation where cellOrigin/cellRotation come from EnvCell.Position (a Frame in landblock-local space) and stabLocal/stabRotation come from the Stab's own Frame (cell-local space). Cell rotation is applied to the stab position because some cells in AC are rotated relative to the landblock grid. Entity counts on Holtburg 3x3: Stabs + Buildings 239 Procedural scenery 419 Interior StaticObjects 475 (NEW) ------- Total 1133 Phase 2d done. Interior geometry (walls, floors, ceilings) is still not rendered — cell shells are Phase 3+ work. Only the StaticObjects list is walked, which is enough to surface the visible decorations the user sees inside buildings in real AC. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 97 ++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index b4d4c21..10196b6 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -320,8 +320,103 @@ public sealed class GameWindow : IDisposable } Console.WriteLine($"scenery: spawned {scenerySpawned} entities across {worldView.Landblocks.Count} landblocks"); + // Phase 2d: walk interior EnvCells and add their StaticObjects. Buildings' + // rooftop statues, doors, interior decorations, and other in-building static + // objects live here rather than in LandBlockInfo.Objects. EnvCell ids for a + // landblock are packed at 0xAAAABBBB where AAAA is the landblock id high word + // and BBBB starts at 0x0100 — documented on LandBlockInfo.NumCells. + int interiorSpawned = 0; + uint interiorIdCounter = 0x40000000u; // distinct from scenery (0x80000000+) and stabs + foreach (var lb in worldView.Landblocks) + { + // Re-fetch LandBlockInfo to get NumCells. WorldView.LoadedLandblock exposes + // Heightmap + Entities but not the raw info record. + var lbInfo = _dats.Get((lb.LandblockId & 0xFFFF0000u) | 0xFFFEu); + if (lbInfo is null || lbInfo.NumCells == 0) continue; + + int lbX = (int)((lb.LandblockId >> 24) & 0xFFu); + int lbY = (int)((lb.LandblockId >> 16) & 0xFFu); + var lbOffset = new System.Numerics.Vector3( + (lbX - centerX) * 192f, + (lbY - centerY) * 192f, + 0f); + + // Interior cells start at 0xAAAA0100 and run for NumCells. + uint firstCellId = (lb.LandblockId & 0xFFFF0000u) | 0x0100u; + for (uint offset = 0; offset < lbInfo.NumCells; offset++) + { + var envCell = _dats.Get(firstCellId + offset); + if (envCell is null) continue; + + foreach (var stab in envCell.StaticObjects) + { + // Resolve stab id to mesh (same as LandBlockInfo.Objects). + var meshRefs = new List(); + if ((stab.Id & 0xFF000000u) == 0x01000000u) + { + var gfx = _dats.Get(stab.Id); + if (gfx is not null) + { + var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx); + _staticMesh.EnsureUploaded(stab.Id, subMeshes); + meshRefs.Add(new AcDream.Core.World.MeshRef(stab.Id, System.Numerics.Matrix4x4.Identity)); + } + } + else if ((stab.Id & 0xFF000000u) == 0x02000000u) + { + var setup = _dats.Get(stab.Id); + if (setup is not null) + { + var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup); + foreach (var mr in flat) + { + var gfx = _dats.Get(mr.GfxObjId); + if (gfx is null) continue; + var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx); + _staticMesh.EnsureUploaded(mr.GfxObjId, subMeshes); + meshRefs.Add(mr); + } + } + } + + if (meshRefs.Count == 0) continue; + + // Compose: the stab's position is cell-local, and the cell's Position + // is landblock-local. Rotate the stab-local position by the cell's + // orientation before adding the cell origin. + var stabLocalPos = stab.Frame.Origin; + var cellOrigin = envCell.Position.Origin; + var cellRot = envCell.Position.Orientation; + var rotatedStab = System.Numerics.Vector3.Transform(stabLocalPos, cellRot); + var landblockLocalPos = cellOrigin + rotatedStab; + var worldPos = landblockLocalPos + lbOffset; + var worldRot = cellRot * stab.Frame.Orientation; + + var hydrated = new AcDream.Core.World.WorldEntity + { + Id = interiorIdCounter++, + SourceGfxObjOrSetupId = stab.Id, + Position = worldPos, + Rotation = worldRot, + MeshRefs = meshRefs, + }; + hydratedEntities.Add(hydrated); + + var snapshot = new AcDream.Plugin.Abstractions.WorldEntitySnapshot( + Id: hydrated.Id, + SourceId: hydrated.SourceGfxObjOrSetupId, + Position: hydrated.Position, + Rotation: hydrated.Rotation); + _worldGameState.Add(snapshot); + _worldEvents.FireEntitySpawned(snapshot); + interiorSpawned++; + } + } + } + Console.WriteLine($"interior: spawned {interiorSpawned} static objects from EnvCells"); + _entities = hydratedEntities; - Console.WriteLine($"hydrated {_entities.Count} entities total (stabs + buildings + scenery)"); + Console.WriteLine($"hydrated {_entities.Count} entities total (stabs + buildings + scenery + interior)"); } ///