acdream/tests/AcDream.App.Tests/Rendering/CameraCollisionUpdateViewerTests.cs

118 lines
5.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.Core.Physics;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
using Xunit;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// Render Residual A — the verbatim <c>SmartBox::update_viewer</c>
/// (<c>acclient_2013_pseudo_c.txt:92761</c>) orchestration in
/// <see cref="PhysicsCameraCollisionProbe.SweepEye"/>: seat the sweep's start
/// cell at the head-PIVOT when indoor (via <c>AdjustPosition</c> →
/// <c>find_visible_child_cell</c>), and snap the eye to the player when there is
/// no start cell / the sweep fails.
/// </summary>
public class CameraCollisionUpdateViewerTests
{
private const uint FeetCellId = 0xA9B40174u; // cellar (low ≥ 0x0100 → indoor), interior Z ≤ 94
private const uint RoomCellId = 0xA9B40171u; // cottage floor above, interior Z ≥ 94, in feet cell's stab list
private const uint LandblockId = 0xA9B40000u;
/// <summary>
/// The cellar-lip case (user point 3): the player's FEET are in the low cellar
/// cell, but the head-PIVOT is up at cottage-floor level in a different cell.
/// Retail seats the sweep at the pivot's cell (<c>AdjustPosition</c>, pc:92832),
/// so the viewer cell is the room above — NOT the feet cell. Without the start-cell
/// port the sweep stays in the feet cell.
/// </summary>
[Fact]
public void SweepEye_IndoorPivotInCellAboveFeet_SeatsStartAtPivotCell()
{
var engine = BuildTwoCellEngine();
var probe = new PhysicsCameraCollisionProbe(engine);
var feet = new Vector3(0f, 0f, 93f); // in the feet cell (Z ≤ 94)
var pivot = new Vector3(0f, 0f, 94.5f); // head, up in the room cell (Z ≥ 94)
var eye = new Vector3(0f, 3f, 95.5f); // behind + up, still in the room region, no wall
var result = probe.SweepEye(pivot, eye, cellId: FeetCellId, selfEntityId: 0u, playerPos: feet);
Assert.Equal(RoomCellId, result.ViewerCellId);
}
/// <summary>
/// Retail <c>update_viewer</c> snaps the viewer to the player position when the
/// player has no cell (pc:92775) and as fallback 2 when the sweep fails
/// (pc:92886): <c>set_viewer(player_pos); viewer_cell = null</c>.
/// </summary>
[Fact]
public void SweepEye_NoStartCell_SnapsToPlayer()
{
var probe = new PhysicsCameraCollisionProbe(new PhysicsEngine());
var player = new Vector3(7f, 8f, 9f);
var result = probe.SweepEye(
pivot: new Vector3(7f, 8f, 10.5f), desiredEye: new Vector3(7f, 13f, 11f),
cellId: 0u, selfEntityId: 0u, playerPos: player);
Assert.Equal(player, result.Eye);
Assert.Equal(0u, result.ViewerCellId);
}
// ── fixture ────────────────────────────────────────────────────────────
private static PhysicsEngine BuildTwoCellEngine()
{
var cache = new PhysicsDataCache();
var engine = new PhysicsEngine { DataCache = cache };
// Feet cell: interior Z ≤ 94, in its stab list the room cell above.
// The inert sentinel portal keeps the synthetic cell eligible for
// retail CEnvCell::point_in_cell without creating a usable transit.
cache.RegisterCellStructForTest(FeetCellId, MakeCell(InteriorZAtMost(94f), new uint[] { RoomCellId }));
// Room cell: interior Z ≥ 94, no walls and no usable portals.
cache.RegisterCellStructForTest(RoomCellId, MakeCell(InteriorZAtLeast(94f), Array.Empty<uint>()));
var heights = new byte[81];
var heightTable = new float[256];
for (int i = 0; i < 256; i++) heightTable[i] = -1000f;
engine.AddLandblock(
landblockId: LandblockId,
terrain: new TerrainSurface(heights, heightTable),
cells: Array.Empty<CellSurface>(),
portals: Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
return engine;
}
private static CellBSPNode InteriorZAtMost(float boundary) => new()
{
SplittingPlane = new Plane(new Vector3(0f, 0f, -1f), boundary), // dist = boundary Z ≥ 0 ⇔ Z ≤ boundary
PosNode = new CellBSPNode { Type = BSPNodeType.Leaf },
};
private static CellBSPNode InteriorZAtLeast(float boundary) => new()
{
SplittingPlane = new Plane(new Vector3(0f, 0f, 1f), -boundary), // dist = Z boundary ≥ 0 ⇔ Z ≥ boundary
PosNode = new CellBSPNode { Type = BSPNodeType.Leaf },
};
private static CellPhysics MakeCell(CellBSPNode cellBspRoot, uint[] visibleCellIds) => new()
{
BSP = new PhysicsBSPTree { Root = new PhysicsBSPNode { Type = BSPNodeType.Leaf } },
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
CellBSP = new CellBSPTree { Root = cellBspRoot },
Portals = [new PortalInfo(0xFFFF, 0, 0)],
PortalPolygons = new Dictionary<ushort, ResolvedPolygon>(),
VisibleCellIds = new HashSet<uint>(visibleCellIds),
};
}