fix(physics): S4/AD-65 — the away-from-plane response snaps to the surface, as retail does
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Campaign S slice S4, the half that landed. Retail's CTransition:: adjust_offset @0x0050a370 branches on dot(offset, contactPlane.N) at 0x0050a4fa: moving INTO the plane subtracts the normal component (0x0050a529), moving AWAY calls Plane::snap_to_plane @0x00509c50 — which preserves X and Y and re-solves ONLY Z so the offset lies in the plane (the d terms cancel algebraically), no-op under the 0.000199999995f |N.z| epsilon. acdream ran the orthogonal projection in BOTH directions, shrinking downhill XY travel by cos^2(theta): 25% at 30 degrees, 50% at 45 — AD-65's recorded shortfall, now retired. The combined Opus review independently re-derived the algebra, the branch polarity, the epsilon's bit-identity (17b75139), and the sabotage magnitude (the re-instated projection yields X = 0.75 = cos^2 30 exactly), and verified the delta is 4 non-comment lines with the into-plane arm, the crease arm, and both no-plane arms untouched. Its blast-radius sweep found the away arm exercised but NOT discriminated by any pre-existing test — every one asserts lower bounds the snap over-satisfies — so the two new exact-value tests are the only discriminating coverage, recorded in the test's class doc, and the felt 33-100% downhill speed-up is the morning gate's one row. AD-66 (the push-out's bare radius) is WITHHELD: byte-confirmed twice, implemented, then pulled after the same clean-room binaries measured contradictory absorbed-tick outcomes flipping with nothing but test assert shape — issue #341 carries the observation matrix and the apparatus plan; its two exact-value tests are [Skip]-ed; the retained substitution's rationale is restored at the site per review F1, with the review's remaining findings (F2/F3/F4/F5/F6) applied and F8 filed as #342. AD-69 filed: the same block omits retail's get_block_offset seam-frame correction, deferred to the AD-66 relanding for attributability. #340 filed: a fifth load-sensitive flake. Review verdict: PASS. AD-65 is provably unable to reach the #341 anomaly's code path (the absorb scenario takes the crease arm). Clean-room suite: 11,239 passed / 6 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4721838916
commit
d73125d3b0
8 changed files with 859 additions and 40 deletions
|
|
@ -0,0 +1,374 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Plane = System.Numerics.Plane;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign S slice S4 (2026-08-07) conformance suite for
|
||||
/// <c>Transition.AdjustOffset</c>'s two register-row substitutions:
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item>AD-65 — the away-from-plane arm (<c>collisionAngle > 0</c>) must
|
||||
/// SNAP to the contact plane (retail's <c>Plane::snap_to_plane</c>
|
||||
/// @0x00509c50: X/Y preserved, Z re-solved, epsilon no-op), not run the
|
||||
/// orthogonal-projection subtraction used by the into-plane arm.</item>
|
||||
/// <item>AD-66 — the safety push-out's trigger comparison and <c>zDist</c>
|
||||
/// numerator must use the BARE <c>global_sphere->radius</c>, not
|
||||
/// <c>radius * ContactPlane.Normal.Z</c>.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// See <c>docs/research/2026-08-07-s4-adjustoffset-contract.md</c> (the
|
||||
/// pinned contract) and <c>docs/research/2026-08-07-s4-pseudocode.md</c>
|
||||
/// (the full branch-tree derivation, including the resolved Binary Ninja
|
||||
/// flag-idiom ambiguity for the two epsilon-relative comparisons this suite
|
||||
/// exercises).
|
||||
/// </summary>
|
||||
public class S4AdjustOffsetConformanceTests
|
||||
{
|
||||
private readonly ITestOutputHelper _out;
|
||||
public S4AdjustOffsetConformanceTests(ITestOutputHelper output) => _out = output;
|
||||
|
||||
private const float Tolerance = 1e-5f;
|
||||
|
||||
// =========================================================================
|
||||
// AD-65 — away-from-plane arm must SNAP (XY preserved, Z re-solved).
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void AdjustOffset_AwayFromPlane_SnapsPreservingXYAndResolvingZ()
|
||||
{
|
||||
// 30 degree contact plane: N = (sin30, 0, cos30) = (0.5, 0, 0.8660254).
|
||||
// Exact unit vector (30-60-90 triangle: 0.5^2 + 0.8660254^2 == 1).
|
||||
var normal = new Vector3(0.5f, 0f, 0.8660254f);
|
||||
var t = new Transition();
|
||||
t.CollisionInfo.SetContactPlane(new Plane(normal, 0f), cellId: 0, isWater: false);
|
||||
|
||||
// Moving +X only: dot(offset, N) = 0.5 > 0 -> AWAY from the plane -> snap.
|
||||
var offset = new Vector3(1f, 0f, 0f);
|
||||
Vector3 result = t.AdjustOffset(offset);
|
||||
|
||||
// snap_to_plane preserves X and Y exactly and re-solves Z:
|
||||
// z = -(x*N.x + y*N.y) / N.z = -(1*0.5 + 0*0) / 0.8660254 = -0.5773502691896258
|
||||
Assert.Equal(1f, result.X, Tolerance);
|
||||
Assert.Equal(0f, result.Y, Tolerance);
|
||||
Assert.Equal(-0.5773502691896258f, result.Z, Tolerance);
|
||||
|
||||
_out.WriteLine($"snap result = ({result.X:F7}, {result.Y:F7}, {result.Z:F7})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustOffset_AwayFromPlane_NearVerticalPlane_IsNoOp()
|
||||
{
|
||||
// |N.z| = 0.0001 <= PhysicsGlobals.EPSILON (0.0002) -> snap_to_plane's
|
||||
// divide-guard trips -> the ENTIRE offset (X, Y, and Z) is left
|
||||
// unchanged, not just Z.
|
||||
var normal = new Vector3(1f, 0f, 0.0001f);
|
||||
var t = new Transition();
|
||||
t.CollisionInfo.SetContactPlane(new Plane(normal, 0f), cellId: 0, isWater: false);
|
||||
|
||||
// dot(offset, N) = 1*1 + 0 + 0*0.0001 = 1 > 0 -> away-from-plane arm
|
||||
// entered, but the epsilon guard inside must no-op.
|
||||
var offset = new Vector3(1f, 0f, 0f);
|
||||
Vector3 result = t.AdjustOffset(offset);
|
||||
|
||||
Assert.Equal(offset, result);
|
||||
_out.WriteLine($"epsilon no-op result = ({result.X:F7}, {result.Y:F7}, {result.Z:F7})");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Into-plane arm — verify unchanged (not part of AD-65's fix, but the
|
||||
// contract requires this exact-value case be covered too).
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void AdjustOffset_IntoPlane_SubtractsFullNormalComponent()
|
||||
{
|
||||
var normal = new Vector3(0.5f, 0f, 0.8660254f);
|
||||
var t = new Transition();
|
||||
t.CollisionInfo.SetContactPlane(new Plane(normal, 0f), cellId: 0, isWater: false);
|
||||
|
||||
// Moving -X: dot(offset, N) = -0.5 <= 0 -> INTO the plane -> subtract.
|
||||
var offset = new Vector3(-1f, 0f, 0f);
|
||||
Vector3 result = t.AdjustOffset(offset);
|
||||
|
||||
// result = offset - N * collisionAngle = (-1,0,0) - (0.5,0,0.8660254)*(-0.5)
|
||||
// = (-0.75, 0, 0.4330127)
|
||||
Assert.Equal(-0.75f, result.X, Tolerance);
|
||||
Assert.Equal(0f, result.Y, Tolerance);
|
||||
Assert.Equal(0.4330127f, result.Z, Tolerance);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AD-66 — safety push-out must use the BARE radius (not radius*N.z) for
|
||||
// both the trigger comparison and the zDist numerator.
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a sphere position whose signed plane distance sits strictly
|
||||
/// BETWEEN the old (buggy) <c>radius*N.z</c> threshold and the new
|
||||
/// (retail-faithful) bare-<c>radius</c> threshold. This single fixture
|
||||
/// discriminates BOTH AD-66 sub-fixes at once:
|
||||
/// <list type="bullet">
|
||||
/// <item>the OLD trigger (<c>dist < radius*N.z - EPSILON</c>) would NOT
|
||||
/// have fired here at all (0.47 is not less than 0.4328);</item>
|
||||
/// <item>the NEW trigger (<c>dist < radius - EPSILON</c>) DOES fire
|
||||
/// (0.47 < 0.4998), and the pushed amount must equal the bare-radius
|
||||
/// zDist formula, not the radius*N.z one.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[Fact(Skip = "AD-66 WITHHELD 2026-08-07: retail's bare radius is byte-confirmed but the landing was pulled after the same clean-room binaries measured contradictory absorbed-tick behaviour flipping with test-assert shape alone. Unskip with the AD-66 relanding. See the S4 outcome note in the contract doc.")]
|
||||
public void AdjustOffset_SafetyPush_UsesBareRadiusForTriggerAndNumerator()
|
||||
{
|
||||
const float radius = 0.5f;
|
||||
var normal = new Vector3(0.5f, 0f, 0.8660254f); // 30 degrees, unit.
|
||||
const float dist = 0.47f; // strictly between radius*N.z (0.4330127) and radius (0.5)
|
||||
|
||||
// Sanity: confirm the fixture actually sits in the discriminating gap
|
||||
// before trusting the test's own claim about it.
|
||||
float naturalRestingDistOld = radius * normal.Z;
|
||||
Assert.True(dist > naturalRestingDistOld - PhysicsGlobals.EPSILON,
|
||||
"fixture must NOT trip the old radius*N.z trigger");
|
||||
Assert.True(dist < radius - PhysicsGlobals.EPSILON,
|
||||
"fixture MUST trip the new bare-radius trigger");
|
||||
|
||||
var t = new Transition();
|
||||
t.CollisionInfo.SetContactPlane(new Plane(normal, 0f), cellId: 0xA9B40001u, isWater: false);
|
||||
|
||||
// globCenter chosen purely along N (Y=0) so dot(globCenter,N)+D == dist
|
||||
// exactly: globCenter.z = dist / N.z (D = 0).
|
||||
float centerZ = dist / normal.Z;
|
||||
t.SpherePath.GlobalSphere[0].Origin = new Vector3(0f, 0f, centerZ);
|
||||
t.SpherePath.GlobalSphere[0].Radius = radius;
|
||||
|
||||
float checkPosZBefore = t.SpherePath.CheckPos.Z;
|
||||
float globSphereZBefore = t.SpherePath.GlobalSphere[0].Origin.Z;
|
||||
|
||||
// Zero movement request: collisionAngle == 0 <= 0 takes the (no-op at
|
||||
// zero) into-plane arm, isolating the safety-push block under test.
|
||||
t.AdjustOffset(Vector3.Zero);
|
||||
|
||||
float expectedZDist = (radius - dist) / normal.Z; // bare-radius numerator
|
||||
float actualPush = t.SpherePath.CheckPos.Z - checkPosZBefore;
|
||||
|
||||
Assert.True(actualPush > 0f,
|
||||
"the bare-radius trigger must fire and push the sphere up; " +
|
||||
"the old radius*N.z trigger would NOT have fired for this fixture " +
|
||||
$"(dist={dist}, old threshold={naturalRestingDistOld - PhysicsGlobals.EPSILON:F7}).");
|
||||
Assert.Equal(expectedZDist, actualPush, Tolerance);
|
||||
// AddOffsetToCheckPos mirrors the same push onto every active
|
||||
// GlobalSphere entry (NumSphere == 1 here), from ITS OWN baseline —
|
||||
// not CheckPos's baseline, which started at a different Z.
|
||||
Assert.Equal(globSphereZBefore + expectedZDist, t.SpherePath.GlobalSphere[0].Origin.Z, Tolerance);
|
||||
|
||||
_out.WriteLine($"push = {actualPush:F7} (expected bare-radius zDist = {expectedZDist:F7}); " +
|
||||
$"old naturalRestingDist formula would have given " +
|
||||
$"{(naturalRestingDistOld - dist) / normal.Z:F7} AND would not have fired at all.");
|
||||
}
|
||||
|
||||
[Fact(Skip = "AD-66 WITHHELD 2026-08-07: retail's bare radius is byte-confirmed but the landing was pulled after the same clean-room binaries measured contradictory absorbed-tick behaviour flipping with test-assert shape alone. Unskip with the AD-66 relanding. See the S4 outcome note in the contract doc.")]
|
||||
public void AdjustOffset_SafetyPush_DoesNotFire_WhenAboveBareRadiusThreshold()
|
||||
{
|
||||
const float radius = 0.5f;
|
||||
var normal = new Vector3(0.5f, 0f, 0.8660254f);
|
||||
const float dist = 0.6f; // > radius (0.5) -> comfortably above threshold either way.
|
||||
|
||||
var t = new Transition();
|
||||
t.CollisionInfo.SetContactPlane(new Plane(normal, 0f), cellId: 0xA9B40001u, isWater: false);
|
||||
|
||||
float centerZ = dist / normal.Z;
|
||||
t.SpherePath.GlobalSphere[0].Origin = new Vector3(0f, 0f, centerZ);
|
||||
t.SpherePath.GlobalSphere[0].Radius = radius;
|
||||
|
||||
float checkPosZBefore = t.SpherePath.CheckPos.Z;
|
||||
t.AdjustOffset(Vector3.Zero);
|
||||
|
||||
Assert.Equal(checkPosZBefore, t.SpherePath.CheckPos.Z, Tolerance);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Uphill no-flap guard. Written as the S4 contract's AD-66 STOP-condition
|
||||
// scenario; AD-66 was then WITHHELD (issue #341), so this currently runs
|
||||
// against the RETAINED radius*N.z substitution and its green is evidence
|
||||
// about THAT code. It must stay green across the AD-66 relanding too —
|
||||
// it is the scenario the substitution was originally written to protect.
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 42 degree contact plane (N.z = cos(42deg) = 0.74314 >
|
||||
/// PhysicsGlobals.FloorZ = 0.6642, so it IS walkable, with a margin of
|
||||
/// about 0.079 — "steep but walkable", matching the register row's own
|
||||
/// framing). A single large sloped BSP polygon rising toward +X stands in
|
||||
/// for outdoor terrain (same mechanism: FindEnvCollisions ->
|
||||
/// AdjustOffset -> ValidateWalkable per tick); the mover requests a
|
||||
/// PURELY HORIZONTAL forward step every tick (a purely
|
||||
/// horizontal request against a seeded contact plane — the ordinary
|
||||
/// grounded-movement shape; note PhysicsBody.cs:350-355 documents
|
||||
/// horizontal PROCEEDING as the failure symptom when the contact-plane
|
||||
/// seed is missing, which is precisely what the per-tick assertions
|
||||
/// below rule out) and relies on collision detection against the
|
||||
/// rising polygon plus AdjustOffset's projection/safety-push to keep the
|
||||
/// sphere glued to the surface, exactly the retail per-tick mechanism.
|
||||
/// <para><b>Sabotage record (SAB-S4-1, 2026-08-07, verified twice —
|
||||
/// implementer and reviewer independently):</b> re-instating
|
||||
/// <c>result -= N * collisionAngle</c> in the away arm reds
|
||||
/// <c>AdjustOffset_AwayFromPlane_SnapsPreservingXYAndResolvingZ</c> with
|
||||
/// X = 0.75 — exactly the cos²30° shrinkage AD-65's register row recorded —
|
||||
/// and reds the near-vertical no-op test with (0, 0, -1e-4).</para>
|
||||
///
|
||||
/// <para><b>What the rest of the suite does NOT discriminate:</b> every
|
||||
/// pre-existing test that reaches the away arm
|
||||
/// (RuntimeRemoteSlopeProjectionTests, RuntimeRemoteSteepContactSlideTests)
|
||||
/// asserts lower bounds or XY-invariant offsets that the snap over-satisfies
|
||||
/// — RuntimeRemoteSlopeProjectionTests records its own AdjustOffset
|
||||
/// short-circuit sabotage staying GREEN. The two exact-value tests here are
|
||||
/// the ONLY discriminating coverage for AD-65; the felt 33–100% downhill
|
||||
/// speed-up is the morning gate's G1 row.</para>
|
||||
///
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Uphill_NoContactFlapAcrossTicks()
|
||||
{
|
||||
const float radius = 0.5f;
|
||||
const float angleDegrees = 42f;
|
||||
const uint cellId = 0xA9B40157u;
|
||||
|
||||
float theta = angleDegrees * MathF.PI / 180f;
|
||||
float sinT = MathF.Sin(theta);
|
||||
float cosT = MathF.Cos(theta);
|
||||
Assert.True(cosT > PhysicsGlobals.FloorZ,
|
||||
"fixture sanity: the slope must be walkable by retail's own FloorZ test");
|
||||
|
||||
var (engine, root) = BuildSlopeEngine(sinT, cosT, cellId);
|
||||
|
||||
// Resting root Z at horizontal x0, derived from: sphere center =
|
||||
// root + (0,0,radius); dot(center, N) + D == radius (D == 0);
|
||||
// N = (-sinT, 0, cosT).
|
||||
float x0 = 1.0f;
|
||||
float RestingRootZ(float x) => (radius * (1f - cosT) + sinT * x) / cosT;
|
||||
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
ContactPlaneValid = true,
|
||||
ContactPlane = new Plane(new Vector3(-sinT, 0f, cosT), 0f),
|
||||
ContactPlaneCellId = cellId,
|
||||
ContactPlaneIsWater = false,
|
||||
TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable,
|
||||
};
|
||||
|
||||
Vector3 position = new(x0, 0f, RestingRootZ(x0));
|
||||
const float dxPerTick = 0.12f;
|
||||
const int ticks = 15;
|
||||
|
||||
for (int tick = 0; tick < ticks; tick++)
|
||||
{
|
||||
Vector3 target = position + new Vector3(dxPerTick, 0f, 0f);
|
||||
|
||||
ResolveResult result = engine.ResolveWithTransition(
|
||||
currentPos: position,
|
||||
targetPos: target,
|
||||
cellId: cellId,
|
||||
sphereRadius: radius,
|
||||
sphereHeight: 0f,
|
||||
stepUpHeight: 0.4f,
|
||||
stepDownHeight: 0.4f,
|
||||
isOnGround: true,
|
||||
body: body);
|
||||
|
||||
_out.WriteLine(
|
||||
$"tick {tick}: ok={result.Ok} pos=({result.Position.X:F4},{result.Position.Y:F4}," +
|
||||
$"{result.Position.Z:F4}) inContact={result.InContact} onWalkable={result.OnWalkable} " +
|
||||
$"planeN=({result.ContactPlane.Normal.X:F4},{result.ContactPlane.Normal.Y:F4}," +
|
||||
$"{result.ContactPlane.Normal.Z:F4})");
|
||||
|
||||
Assert.True(result.Ok, $"tick {tick}: transition must not get stuck running uphill");
|
||||
Assert.True(result.InContact,
|
||||
$"tick {tick}: contact must not be lost running uphill (the AD-66 flap symptom)");
|
||||
Assert.True(result.OnWalkable,
|
||||
$"tick {tick}: OnWalkable must not flap to false running uphill on a walkable " +
|
||||
"slope. Written as the S4/AD-66 STOP condition; with AD-66 withheld (#341) it " +
|
||||
"guards the RETAINED substitution and must survive the relanding.");
|
||||
|
||||
position = result.Position;
|
||||
}
|
||||
|
||||
// retail's unchanged, correct into-plane arm (AD-65's register row:
|
||||
// "Uphill (collisionAngle <= 0) is correct and identical to retail")
|
||||
// projects a purely-horizontal request of dx against a theta-degree
|
||||
// incline down to dx*cos^2(theta) of effective horizontal advance —
|
||||
// removing the into-plane component always costs a cos^2(theta)
|
||||
// factor. That is expected slope-climbing physics, not a stall, so
|
||||
// the progress floor below is calibrated to it rather than a naive
|
||||
// dx-per-tick expectation (which the earlier, wrong version of this
|
||||
// test asserted and which redded even though nothing was stuck).
|
||||
float expectedMinimumAdvance = dxPerTick * cosT * cosT * (ticks - 3);
|
||||
Assert.True(position.X - x0 > expectedMinimumAdvance,
|
||||
$"expected at least {expectedMinimumAdvance:F4} m of horizontal advance " +
|
||||
"(dx*cos^2(theta) per tick, retail's unchanged into-plane projection); " +
|
||||
$"got {position.X - x0:F4} m -- a shortfall here would mean the mover " +
|
||||
"stalled, not merely slowed by the expected slope projection.");
|
||||
}
|
||||
|
||||
private static (PhysicsEngine Engine, PhysicsBSPNode Root) BuildSlopeEngine(
|
||||
float sinT, float cosT, uint cellId)
|
||||
{
|
||||
// Large sloped quad, plane through the origin: N = (-sinT, 0, cosT),
|
||||
// D = 0 -> z(x) = x * tan(theta). Spans far enough in X/Y to hold the
|
||||
// whole multi-tick uphill run away from any polygon edge.
|
||||
float ZAt(float x) => x * sinT / cosT;
|
||||
Vector3[] vertices =
|
||||
[
|
||||
new(-10f, -30f, ZAt(-10f)),
|
||||
new(60f, -30f, ZAt(60f)),
|
||||
new(60f, 30f, ZAt(60f)),
|
||||
new(-10f, 30f, ZAt(-10f)),
|
||||
];
|
||||
var plane = new Plane(new Vector3(-sinT, 0f, cosT), 0f);
|
||||
|
||||
var root = new PhysicsBSPNode
|
||||
{
|
||||
Type = BSPNodeType.Leaf,
|
||||
BoundingSphere = new Sphere { Origin = new Vector3(25f, 0f, ZAt(25f)), Radius = 100f },
|
||||
};
|
||||
root.Polygons.Add(1);
|
||||
|
||||
var resolved = new Dictionary<ushort, ResolvedPolygon>
|
||||
{
|
||||
[1] = new ResolvedPolygon
|
||||
{
|
||||
Id = 1,
|
||||
Vertices = vertices,
|
||||
Plane = plane,
|
||||
NumPoints = vertices.Length,
|
||||
SidesType = CullMode.None,
|
||||
},
|
||||
};
|
||||
|
||||
var cell = new CellPhysics
|
||||
{
|
||||
BSP = new PhysicsBSPTree { Root = root },
|
||||
WorldTransform = Matrix4x4.Identity,
|
||||
InverseWorldTransform = Matrix4x4.Identity,
|
||||
Resolved = resolved,
|
||||
CellBSP = new CellBSPTree { Root = new CellBSPNode { Type = BSPNodeType.Leaf } },
|
||||
};
|
||||
|
||||
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
|
||||
|
||||
var heights = new byte[81];
|
||||
var heightTable = new float[256];
|
||||
for (int i = 0; i < 256; i++) heightTable[i] = i * 1f;
|
||||
engine.AddLandblock(0xA9B4FFFFu, new TerrainSurface(heights, heightTable),
|
||||
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(),
|
||||
worldOffsetX: 0f, worldOffsetY: 0f);
|
||||
|
||||
engine.DataCache.RegisterCellStructForTest(cellId, cell);
|
||||
return (engine, root);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue