feat(headless): share immutable gameplay content
This commit is contained in:
parent
12b500d383
commit
9569dadb57
25 changed files with 1031 additions and 429 deletions
|
|
@ -1,4 +1,3 @@
|
|||
using System.Collections.Immutable;
|
||||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
|
||||
|
|
@ -102,7 +101,9 @@ public sealed class LandblockBuildFactory
|
|||
}
|
||||
|
||||
AcDream.Core.World.LandblockCollisionBuild collisions =
|
||||
BuildPreparedCollisionClosure(build.Landblock);
|
||||
LandblockPhysicsContentBuilder.BuildPreparedCollisionClosure(
|
||||
_preparedCollisions,
|
||||
build.Landblock);
|
||||
AcDream.Core.World.PhysicsDatBundle publicationDats =
|
||||
(build.Landblock.PhysicsDats
|
||||
?? AcDream.Core.World.PhysicsDatBundle.Empty)
|
||||
|
|
@ -155,59 +156,11 @@ public sealed class LandblockBuildFactory
|
|||
// expand into per-part MeshRefs via SetupMesh.Flatten.
|
||||
// GPU upload happens later through the render-thread presentation
|
||||
// pipeline, never in this worker-side build.
|
||||
var hydrated = new List<AcDream.Core.World.WorldEntity>(baseLoaded.Entities.Count);
|
||||
foreach (var e in baseLoaded.Entities)
|
||||
{
|
||||
var meshRefs = new List<AcDream.Core.World.MeshRef>();
|
||||
var stabBounds = new AcDream.Core.Meshing.LocalBoundsAccumulator();
|
||||
|
||||
if ((e.SourceGfxObjOrSetupId & 0xFF000000u) == 0x01000000u)
|
||||
{
|
||||
// Single GfxObj stab — identity part transform.
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(e.SourceGfxObjOrSetupId);
|
||||
if (gfx is not null)
|
||||
{
|
||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||
if (pb is not null) stabBounds.Add(System.Numerics.Matrix4x4.Identity, pb.Value);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(
|
||||
e.SourceGfxObjOrSetupId, System.Numerics.Matrix4x4.Identity));
|
||||
}
|
||||
}
|
||||
else if ((e.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u)
|
||||
{
|
||||
// Multi-part Setup — flatten to per-part GfxObj refs.
|
||||
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(e.SourceGfxObjOrSetupId);
|
||||
if (setup is not null)
|
||||
{
|
||||
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
||||
foreach (var mr in flat)
|
||||
{
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||
if (pb is not null) stabBounds.Add(mr.PartTransform, pb.Value);
|
||||
meshRefs.Add(mr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (meshRefs.Count == 0) continue;
|
||||
|
||||
var entity = new AcDream.Core.World.WorldEntity
|
||||
{
|
||||
Id = e.Id,
|
||||
SourceGfxObjOrSetupId = e.SourceGfxObjOrSetupId,
|
||||
Position = e.Position + worldOffset,
|
||||
Rotation = e.Rotation,
|
||||
MeshRefs = meshRefs,
|
||||
EffectCellId = e.EffectCellId,
|
||||
IsBuildingShell = e.IsBuildingShell, // Phase A8: preserve dat-level tag
|
||||
BuildingShellAnchorCellId = e.BuildingShellAnchorCellId,
|
||||
};
|
||||
if (stabBounds.TryGet(out var sbMin, out var sbMax))
|
||||
entity.SetLocalBounds(sbMin, sbMax);
|
||||
hydrated.Add(entity);
|
||||
}
|
||||
IReadOnlyList<AcDream.Core.World.WorldEntity> hydrated =
|
||||
LandblockPhysicsContentBuilder.HydrateStaticEntities(
|
||||
_dats,
|
||||
baseLoaded,
|
||||
worldOffset);
|
||||
|
||||
// Task 8: merge stabs + scenery + interior into one entity list.
|
||||
var merged = new List<AcDream.Core.World.WorldEntity>(hydrated);
|
||||
|
|
@ -226,7 +179,10 @@ public sealed class LandblockBuildFactory
|
|||
|
||||
// Pre-read every DAT object publication consumes. Build this after
|
||||
// `merged` so all entity GfxObj/Setup ids are known.
|
||||
var physicsDats = BuildPhysicsDatBundle(landblockId, merged);
|
||||
var physicsDats = LandblockPhysicsContentBuilder.BuildDatBundle(
|
||||
_dats,
|
||||
landblockId,
|
||||
merged);
|
||||
var completedEnvCells = envCellBuild.Build();
|
||||
return new AcDream.App.Streaming.LandblockBuild(
|
||||
new AcDream.Core.World.LoadedLandblock(
|
||||
|
|
@ -237,180 +193,6 @@ public sealed class LandblockBuildFactory
|
|||
completedEnvCells,
|
||||
request.Origin);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Pre-reads every DAT object that landblock publication consumes, so the
|
||||
/// update thread performs no DAT reads. Gather and publication must
|
||||
/// enumerate the same identifiers. Reads only; no runtime registration.
|
||||
/// </summary>
|
||||
private AcDream.Core.World.PhysicsDatBundle BuildPhysicsDatBundle(
|
||||
uint landblockId,
|
||||
System.Collections.Generic.IReadOnlyList<AcDream.Core.World.WorldEntity> entities)
|
||||
{
|
||||
var envCells = new System.Collections.Generic.Dictionary<uint, DatReaderWriter.DBObjs.EnvCell>();
|
||||
var environments = new System.Collections.Generic.Dictionary<uint, DatReaderWriter.DBObjs.Environment>();
|
||||
var setups = new System.Collections.Generic.Dictionary<uint, DatReaderWriter.DBObjs.Setup>();
|
||||
var gfxObjs = new System.Collections.Generic.Dictionary<uint, DatReaderWriter.DBObjs.GfxObj>();
|
||||
|
||||
// (1) LandBlockInfo
|
||||
var lbInfo = _dats!.Get<DatReaderWriter.DBObjs.LandBlockInfo>(
|
||||
(landblockId & 0xFFFF0000u) | 0xFFFEu);
|
||||
|
||||
if (lbInfo is not null)
|
||||
{
|
||||
// (2)+(3) EnvCell + Environment, per cell
|
||||
if (lbInfo.NumCells > 0)
|
||||
{
|
||||
uint firstCellId = (landblockId & 0xFFFF0000u) | 0x0100u;
|
||||
for (uint offset = 0; offset < lbInfo.NumCells; offset++)
|
||||
{
|
||||
uint envCellId = firstCellId + offset;
|
||||
var envCell = _dats.Get<DatReaderWriter.DBObjs.EnvCell>(envCellId);
|
||||
if (envCell is null) continue;
|
||||
envCells[envCellId] = envCell;
|
||||
if (envCell.EnvironmentId == 0) continue;
|
||||
uint envId = 0x0D000000u | envCell.EnvironmentId;
|
||||
if (!environments.ContainsKey(envId))
|
||||
{
|
||||
var environment = _dats.Get<DatReaderWriter.DBObjs.Environment>(envId);
|
||||
if (environment is not null) environments[envId] = environment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (4) building shell Setup, per building
|
||||
foreach (var building in lbInfo.Buildings)
|
||||
{
|
||||
uint modelId = building.ModelId;
|
||||
if ((modelId & 0xFF000000u) == 0x02000000u && !setups.ContainsKey(modelId))
|
||||
{
|
||||
var bldSetup = _dats.Get<DatReaderWriter.DBObjs.Setup>(modelId);
|
||||
if (bldSetup is not null) setups[modelId] = bldSetup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (5)+(6) entity GfxObj (BSP cache) + entity Setup (ShadowObjects parts/lights)
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
uint src = entity.SourceGfxObjOrSetupId;
|
||||
if ((src & 0xFF000000u) == 0x02000000u && !setups.ContainsKey(src))
|
||||
{
|
||||
var s = _dats.Get<DatReaderWriter.DBObjs.Setup>(src);
|
||||
if (s is not null) setups[src] = s;
|
||||
}
|
||||
foreach (var meshRef in entity.MeshRefs)
|
||||
{
|
||||
uint gid = meshRef.GfxObjId;
|
||||
if ((gid & 0xFF000000u) != 0x01000000u) continue;
|
||||
if (gfxObjs.ContainsKey(gid)) continue;
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(gid);
|
||||
if (gfx is not null) gfxObjs[gid] = gfx;
|
||||
}
|
||||
}
|
||||
|
||||
return new AcDream.Core.World.PhysicsDatBundle(lbInfo, envCells, environments, setups, gfxObjs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the exact prepared collision closure after the serialized DAT
|
||||
/// transaction completes. The immutable package is independently
|
||||
/// thread-safe, so these reads never extend the shared DAT-reader lock.
|
||||
/// Missing or corrupt prepared data fails the complete near-tier build;
|
||||
/// production must not publish a graph-only partial generation.
|
||||
/// </summary>
|
||||
private AcDream.Core.World.LandblockCollisionBuild
|
||||
BuildPreparedCollisionClosure(
|
||||
AcDream.Core.World.LoadedLandblock landblock)
|
||||
{
|
||||
AcDream.Core.World.PhysicsDatBundle dats =
|
||||
landblock.PhysicsDats ??
|
||||
AcDream.Core.World.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,
|
||||
AcDream.Core.Physics.FlatGfxObjCollisionAsset>();
|
||||
var setups =
|
||||
ImmutableDictionary.CreateBuilder<
|
||||
uint,
|
||||
AcDream.Core.Physics.FlatSetupCollision>();
|
||||
var cellStructures =
|
||||
ImmutableDictionary.CreateBuilder<
|
||||
uint,
|
||||
AcDream.Core.Physics.FlatCellStructureCollisionAsset>();
|
||||
var envCells =
|
||||
ImmutableDictionary.CreateBuilder<
|
||||
uint,
|
||||
AcDream.Core.Physics.FlatEnvCellTopology>();
|
||||
|
||||
foreach (uint id in gfxObjIds)
|
||||
{
|
||||
gfxObjs.Add(
|
||||
id,
|
||||
RequirePrepared(
|
||||
_preparedCollisions.ReadGfxObjCollision(id),
|
||||
"GfxObj collision",
|
||||
id));
|
||||
}
|
||||
|
||||
foreach (uint id in setupIds)
|
||||
{
|
||||
setups.Add(
|
||||
id,
|
||||
RequirePrepared(
|
||||
_preparedCollisions.ReadSetupCollision(id),
|
||||
"Setup collision",
|
||||
id));
|
||||
}
|
||||
|
||||
foreach (uint id in envCellIds)
|
||||
{
|
||||
cellStructures.Add(
|
||||
id,
|
||||
RequirePrepared(
|
||||
_preparedCollisions.ReadCellStructureCollision(id),
|
||||
"CellStruct collision",
|
||||
id));
|
||||
envCells.Add(
|
||||
id,
|
||||
RequirePrepared(
|
||||
_preparedCollisions.ReadEnvCellTopology(id),
|
||||
"EnvCell topology",
|
||||
id));
|
||||
}
|
||||
|
||||
return new AcDream.Core.World.LandblockCollisionBuild(
|
||||
gfxObjs.ToImmutable(),
|
||||
setups.ToImmutable(),
|
||||
cellStructures.ToImmutable(),
|
||||
envCells.ToImmutable(),
|
||||
gfxObjIds,
|
||||
setupIds,
|
||||
envCellIds);
|
||||
}
|
||||
|
||||
private static T RequirePrepared<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.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A.1 Task 8: generate scenery (trees, rocks, bushes) for a single
|
||||
/// landblock on the worker thread. Pure CPU — no GL calls.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue