feat(headless): hydrate isolated collision worlds

This commit is contained in:
Erik 2026-07-27 09:25:58 +02:00
parent 9569dadb57
commit b6547ff38c
20 changed files with 1960 additions and 101 deletions

View file

@ -4,6 +4,7 @@ using AcDream.Core.Meshing;
using AcDream.Core.Physics;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
namespace AcDream.Content;
@ -16,10 +17,16 @@ namespace AcDream.Content;
/// </summary>
public static class LandblockPhysicsContentBuilder
{
public readonly record struct StaticCollisionPublication(
int BspOwnerCount,
int SetupOwnerCount,
int NoCollisionCount);
public static IReadOnlyList<WorldEntity> HydrateStaticEntities(
IDatReaderWriter dats,
LoadedLandblock source,
Vector3 worldOffset)
Vector3 worldOffset,
bool includeVisualBounds = true)
{
ArgumentNullException.ThrowIfNull(dats);
ArgumentNullException.ThrowIfNull(source);
@ -35,13 +42,12 @@ public static class LandblockPhysicsContentBuilder
GfxObj? gfx = dats.Get<GfxObj>(sourceId);
if (gfx is not null)
{
(Vector3 Min, Vector3 Max)? partBounds =
GfxObjBounds.Get(gfx);
if (partBounds is not null)
if (includeVisualBounds
&& GfxObjBounds.Get(gfx) is { } partBounds)
{
bounds.Add(
Matrix4x4.Identity,
partBounds.Value);
partBounds);
}
meshRefs.Add(new MeshRef(
sourceId,
@ -59,13 +65,12 @@ public static class LandblockPhysicsContentBuilder
dats.Get<GfxObj>(meshRef.GfxObjId);
if (gfx is null)
continue;
(Vector3 Min, Vector3 Max)? partBounds =
GfxObjBounds.Get(gfx);
if (partBounds is not null)
if (includeVisualBounds
&& GfxObjBounds.Get(gfx) is { } partBounds)
{
bounds.Add(
meshRef.PartTransform,
partBounds.Value);
partBounds);
}
meshRefs.Add(meshRef);
}
@ -94,6 +99,151 @@ public static class LandblockPhysicsContentBuilder
return hydrated;
}
/// <summary>
/// Hydrates retail's deterministic Region/Scene-generated outdoor
/// scenery into the same immutable entity shape consumed by graphical
/// and direct hosts. The supplied offset is the host's current
/// landblock-relative world origin; DAT placement remains local.
/// </summary>
public static IReadOnlyList<WorldEntity> HydrateProceduralScenery(
IDatReaderWriter dats,
LoadedLandblock source,
Vector3 worldOffset,
ReadOnlySpan<float> heightTable,
bool includeVisualBounds = true)
{
ArgumentNullException.ThrowIfNull(dats);
ArgumentNullException.ThrowIfNull(source);
if (heightTable.Length < 256)
{
throw new ArgumentException(
"The retail terrain height table must contain at least 256 entries.",
nameof(heightTable));
}
Region? region = dats.Get<Region>(0x13000000u);
if (region is null)
return [];
HashSet<int>? buildingCells = null;
LandBlockInfo? info = dats.Get<LandBlockInfo>(
(source.LandblockId & 0xFFFF0000u) | 0xFFFEu);
if (info is not null)
{
buildingCells = [];
foreach (BuildingInfo building in info.Buildings)
{
int cellX = Math.Clamp(
(int)(building.Frame.Origin.X / 24f),
0,
8);
int cellY = Math.Clamp(
(int)(building.Frame.Origin.Y / 24f),
0,
8);
buildingCells.Add(cellX * 9 + cellY);
}
}
IReadOnlyList<SceneryGenerator.ScenerySpawn> spawns =
SceneryGenerator.Generate(
dats,
region,
source.Heightmap,
source.LandblockId,
buildingCells);
if (spawns.Count == 0)
return [];
var entities = new List<WorldEntity>(spawns.Count);
uint landblockX =
(source.LandblockId >> 24) & 0xFFu;
uint landblockY =
(source.LandblockId >> 16) & 0xFFu;
uint sceneryCounter = 0u;
foreach (SceneryGenerator.ScenerySpawn spawn in spawns)
{
var meshRefs = new List<MeshRef>();
var bounds = new LocalBoundsAccumulator();
Matrix4x4 scale =
Matrix4x4.CreateScale(spawn.Scale);
if ((spawn.ObjectId & 0xFF000000u) == 0x01000000u)
{
GfxObj? gfx = dats.Get<GfxObj>(spawn.ObjectId);
if (gfx is not null)
{
if (includeVisualBounds
&& GfxObjBounds.Get(gfx) is { } partBounds)
{
bounds.Add(scale, partBounds);
}
meshRefs.Add(new MeshRef(spawn.ObjectId, scale));
}
}
else if ((spawn.ObjectId & 0xFF000000u) == 0x02000000u)
{
Setup? setup = dats.Get<Setup>(spawn.ObjectId);
if (setup is not null)
{
foreach (MeshRef meshRef in SetupMesh.Flatten(setup))
{
GfxObj? gfx =
dats.Get<GfxObj>(meshRef.GfxObjId);
if (gfx is null)
continue;
Matrix4x4 partTransform =
meshRef.PartTransform * scale;
if (includeVisualBounds
&& GfxObjBounds.Get(gfx) is { } partBounds)
{
bounds.Add(partTransform, partBounds);
}
meshRefs.Add(new MeshRef(
meshRef.GfxObjId,
partTransform));
}
}
}
if (meshRefs.Count == 0)
continue;
float localX = spawn.LocalPosition.X;
float localY = spawn.LocalPosition.Y;
float groundZ = TerrainSurface.SampleZFromHeightmap(
source.Heightmap.Height,
heightTable,
landblockX,
landblockY,
localX,
localY);
var entity = new WorldEntity
{
Id = ProceduralSceneryIdAllocator.Allocate(
landblockX,
landblockY,
ref sceneryCounter),
SourceGfxObjOrSetupId = spawn.ObjectId,
Position = new Vector3(
localX,
localY,
groundZ + spawn.LocalPosition.Z)
+ worldOffset,
Rotation = spawn.Rotation,
MeshRefs = meshRefs,
Scale = spawn.Scale,
EffectCellId =
TerrainSurface.ComputeOutdoorCellId(
source.LandblockId,
localX,
localY),
};
if (bounds.TryGet(out Vector3 min, out Vector3 max))
entity.SetLocalBounds(min, max);
entities.Add(entity);
}
return entities;
}
public static PhysicsDatBundle BuildDatBundle(
IDatReaderWriter dats,
uint landblockId,
@ -247,6 +397,353 @@ public static class LandblockPhysicsContentBuilder
envCellIds);
}
public static TerrainSurface BuildTerrainSurface(
LoadedLandblock landblock,
ReadOnlySpan<float> heightTable)
{
ArgumentNullException.ThrowIfNull(landblock);
if (heightTable.Length < 256)
{
throw new ArgumentException(
"The retail terrain height table must contain at least 256 entries.",
nameof(heightTable));
}
uint landblockX = (landblock.LandblockId >> 24) & 0xFFu;
uint landblockY = (landblock.LandblockId >> 16) & 0xFFu;
var terrainBytes = new byte[81];
for (int index = 0; index < terrainBytes.Length; index++)
{
terrainBytes[index] =
(byte)(ushort)landblock.Heightmap.Terrain[index];
}
return new TerrainSurface(
landblock.Heightmap.Height,
heightTable,
landblockX,
landblockY,
terrainBytes);
}
public static void PublishPreparedCells(
PhysicsDataCache cache,
LoadedLandblock landblock,
LandblockCollisionBuild collisions,
Vector3 origin,
ICollection<CellSurface> cellSurfaces,
ICollection<PortalPlane> portalPlanes)
{
ArgumentNullException.ThrowIfNull(cache);
ArgumentNullException.ThrowIfNull(landblock);
ArgumentNullException.ThrowIfNull(collisions);
ArgumentNullException.ThrowIfNull(cellSurfaces);
ArgumentNullException.ThrowIfNull(portalPlanes);
PhysicsDatBundle dats =
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
uint count = dats.Info?.NumCells ?? 0u;
for (uint offset = 0; offset < count; offset++)
{
uint envCellId =
(landblock.LandblockId & 0xFFFF0000u)
| (0x0100u + offset);
if (!dats.EnvCells.TryGetValue(
envCellId,
out EnvCell? envCell)
|| !collisions.CellStructures.TryGetValue(
envCellId,
out FlatCellStructureCollisionAsset? structure)
|| !collisions.EnvCells.TryGetValue(
envCellId,
out FlatEnvCellTopology? topology))
{
continue;
}
Quaternion rotation = envCell.Position.Orientation;
Vector3 cellOriginWorld =
envCell.Position.Origin + origin;
Matrix4x4 transform =
Matrix4x4.CreateFromQuaternion(rotation)
* Matrix4x4.CreateTranslation(cellOriginWorld);
cache.CacheCellStruct(
envCellId,
envCell,
transform,
structure,
topology);
FlatPolygonTable portalPolygons =
structure.PortalPolygons;
for (int portalIndex = 0;
portalIndex < topology.Portals.Length;
portalIndex++)
{
FlatEnvCellPortal portal =
topology.Portals[portalIndex];
if ((uint)portal.PolygonIndex
>= (uint)portalPolygons.Polygons.Length)
{
continue;
}
FlatCollisionPolygon polygon =
portalPolygons.Polygons[portal.PolygonIndex];
if (polygon.VertexRange.Count < 3)
continue;
var vertices =
new Vector3[polygon.VertexRange.Count];
for (int index = 0; index < vertices.Length; index++)
{
Vector3 local = portalPolygons.Vertices[
polygon.VertexRange.Start + index];
vertices[index] =
Vector3.Transform(local, rotation)
+ cellOriginWorld;
}
portalPlanes.Add(PortalPlane.FromVertices(
vertices.AsSpan(),
portal.OtherCellId,
envCellId & 0xFFFFu,
portal.Flags));
}
cellSurfaces.Add(new CellSurface(
envCellId,
structure.PhysicsBsp.PolygonTable,
rotation,
cellOriginWorld));
}
}
public static void CacheBuildings(
PhysicsDataCache cache,
LoadedLandblock landblock,
TerrainSurface terrain,
Vector3 origin)
{
ArgumentNullException.ThrowIfNull(cache);
ArgumentNullException.ThrowIfNull(landblock);
ArgumentNullException.ThrowIfNull(terrain);
PhysicsDatBundle dats =
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
if (dats.Info is not { } info)
return;
uint prefix = landblock.LandblockId & 0xFFFF0000u;
foreach (BuildingInfo building in info.Buildings)
{
var portals =
new List<BldPortalInfo>(building.Portals.Count);
foreach (var portal in building.Portals)
{
portals.Add(new BldPortalInfo(
prefix | (uint)portal.OtherCellId,
unchecked((short)portal.OtherPortalId),
(ushort)portal.Flags));
}
Vector3 buildingOrigin = building.Frame.Origin + origin;
Matrix4x4 transform =
Matrix4x4.CreateFromQuaternion(
building.Frame.Orientation)
* Matrix4x4.CreateTranslation(buildingOrigin);
uint landcellId = prefix
| terrain.ComputeOutdoorCellId(
building.Frame.Origin.X,
building.Frame.Origin.Y);
uint shellPartZero = building.ModelId;
if ((shellPartZero & 0xFF000000u) == 0x02000000u)
{
dats.Setups.TryGetValue(
building.ModelId,
out Setup? setup);
shellPartZero =
setup is not null && setup.Parts.Count > 0
? setup.Parts[0]
: 0u;
}
cache.CacheBuilding(
landcellId,
portals,
transform,
shellPartZero);
}
}
public static void CachePreparedObjects(
PhysicsDataCache cache,
LandblockCollisionBuild collisions)
{
ArgumentNullException.ThrowIfNull(cache);
ArgumentNullException.ThrowIfNull(collisions);
foreach ((uint id, FlatGfxObjCollisionAsset asset) in
collisions.GfxObjs)
{
cache.CacheGfxObj(id, asset);
}
foreach ((uint id, FlatSetupCollision setup) in
collisions.Setups)
{
cache.CacheSetup(id, setup);
}
}
public static StaticCollisionPublication PublishStaticCollision(
PhysicsEngine engine,
PhysicsDataCache cache,
LoadedLandblock landblock,
LandblockCollisionBuild collisions,
Vector3 origin)
{
ArgumentNullException.ThrowIfNull(engine);
ArgumentNullException.ThrowIfNull(cache);
ArgumentNullException.ThrowIfNull(landblock);
ArgumentNullException.ThrowIfNull(collisions);
int bspOwners = 0;
int setupOwners = 0;
int noCollision = 0;
foreach (WorldEntity entity in landblock.Entities)
{
if (entity.IsBuildingShell)
continue;
IReadOnlyList<ShadowShape> bspShapes =
ShadowShapeBuilder.FromLandblockBspParts(
entity.MeshRefs,
entity.IsBuildingShell,
cache.GetGfxObj);
if (bspShapes.Count > 0)
{
engine.ShadowObjects.RegisterMultiPart(
entity.Id,
entity.Position,
entity.Rotation,
bspShapes,
0u,
EntityCollisionFlags.None,
worldOffsetX: origin.X,
worldOffsetY: origin.Y,
landblockId: landblock.LandblockId,
seedCellId: entity.ParentCellId ?? 0u,
isStatic: true);
bspOwners++;
continue;
}
FlatSetupCollision? setup =
cache.GetFlatSetup(
entity.SourceGfxObjOrSetupId);
if (setup is null)
{
noCollision++;
continue;
}
float scale = entity.Scale > 0f ? entity.Scale : 1f;
var setupShapes = new List<ShadowShape>();
for (int index = 0;
index < setup.Cylinders.Length;
index++)
{
FlatCollisionCylinder cylinder =
setup.Cylinders[index];
float radius = cylinder.Radius * scale;
float height = (cylinder.Height > 0f
? cylinder.Height
: cylinder.Radius * 4f) * scale;
if (radius <= 0f)
continue;
setupShapes.Add(new ShadowShape(
entity.SourceGfxObjOrSetupId,
cylinder.Origin * scale,
Quaternion.Identity,
scale,
ShadowCollisionType.Cylinder,
radius,
height));
}
if (setup.Cylinders.Length == 0)
{
for (int index = 0;
index < setup.Spheres.Length;
index++)
{
FlatCollisionSphere sphere = setup.Spheres[index];
if (sphere.Radius <= 0f)
continue;
float radius = sphere.Radius * scale;
Vector3 localOffset = sphere.Origin * scale;
Vector3 localBaseOffset = localOffset
+ Vector3.Transform(
-Vector3.UnitZ * radius,
Quaternion.Inverse(entity.Rotation));
setupShapes.Add(new ShadowShape(
entity.SourceGfxObjOrSetupId,
localBaseOffset,
Quaternion.Identity,
scale,
ShadowCollisionType.Cylinder,
radius,
radius * 2f));
}
}
if (setup.Cylinders.Length == 0
&& setup.Spheres.Length == 0
&& setup.Radius > 0f)
{
float radius = setup.Radius * scale;
float height = (setup.Height > 0f
? setup.Height
: setup.Radius * 2f) * scale;
setupShapes.Add(new ShadowShape(
entity.SourceGfxObjOrSetupId,
Vector3.Zero,
Quaternion.Identity,
scale,
ShadowCollisionType.Cylinder,
radius,
height));
}
if (setupShapes.Count == 0)
{
noCollision++;
continue;
}
engine.ShadowObjects.RegisterMultiPart(
entity.Id,
entity.Position,
entity.Rotation,
setupShapes,
0u,
EntityCollisionFlags.None,
worldOffsetX: origin.X,
worldOffsetY: origin.Y,
landblockId: landblock.LandblockId,
seedCellId: entity.ParentCellId ?? 0u,
isStatic: true);
setupOwners++;
}
uint[] reflood =
engine.ShadowObjects.CaptureRefloodOwnersForLandblock(
landblock.LandblockId);
foreach (uint ownerId in reflood)
{
engine.ShadowObjects.RefloodOwnerForLandblock(
ownerId,
landblock.LandblockId);
}
return new StaticCollisionPublication(
bspOwners,
setupOwners,
noCollision);
}
private static T Require<T>(
PreparedCollisionReadResult<T> result,
string kind,