feat(headless): share immutable gameplay content

This commit is contained in:
Erik 2026-07-27 09:00:48 +02:00
parent 12b500d383
commit 9569dadb57
25 changed files with 1031 additions and 429 deletions

View file

@ -0,0 +1,266 @@
using System.Collections.Immutable;
using System.Numerics;
using AcDream.Core.Meshing;
using AcDream.Core.Physics;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
namespace AcDream.Content;
/// <summary>
/// Presentation-independent content half of one near-landblock collision
/// build. Graphical and no-window hosts consume the same parsed DAT closure,
/// immutable package collision records, and static Setup/GfxObj identities.
/// No Runtime world or backend resource is mutated here.
/// </summary>
public static class LandblockPhysicsContentBuilder
{
public static IReadOnlyList<WorldEntity> HydrateStaticEntities(
IDatReaderWriter dats,
LoadedLandblock source,
Vector3 worldOffset)
{
ArgumentNullException.ThrowIfNull(dats);
ArgumentNullException.ThrowIfNull(source);
var hydrated = new List<WorldEntity>(source.Entities.Count);
foreach (WorldEntity candidate in source.Entities)
{
var meshRefs = new List<MeshRef>();
var bounds = new LocalBoundsAccumulator();
uint sourceId = candidate.SourceGfxObjOrSetupId;
if ((sourceId & 0xFF000000u) == 0x01000000u)
{
GfxObj? gfx = dats.Get<GfxObj>(sourceId);
if (gfx is not null)
{
(Vector3 Min, Vector3 Max)? partBounds =
GfxObjBounds.Get(gfx);
if (partBounds is not null)
{
bounds.Add(
Matrix4x4.Identity,
partBounds.Value);
}
meshRefs.Add(new MeshRef(
sourceId,
Matrix4x4.Identity));
}
}
else if ((sourceId & 0xFF000000u) == 0x02000000u)
{
Setup? setup = dats.Get<Setup>(sourceId);
if (setup is not null)
{
foreach (MeshRef meshRef in SetupMesh.Flatten(setup))
{
GfxObj? gfx =
dats.Get<GfxObj>(meshRef.GfxObjId);
if (gfx is null)
continue;
(Vector3 Min, Vector3 Max)? partBounds =
GfxObjBounds.Get(gfx);
if (partBounds is not null)
{
bounds.Add(
meshRef.PartTransform,
partBounds.Value);
}
meshRefs.Add(meshRef);
}
}
}
if (meshRefs.Count == 0)
continue;
var entity = new WorldEntity
{
Id = candidate.Id,
SourceGfxObjOrSetupId = sourceId,
Position = candidate.Position + worldOffset,
Rotation = candidate.Rotation,
MeshRefs = meshRefs,
EffectCellId = candidate.EffectCellId,
IsBuildingShell = candidate.IsBuildingShell,
BuildingShellAnchorCellId =
candidate.BuildingShellAnchorCellId,
};
if (bounds.TryGet(out Vector3 min, out Vector3 max))
entity.SetLocalBounds(min, max);
hydrated.Add(entity);
}
return hydrated;
}
public static PhysicsDatBundle BuildDatBundle(
IDatReaderWriter dats,
uint landblockId,
IReadOnlyList<WorldEntity> entities)
{
ArgumentNullException.ThrowIfNull(dats);
ArgumentNullException.ThrowIfNull(entities);
var envCells = new Dictionary<uint, EnvCell>();
var environments = new Dictionary<uint, DatEnvironment>();
var setups = new Dictionary<uint, Setup>();
var gfxObjs = new Dictionary<uint, GfxObj>();
LandBlockInfo? info = dats.Get<LandBlockInfo>(
(landblockId & 0xFFFF0000u) | 0xFFFEu);
if (info is not null)
{
uint firstCellId =
(landblockId & 0xFFFF0000u) | 0x0100u;
for (uint offset = 0; offset < info.NumCells; offset++)
{
uint envCellId = firstCellId + offset;
EnvCell? envCell = dats.Get<EnvCell>(envCellId);
if (envCell is null)
continue;
envCells[envCellId] = envCell;
if (envCell.EnvironmentId == 0)
continue;
uint environmentId =
0x0D000000u | envCell.EnvironmentId;
if (!environments.ContainsKey(environmentId)
&& dats.Get<DatEnvironment>(
environmentId) is { } environment)
{
environments[environmentId] = environment;
}
}
foreach (var building in info.Buildings)
{
uint setupId = building.ModelId;
if ((setupId & 0xFF000000u) == 0x02000000u
&& !setups.ContainsKey(setupId)
&& dats.Get<Setup>(setupId) is { } setup)
{
setups[setupId] = setup;
}
}
}
foreach (WorldEntity entity in entities)
{
uint sourceId = entity.SourceGfxObjOrSetupId;
if ((sourceId & 0xFF000000u) == 0x02000000u
&& !setups.ContainsKey(sourceId)
&& dats.Get<Setup>(sourceId) is { } setup)
{
setups[sourceId] = setup;
}
foreach (MeshRef meshRef in entity.MeshRefs)
{
uint gfxObjId = meshRef.GfxObjId;
if ((gfxObjId & 0xFF000000u) != 0x01000000u
|| gfxObjs.ContainsKey(gfxObjId))
{
continue;
}
if (dats.Get<GfxObj>(gfxObjId) is { } gfxObj)
gfxObjs[gfxObjId] = gfxObj;
}
}
return new PhysicsDatBundle(
info,
envCells,
environments,
setups,
gfxObjs);
}
public static LandblockCollisionBuild BuildPreparedCollisionClosure(
IPreparedCollisionSource source,
LoadedLandblock landblock)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(landblock);
PhysicsDatBundle dats =
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
ImmutableArray<uint> gfxObjIds =
[.. dats.GfxObjs.Keys.Order()];
ImmutableArray<uint> setupIds =
[.. dats.Setups.Keys.Order()];
ImmutableArray<uint> envCellIds =
[.. dats.EnvCells.Keys.Order()];
var gfxObjs = ImmutableDictionary.CreateBuilder<
uint,
FlatGfxObjCollisionAsset>();
var setups = ImmutableDictionary.CreateBuilder<
uint,
FlatSetupCollision>();
var cellStructures = ImmutableDictionary.CreateBuilder<
uint,
FlatCellStructureCollisionAsset>();
var envCells = ImmutableDictionary.CreateBuilder<
uint,
FlatEnvCellTopology>();
foreach (uint id in gfxObjIds)
{
gfxObjs.Add(
id,
Require(
source.ReadGfxObjCollision(id),
"GfxObj collision",
id));
}
foreach (uint id in setupIds)
{
setups.Add(
id,
Require(
source.ReadSetupCollision(id),
"Setup collision",
id));
}
foreach (uint id in envCellIds)
{
cellStructures.Add(
id,
Require(
source.ReadCellStructureCollision(id),
"CellStruct collision",
id));
envCells.Add(
id,
Require(
source.ReadEnvCellTopology(id),
"EnvCell topology",
id));
}
return new LandblockCollisionBuild(
gfxObjs.ToImmutable(),
setups.ToImmutable(),
cellStructures.ToImmutable(),
envCells.ToImmutable(),
gfxObjIds,
setupIds,
envCellIds);
}
private static T Require<T>(
PreparedCollisionReadResult<T> result,
string kind,
uint id)
where T : class
{
if (result.Status == PreparedAssetReadStatus.Loaded
&& result.Data is not null)
{
return result.Data;
}
throw new InvalidDataException(
$"{kind} 0x{id:X8} is {result.Status}. "
+ "The complete near-tier generation cannot be published.");
}
}