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)"); } ///