Own render, physics, DAT-static presentation, and retained static-resource rebinds behind retryable receipts so a failed publication resumes its exact unfinished suffix without replaying retail create-time defaults. Co-authored-by: Codex <noreply@openai.com>
694 lines
27 KiB
C#
694 lines
27 KiB
C#
using System.Diagnostics;
|
|
using System.Numerics;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Streaming;
|
|
|
|
/// <summary>
|
|
/// Exact receipt for the two physics-owned stages of one accepted landblock
|
|
/// publication. The render publisher commits buildings and EnvCells between
|
|
/// these stages without recomputing the captured origin.
|
|
/// </summary>
|
|
public sealed class LandblockPhysicsPublication
|
|
{
|
|
internal LandblockPhysicsPublication(
|
|
object owner,
|
|
LandblockBuild build,
|
|
Vector3 origin)
|
|
{
|
|
Owner = owner;
|
|
Build = build;
|
|
Origin = origin;
|
|
}
|
|
|
|
internal object Owner { get; }
|
|
internal LandblockBuild Build { get; }
|
|
internal bool BeginCommitted { get; set; }
|
|
internal bool CompletionCommitted { get; set; }
|
|
|
|
public uint LandblockId => Build.Landblock.LandblockId;
|
|
public Vector3 Origin { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cumulative update-thread diagnostics for physics publication and removal.
|
|
/// Durations are <see cref="Stopwatch"/> ticks.
|
|
/// </summary>
|
|
public readonly record struct LandblockPhysicsPublisherDiagnostics(
|
|
long BeginCount,
|
|
long CompleteCount,
|
|
long BasePublishTicks,
|
|
long GfxCacheTicks,
|
|
long CompletePublishTicks,
|
|
long CellSurfaceCount,
|
|
long PortalPlaneCount,
|
|
long BuildingCount,
|
|
long StaticBspOwnerCount,
|
|
long StaticCylinderOwnerCount,
|
|
long RefloodCount,
|
|
long DemotionCount,
|
|
long FullRemovalCount);
|
|
|
|
/// <summary>
|
|
/// Update-thread owner of streamed landblock physics publication. It consumes
|
|
/// only the immutable build payload: terrain, cells, portal planes, building
|
|
/// collision, cached GfxObj shapes, static BSP/cylinder registration, and the
|
|
/// final cross-cell reflood. It has no DAT reader or residency policy.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The order preserves retail <c>CObjCell::init_objects</c> (0x0052B420),
|
|
/// <c>CPhysicsObj::recalc_cross_cells</c> (0x00515A30),
|
|
/// <c>CBuildingObj::find_building_collisions</c> (0x006B5300), and
|
|
/// <c>CPhysicsObj::add_shadows_to_cells</c> (0x00514AE0). The BSP-or-Setup
|
|
/// collision gate follows <c>CPhysicsObj::FindObjCollisions</c> (0x0050F050).
|
|
/// Building shells use the per-landcell building channel; ordinary statics
|
|
/// use one multipart shadow owner or the mutually exclusive Setup
|
|
/// cylinder/sphere fallback.
|
|
/// </remarks>
|
|
public sealed class LandblockPhysicsPublisher
|
|
{
|
|
private readonly object _receiptOwner = new();
|
|
private readonly PhysicsEngine _physicsEngine;
|
|
private readonly PhysicsDataCache _physicsDataCache;
|
|
private readonly float[] _heightTable;
|
|
|
|
private long _beginCount;
|
|
private long _completeCount;
|
|
private long _basePublishTicks;
|
|
private long _gfxCacheTicks;
|
|
private long _completePublishTicks;
|
|
private long _cellSurfaceCount;
|
|
private long _portalPlaneCount;
|
|
private long _buildingCount;
|
|
private long _staticBspOwnerCount;
|
|
private long _staticCylinderOwnerCount;
|
|
private long _refloodCount;
|
|
private long _demotionCount;
|
|
private long _fullRemovalCount;
|
|
|
|
public LandblockPhysicsPublisher(
|
|
PhysicsEngine physicsEngine,
|
|
float[] heightTable)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(physicsEngine);
|
|
ArgumentNullException.ThrowIfNull(heightTable);
|
|
if (heightTable.Length < 256)
|
|
throw new ArgumentException(
|
|
"The retail terrain height table must contain at least 256 entries.",
|
|
nameof(heightTable));
|
|
|
|
_physicsEngine = physicsEngine;
|
|
_physicsDataCache = physicsEngine.DataCache
|
|
?? throw new ArgumentException(
|
|
"The physics engine must own its canonical data cache before publication wiring.",
|
|
nameof(physicsEngine));
|
|
_heightTable = (float[])heightTable.Clone();
|
|
}
|
|
|
|
public LandblockPhysicsPublisherDiagnostics Diagnostics => new(
|
|
_beginCount,
|
|
_completeCount,
|
|
_basePublishTicks,
|
|
_gfxCacheTicks,
|
|
_completePublishTicks,
|
|
_cellSurfaceCount,
|
|
_portalPlaneCount,
|
|
_buildingCount,
|
|
_staticBspOwnerCount,
|
|
_staticCylinderOwnerCount,
|
|
_refloodCount,
|
|
_demotionCount,
|
|
_fullRemovalCount);
|
|
|
|
/// <summary>
|
|
/// Publishes terrain, EnvCell physics, portal planes, and building shells.
|
|
/// The render publisher completes its building/EnvCell transaction before
|
|
/// <see cref="CompletePublication"/> publishes ordinary static collision.
|
|
/// </summary>
|
|
public LandblockPhysicsPublication BeginPublication(
|
|
LandblockRenderPublication renderPublication)
|
|
{
|
|
LandblockPhysicsPublication publication = PreparePublication(
|
|
renderPublication);
|
|
BeginPublication(publication);
|
|
return publication;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Captures the exact render receipt before physics mutation begins. The
|
|
/// coordinator retains this receipt and can retry the idempotent exact
|
|
/// replacement when a cache/backend operation fails.
|
|
/// </summary>
|
|
public LandblockPhysicsPublication PreparePublication(
|
|
LandblockRenderPublication renderPublication)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(renderPublication);
|
|
LandblockBuild build = renderPublication.Build;
|
|
Vector3 origin = renderPublication.Origin;
|
|
if (!IsFinite(origin))
|
|
throw new ArgumentOutOfRangeException(nameof(renderPublication));
|
|
|
|
return new LandblockPhysicsPublication(
|
|
_receiptOwner,
|
|
build,
|
|
origin);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advances the exact terrain/cell/building physics replacement from a
|
|
/// retained receipt.
|
|
/// </summary>
|
|
public void BeginPublication(LandblockPhysicsPublication publication)
|
|
{
|
|
ValidateReceipt(publication);
|
|
if (publication.BeginCommitted)
|
|
return;
|
|
|
|
long started = Stopwatch.GetTimestamp();
|
|
LandblockBuild build = publication.Build;
|
|
Vector3 origin = publication.Origin;
|
|
LoadedLandblock landblock = build.Landblock;
|
|
PhysicsDatBundle datBundle =
|
|
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
|
uint currentCellId = _physicsDataCache.CellGraph.CurrCell?.Id ?? 0u;
|
|
uint landblockX = (landblock.LandblockId >> 24) & 0xFFu;
|
|
uint landblockY = (landblock.LandblockId >> 16) & 0xFFu;
|
|
|
|
// A publication is an exact replacement for this landblock prefix.
|
|
// CacheCellStruct/CacheBuilding intentionally use retail first-wins
|
|
// semantics within one pass, so retire the prior pass before adding
|
|
// the replacement. Adjacent prefixes remain untouched.
|
|
_physicsDataCache.RemoveCellsForLandblock(landblock.LandblockId);
|
|
_physicsDataCache.RemoveBuildingsForLandblock(landblock.LandblockId);
|
|
_physicsDataCache.CellGraph.RemoveEnvCellsForLandblock(
|
|
landblock.LandblockId);
|
|
|
|
// TerrainInfo.Type occupies bits 2-6. The low byte retains those bits
|
|
// plus Road (bits 0-1), which TerrainSurface masks independently.
|
|
var terrainBytes = new byte[81];
|
|
for (int i = 0; i < terrainBytes.Length; i++)
|
|
terrainBytes[i] = (byte)(ushort)landblock.Heightmap.Terrain[i];
|
|
|
|
var terrainSurface = new TerrainSurface(
|
|
landblock.Heightmap.Height,
|
|
_heightTable,
|
|
landblockX,
|
|
landblockY,
|
|
terrainBytes);
|
|
var cellSurfaces = new List<CellSurface>();
|
|
var portalPlanes = new List<PortalPlane>();
|
|
|
|
DatReaderWriter.DBObjs.LandBlockInfo? landblockInfo = datBundle.Info;
|
|
if (landblockInfo is not null && landblockInfo.NumCells > 0)
|
|
{
|
|
uint firstCellId =
|
|
(landblock.LandblockId & 0xFFFF0000u) | 0x0100u;
|
|
for (uint offset = 0; offset < landblockInfo.NumCells; offset++)
|
|
{
|
|
uint envCellId = firstCellId + offset;
|
|
if (!datBundle.EnvCells.TryGetValue(envCellId, out var envCell))
|
|
continue;
|
|
if (envCell.EnvironmentId == 0)
|
|
continue;
|
|
if (!datBundle.Environments.TryGetValue(
|
|
0x0D000000u | envCell.EnvironmentId,
|
|
out var environment))
|
|
{
|
|
continue;
|
|
}
|
|
if (!environment.Cells.TryGetValue(
|
|
envCell.CellStructure,
|
|
out var cellStruct))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Quaternion rotation = envCell.Position.Orientation;
|
|
Vector3 cellOriginWorld = envCell.Position.Origin + origin;
|
|
Matrix4x4 physicsCellTransform =
|
|
Matrix4x4.CreateFromQuaternion(rotation)
|
|
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
|
|
|
_physicsDataCache.CacheCellStruct(
|
|
envCellId,
|
|
envCell,
|
|
cellStruct,
|
|
physicsCellTransform);
|
|
|
|
var worldVertices = new Dictionary<ushort, Vector3>(
|
|
cellStruct.VertexArray.Vertices.Count);
|
|
foreach ((ushort vertexId, var vertex) in
|
|
cellStruct.VertexArray.Vertices)
|
|
{
|
|
Vector3 worldPosition =
|
|
Vector3.Transform(vertex.Origin, rotation)
|
|
+ cellOriginWorld;
|
|
worldVertices[(ushort)vertexId] = worldPosition;
|
|
}
|
|
|
|
var polygonVertexIds = new List<List<short>>(
|
|
cellStruct.PhysicsPolygons.Count);
|
|
foreach (var polygon in cellStruct.PhysicsPolygons.Values)
|
|
{
|
|
var vertexIds = new List<short>(polygon.VertexIds.Count);
|
|
foreach (short vertexId in polygon.VertexIds)
|
|
vertexIds.Add(vertexId);
|
|
polygonVertexIds.Add(vertexIds);
|
|
}
|
|
cellSurfaces.Add(new CellSurface(
|
|
envCellId,
|
|
worldVertices,
|
|
polygonVertexIds));
|
|
|
|
// CellPortal.PolygonId indexes rendering Polygons, not
|
|
// PhysicsPolygons (ACViewer EnvCell.find_transit_cells).
|
|
foreach (var portal in envCell.CellPortals)
|
|
{
|
|
if (!cellStruct.Polygons.TryGetValue(
|
|
portal.PolygonId,
|
|
out var polygon)
|
|
|| polygon.VertexIds.Count < 3)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var portalVertices = new Vector3[polygon.VertexIds.Count];
|
|
bool allFound = true;
|
|
for (int index = 0; index < polygon.VertexIds.Count; index++)
|
|
{
|
|
if (!worldVertices.TryGetValue(
|
|
(ushort)polygon.VertexIds[index],
|
|
out portalVertices[index]))
|
|
{
|
|
allFound = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!allFound)
|
|
continue;
|
|
|
|
portalPlanes.Add(PortalPlane.FromVertices(
|
|
portalVertices.AsSpan(),
|
|
portal.OtherCellId,
|
|
envCellId & 0xFFFFu,
|
|
(ushort)portal.Flags));
|
|
}
|
|
}
|
|
}
|
|
|
|
int buildingCount = PublishBuildings(
|
|
landblock,
|
|
datBundle,
|
|
landblockInfo,
|
|
terrainSurface,
|
|
origin);
|
|
_physicsEngine.AddLandblock(
|
|
landblock.LandblockId,
|
|
terrainSurface,
|
|
cellSurfaces,
|
|
portalPlanes,
|
|
origin.X,
|
|
origin.Y);
|
|
if ((currentCellId & 0xFFFF0000u)
|
|
== (landblock.LandblockId & 0xFFFF0000u))
|
|
{
|
|
_physicsEngine.UpdatePlayerCurrCell(currentCellId);
|
|
}
|
|
|
|
_cellSurfaceCount += cellSurfaces.Count;
|
|
_portalPlaneCount += portalPlanes.Count;
|
|
_buildingCount += buildingCount;
|
|
publication.BeginCommitted = true;
|
|
_beginCount++;
|
|
_basePublishTicks += Stopwatch.GetTimestamp() - started;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Publishes ordinary static collision and refloods after the caller has
|
|
/// completed the render-owned building/EnvCell suffix. The callback keeps
|
|
/// the shipped per-entity light-before-collision order until checkpoint E
|
|
/// moves static presentation into the transaction coordinator.
|
|
/// </summary>
|
|
public void CompletePublication(
|
|
LandblockPhysicsPublication publication,
|
|
Action<WorldEntity>? beforeStaticCollision = null)
|
|
{
|
|
ValidateReceipt(publication);
|
|
if (!publication.BeginCommitted)
|
|
throw new InvalidOperationException(
|
|
"Physics publication cannot complete before its prefix commits.");
|
|
if (publication.CompletionCommitted)
|
|
return;
|
|
|
|
long completedStarted = Stopwatch.GetTimestamp();
|
|
LoadedLandblock landblock = publication.Build.Landblock;
|
|
PhysicsDatBundle datBundle =
|
|
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
|
|
|
long cacheStarted = Stopwatch.GetTimestamp();
|
|
foreach (WorldEntity entity in landblock.Entities)
|
|
{
|
|
foreach (MeshRef meshRef in entity.MeshRefs)
|
|
{
|
|
if ((meshRef.GfxObjId & 0xFF000000u) != 0x01000000u)
|
|
continue;
|
|
if (datBundle.GfxObjs.TryGetValue(meshRef.GfxObjId, out var gfx))
|
|
_physicsDataCache.CacheGfxObj(meshRef.GfxObjId, gfx);
|
|
}
|
|
}
|
|
_gfxCacheTicks += Stopwatch.GetTimestamp() - cacheStarted;
|
|
|
|
// The immutable entity list is an exact Near snapshot. Retire every
|
|
// static owner created by the prior snapshot before registering this
|
|
// one; RegisterMultiPart can replace matching IDs but cannot discover
|
|
// owners omitted by a ForceReload/reapply. Logical-owner removal also
|
|
// clears footprints flooded across an adjacent landblock seam.
|
|
_physicsEngine.ShadowObjects.DeregisterStaticOwnersForLandblock(
|
|
landblock.LandblockId);
|
|
|
|
int landblockBspCount = 0;
|
|
int landblockCylinderCount = 0;
|
|
int landblockNoCollisionCount = 0;
|
|
int sceneryTried = 0;
|
|
foreach (WorldEntity entity in landblock.Entities)
|
|
{
|
|
beforeStaticCollision?.Invoke(entity);
|
|
|
|
// Retail building shells collide exclusively through the
|
|
// per-landcell building channel. They never become ordinary
|
|
// shadow objects, even when their source Setup has cylinders.
|
|
if (entity.IsBuildingShell)
|
|
continue;
|
|
|
|
int entityBspCount = 0;
|
|
int entityCylinderCount = 0;
|
|
uint sourcePrefix =
|
|
entity.SourceGfxObjOrSetupId & 0xFF000000u;
|
|
bool isOutdoorMesh =
|
|
(entity.Id & 0x80000000u) != 0
|
|
|| (entity.Id < 0x40000000u
|
|
&& (sourcePrefix == 0x01000000u
|
|
|| sourcePrefix == 0x02000000u));
|
|
if (isOutdoorMesh)
|
|
sceneryTried++;
|
|
|
|
IReadOnlyList<ShadowShape> bspShapes =
|
|
ShadowShapeBuilder.FromLandblockBspParts(
|
|
entity.MeshRefs,
|
|
entity.IsBuildingShell,
|
|
_physicsDataCache.GetGfxObj);
|
|
entityBspCount = bspShapes.Count;
|
|
if (entityBspCount > 0)
|
|
{
|
|
_physicsEngine.ShadowObjects.RegisterMultiPart(
|
|
entity.Id,
|
|
entity.Position,
|
|
entity.Rotation,
|
|
bspShapes,
|
|
0u,
|
|
EntityCollisionFlags.None,
|
|
publication.Origin.X,
|
|
publication.Origin.Y,
|
|
landblock.LandblockId,
|
|
seedCellId: entity.ParentCellId ?? 0u,
|
|
isStatic: true);
|
|
LogMultipartRegistration(landblock, entity, bspShapes);
|
|
}
|
|
|
|
SetupPhysics? setup =
|
|
_physicsDataCache.GetSetup(entity.SourceGfxObjOrSetupId);
|
|
if (setup is not null && entityBspCount == 0)
|
|
{
|
|
float scale = entity.Scale > 0f ? entity.Scale : 1f;
|
|
var setupShapes = new List<ShadowShape>();
|
|
for (int cylinderIndex = 0;
|
|
cylinderIndex < setup.CylSpheres.Count;
|
|
cylinderIndex++)
|
|
{
|
|
DatReaderWriter.Types.CylSphere cylinder =
|
|
setup.CylSpheres[cylinderIndex];
|
|
float radius = cylinder.Radius * scale;
|
|
float baseHeight = cylinder.Height > 0f
|
|
? cylinder.Height
|
|
: cylinder.Radius * 4f;
|
|
float height = baseHeight * scale;
|
|
if (radius <= 0f)
|
|
continue;
|
|
|
|
Vector3 localOffset = cylinder.Origin * scale;
|
|
setupShapes.Add(new ShadowShape(
|
|
GfxObjId: entity.SourceGfxObjOrSetupId,
|
|
LocalPosition: localOffset,
|
|
LocalRotation: Quaternion.Identity,
|
|
Scale: scale,
|
|
CollisionType: ShadowCollisionType.Cylinder,
|
|
Radius: radius,
|
|
CylHeight: height));
|
|
}
|
|
|
|
if (setup.CylSpheres.Count == 0)
|
|
{
|
|
for (int sphereIndex = 0;
|
|
sphereIndex < setup.Spheres.Count;
|
|
sphereIndex++)
|
|
{
|
|
DatReaderWriter.Types.Sphere sphere =
|
|
setup.Spheres[sphereIndex];
|
|
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(
|
|
GfxObjId: entity.SourceGfxObjOrSetupId,
|
|
LocalPosition: localBaseOffset,
|
|
LocalRotation: Quaternion.Identity,
|
|
Scale: scale,
|
|
CollisionType: ShadowCollisionType.Cylinder,
|
|
Radius: radius,
|
|
CylHeight: radius * 2f));
|
|
}
|
|
}
|
|
|
|
if (setup.CylSpheres.Count == 0
|
|
&& setup.Spheres.Count == 0
|
|
&& setup.Radius > 0f)
|
|
{
|
|
float radius = setup.Radius * scale;
|
|
float height = (setup.Height > 0f
|
|
? setup.Height
|
|
: setup.Radius * 2f) * scale;
|
|
setupShapes.Add(new ShadowShape(
|
|
GfxObjId: entity.SourceGfxObjOrSetupId,
|
|
LocalPosition: Vector3.Zero,
|
|
LocalRotation: Quaternion.Identity,
|
|
Scale: scale,
|
|
CollisionType: ShadowCollisionType.Cylinder,
|
|
Radius: radius,
|
|
CylHeight: height));
|
|
}
|
|
|
|
if (setupShapes.Count > 0)
|
|
{
|
|
_physicsEngine.ShadowObjects.RegisterMultiPart(
|
|
entity.Id,
|
|
entity.Position,
|
|
entity.Rotation,
|
|
setupShapes,
|
|
0u,
|
|
EntityCollisionFlags.None,
|
|
publication.Origin.X,
|
|
publication.Origin.Y,
|
|
landblock.LandblockId,
|
|
seedCellId: entity.ParentCellId ?? 0u,
|
|
isStatic: true);
|
|
LogSetupRegistration(landblock, entity, setupShapes);
|
|
entityCylinderCount = setupShapes.Count;
|
|
}
|
|
}
|
|
|
|
if (entityBspCount > 0)
|
|
landblockBspCount++;
|
|
if (entityCylinderCount > 0)
|
|
landblockCylinderCount++;
|
|
if (entityBspCount == 0 && entityCylinderCount == 0
|
|
&& (sourcePrefix == 0x01000000u
|
|
|| sourcePrefix == 0x02000000u))
|
|
{
|
|
landblockNoCollisionCount++;
|
|
}
|
|
}
|
|
|
|
if (PhysicsDiagnostics.ProbeBuildingEnabled && sceneryTried > 0)
|
|
{
|
|
Console.WriteLine(
|
|
$"lb 0x{landblock.LandblockId:X8}: scenery tried={sceneryTried} " +
|
|
$"(outdoorNone={landblockNoCollisionCount})");
|
|
}
|
|
LogMissingSceneryBounds(landblock);
|
|
|
|
_physicsEngine.ShadowObjects.RefloodLandblock(landblock.LandblockId);
|
|
_refloodCount++;
|
|
|
|
_staticBspOwnerCount += landblockBspCount;
|
|
_staticCylinderOwnerCount += landblockCylinderCount;
|
|
publication.CompletionCommitted = true;
|
|
_completeCount++;
|
|
_completePublishTicks +=
|
|
Stopwatch.GetTimestamp() - completedStarted;
|
|
}
|
|
|
|
public void DemoteToTerrain(uint landblockId)
|
|
{
|
|
_physicsEngine.DemoteLandblockToTerrain(landblockId);
|
|
_demotionCount++;
|
|
}
|
|
|
|
public void RemoveLandblock(uint landblockId)
|
|
{
|
|
_physicsEngine.RemoveLandblock(landblockId);
|
|
_fullRemovalCount++;
|
|
}
|
|
|
|
private int PublishBuildings(
|
|
LoadedLandblock landblock,
|
|
PhysicsDatBundle datBundle,
|
|
DatReaderWriter.DBObjs.LandBlockInfo? landblockInfo,
|
|
TerrainSurface terrainSurface,
|
|
Vector3 origin)
|
|
{
|
|
if (landblockInfo is null || landblockInfo.Buildings.Count == 0)
|
|
return 0;
|
|
|
|
uint landblockPrefix = landblock.LandblockId & 0xFFFF0000u;
|
|
foreach (var building in landblockInfo.Buildings)
|
|
{
|
|
var portals = new List<BldPortalInfo>(building.Portals.Count);
|
|
foreach (var portal in building.Portals)
|
|
{
|
|
portals.Add(new BldPortalInfo(
|
|
otherCellId: landblockPrefix | (uint)portal.OtherCellId,
|
|
otherPortalId: unchecked((short)portal.OtherPortalId),
|
|
flags: (ushort)portal.Flags));
|
|
}
|
|
|
|
Vector3 buildingOrigin = building.Frame.Origin + origin;
|
|
Matrix4x4 buildingTransform =
|
|
Matrix4x4.CreateFromQuaternion(building.Frame.Orientation)
|
|
* Matrix4x4.CreateTranslation(buildingOrigin);
|
|
uint landcellLow = terrainSurface.ComputeOutdoorCellId(
|
|
building.Frame.Origin.X,
|
|
building.Frame.Origin.Y);
|
|
uint landcellId = landblockPrefix | landcellLow;
|
|
|
|
uint shellPartZero = building.ModelId;
|
|
if ((shellPartZero & 0xFF000000u) == 0x02000000u)
|
|
{
|
|
datBundle.Setups.TryGetValue(
|
|
building.ModelId,
|
|
out var setup);
|
|
shellPartZero = setup is not null && setup.Parts.Count > 0
|
|
? setup.Parts[0]
|
|
: 0u;
|
|
}
|
|
_physicsDataCache.CacheBuilding(
|
|
landcellId,
|
|
portals,
|
|
buildingTransform,
|
|
modelId: shellPartZero);
|
|
}
|
|
return landblockInfo.Buildings.Count;
|
|
}
|
|
|
|
private static void LogSetupRegistration(
|
|
LoadedLandblock landblock,
|
|
WorldEntity entity,
|
|
IReadOnlyList<ShadowShape> shapes)
|
|
{
|
|
if (!PhysicsDiagnostics.ProbeBuildingEnabled)
|
|
return;
|
|
|
|
for (int index = 0; index < shapes.Count; index++)
|
|
{
|
|
Console.WriteLine(FormattableString.Invariant(
|
|
$"[entity-source] id=0x{entity.Id:X8} entityId=0x{entity.Id:X8} src=0x{entity.SourceGfxObjOrSetupId:X8} gfxObj=0x{shapes[index].GfxObjId:X8} lb=0x{landblock.LandblockId:X8} type=Cylinder note=setup-part{index} state=0x{0u:X8} flags={EntityCollisionFlags.None}"));
|
|
}
|
|
}
|
|
|
|
private static void LogMultipartRegistration(
|
|
LoadedLandblock landblock,
|
|
WorldEntity entity,
|
|
IReadOnlyList<ShadowShape> shapes)
|
|
{
|
|
if (!PhysicsDiagnostics.ProbeBuildingEnabled)
|
|
return;
|
|
|
|
for (int index = 0; index < shapes.Count; index++)
|
|
{
|
|
Console.WriteLine(FormattableString.Invariant(
|
|
$"[entity-source] id=0x{entity.Id:X8} entityId=0x{entity.Id:X8} src=0x{entity.SourceGfxObjOrSetupId:X8} gfxObj=0x{shapes[index].GfxObjId:X8} lb=0x{landblock.LandblockId:X8} type=BSP note=multipart-part{index} hasPhys=true state=0x{0u:X8} flags={EntityCollisionFlags.None}"));
|
|
}
|
|
}
|
|
|
|
private void LogMissingSceneryBounds(LoadedLandblock landblock)
|
|
{
|
|
if (!PhysicsDiagnostics.ProbeBuildingEnabled)
|
|
return;
|
|
|
|
int missingCount = 0;
|
|
var samples = new List<uint>();
|
|
foreach (WorldEntity entity in landblock.Entities)
|
|
{
|
|
if ((entity.Id & 0x80000000u) == 0)
|
|
continue;
|
|
|
|
bool hasBounds = false;
|
|
foreach (MeshRef meshRef in entity.MeshRefs)
|
|
{
|
|
GfxObjVisualBounds? bounds =
|
|
_physicsDataCache.GetVisualBounds(meshRef.GfxObjId);
|
|
if (bounds is not null && bounds.Radius > 0f)
|
|
{
|
|
hasBounds = true;
|
|
break;
|
|
}
|
|
}
|
|
if (hasBounds)
|
|
continue;
|
|
|
|
missingCount++;
|
|
if (samples.Count < 3)
|
|
samples.Add(entity.SourceGfxObjOrSetupId);
|
|
}
|
|
|
|
if (missingCount > 0)
|
|
{
|
|
string sampleText = string.Join(",", samples.Select(
|
|
value => $"0x{value:X8}"));
|
|
Console.WriteLine(
|
|
$" → {missingCount} scenery entities had no visual bounds cached. " +
|
|
$"Samples: {sampleText}");
|
|
}
|
|
}
|
|
|
|
private static bool IsFinite(Vector3 value) =>
|
|
float.IsFinite(value.X)
|
|
&& float.IsFinite(value.Y)
|
|
&& float.IsFinite(value.Z);
|
|
|
|
private void ValidateReceipt(LandblockPhysicsPublication publication)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(publication);
|
|
if (!ReferenceEquals(publication.Owner, _receiptOwner))
|
|
{
|
|
throw new ArgumentException(
|
|
"The physics publication receipt belongs to another publisher.",
|
|
nameof(publication));
|
|
}
|
|
}
|
|
}
|