diff --git a/tests/AcDream.Runtime.Tests/Physics/RemoteRampHarness.cs b/tests/AcDream.Runtime.Tests/Physics/RemoteRampHarness.cs new file mode 100644 index 00000000..e7f42443 --- /dev/null +++ b/tests/AcDream.Runtime.Tests/Physics/RemoteRampHarness.cs @@ -0,0 +1,272 @@ +using System.Numerics; +using AcDream.Core.Net; +using AcDream.Core.Net.Messages; +using AcDream.Core.Physics; +using AcDream.Core.Physics.Motion; +using AcDream.Runtime.Entities; +using AcDream.Runtime.Physics; + +namespace AcDream.Runtime.Tests.Physics; + +/// +/// Drives the production tick over a +/// synthetic single-landblock world whose terrain is a constant-gradient ramp, +/// so every contact plane the sweep reports is a real geometric result rather +/// than a stubbed value. +/// +/// Extracted 2026-08-06 from RuntimeRemoteSteepContactSlideTests +/// (where it was a private nested class) so the AD-10 slope-projection tests +/// can build on the same fixture instead of cloning it. Behaviour is +/// unchanged; the only additions are , +/// , and the root-motion-driving +/// overload. +/// +internal sealed class RemoteRampHarness : IDisposable +{ + private const uint LandblockId = 0x0101FFFFu; + + /// Local XY the body is placed at, near the landblock centre. + internal const float StartX = 96f; + + /// Local XY the body is placed at, near the landblock centre. + internal const float StartY = 96f; + + private readonly RuntimeEntityObjectLifetime _lifetime; + private readonly RuntimeEntityRecord _record; + private readonly RuntimeRemotePhysicsUpdater _updater; + + internal RemoteMotion Remote { get; } + + /// + /// The exact terrain this fixture published into the engine. Tests read + /// the ground's own geometry from here, so an assertion about "is the body + /// on the surface" is answered by the surface rather than by + /// re-implementing whatever the code under test computed. + /// + internal TerrainSurface Surface { get; } + + internal PhysicsEngine Engine => _lifetime.Physics.Engine; + + private RemoteRampHarness( + RuntimeEntityObjectLifetime lifetime, + RuntimeEntityRecord record, + RemoteMotion remote, + RuntimeRemotePhysicsUpdater updater, + TerrainSurface surface) + { + _lifetime = lifetime; + _record = record; + Remote = remote; + _updater = updater; + Surface = surface; + } + + /// Terrain height at a world-space XY (the landblock sits at 0,0). + internal float SurfaceZ(float worldX, float worldY) + => Surface.SampleZ(worldX, worldY); + + /// Terrain height directly under the body's current XY. + internal float SurfaceZUnderBody() + => SurfaceZ(Remote.Body.Position.X, Remote.Body.Position.Y); + + /// Body already resting on the ramp, contact established. + internal static RemoteRampHarness OnRamp(float gradient) + { + RemoteRampHarness harness = Create(gradient, heightAboveSurface: 0f); + // Retail gains spawn contact from the first gravity frame; the + // stationary-remote settle (SpawnPlacementSettler, #270) compresses + // it. Use the production seam so the fixture starts from exactly + // the state a live spawn would. + SpawnPlacementSettler.TrySettle( + harness._lifetime.Physics.Engine, + harness.Remote.Body, + harness.Remote.Body.Position, + harness.Remote.CellId, + sphereRadius: 0.48f, + sphereHeight: 1.835f, + ObjectInfoState.EdgeSlide, + harness._record.LocalEntityId!.Value, + harness.Remote.Movement.HitGround, + harness.Remote.Motion.LeaveGround); + harness.Remote.Airborne = !harness.Remote.Body.OnWalkable; + return harness; + } + + /// Body suspended above the ramp with no contact at all. + internal static RemoteRampHarness Airborne(float gradient, float height) + { + RemoteRampHarness harness = Create(gradient, heightAboveSurface: height); + harness.Remote.Body.TransientState &= ~(TransientStateFlags.Contact + | TransientStateFlags.OnWalkable); + harness.Remote.Body.ContactPlaneValid = false; + harness.Remote.Airborne = true; + return harness; + } + + private static RemoteRampHarness Create(float gradient, float heightAboveSurface) + { + var lifetime = new RuntimeEntityObjectLifetime(); + TerrainSurface surface = Ramp(gradient); + lifetime.Physics.Engine.AddLandblock( + LandblockId, + surface, + Array.Empty(), + Array.Empty(), + worldOffsetX: 0f, + worldOffsetY: 0f); + + RuntimeEntityRecord record = lifetime.Entities.AddActive(Spawn()); + var body = new PhysicsBody + { + // Retail CPhysicsObj constructor state 0x400C08 @0x00512508 + // (EdgeSlide | Lighting | Gravity | ReportCollisions), which + // ACE also sends for every creature (PhysicsGlobals.DefaultState). + State = PhysicsStateFlags.Gravity + | PhysicsStateFlags.ReportCollisions + | PhysicsStateFlags.EdgeSlide, + InWorld = true, + }; + var remote = new RemoteMotion(body); + lifetime.Entities.SetPhysicsBody(record, body); + lifetime.Entities.SetRemoteMotion(record, remote); + lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true); + + float surfaceZ = surface.SampleZ(StartX, StartY); + body.Position = new Vector3( + StartX, + StartY, + surfaceZ + heightAboveSurface); + body.Orientation = Quaternion.Identity; + remote.CellId = TerrainSurface.ComputeOutdoorCellId( + LandblockId, + StartX, + StartY); + remote.LastServerPos = body.Position; + remote.LastServerPosTime = 1.0; + + return new RemoteRampHarness( + lifetime, + record, + remote, + new RuntimeRemotePhysicsUpdater(lifetime.Physics), + surface); + } + + /// Tick with an empty animation root-motion frame. + internal void Tick(int count, float dt = 1f / 30f) + => Tick(count, Vector3.Zero, dt); + + /// + /// Tick while CSequence::update reports the given body-local root + /// displacement every frame — the locomotion-cycle push that drives a + /// running remote between server position updates. + /// + internal void Tick(int count, Vector3 rootMotionLocalPerTick, float dt = 1f / 30f) + { + var frame = new MotionDeltaFrame(); + for (int i = 0; i < count; i++) + { + frame.Reset(); + frame.Origin = rootMotionLocalPerTick; + _updater.Tick( + _record, + Remote, + objectScale: 1f, + sequencer: null, + dt, + _record.ObjectClockEpoch, + frame, + radius: 0.48f, + height: 1.835f, + liveCenterX: 1, + liveCenterY: 1); + } + } + + /// + /// A constant-gradient ramp. The heightmap byte at (x, y) indexes a table + /// whose entries rise linearly, so every cell of the landblock has the same + /// plane normal and the sampled contact plane is a single constant plane. + /// + private static TerrainSurface Ramp(float gradient) + { + var heightTable = new float[256]; + for (int i = 0; i < heightTable.Length; i++) + heightTable[i] = i * gradient * TerrainSurface.CellSize; + + var heights = new byte[81]; + for (int x = 0; x < 9; x++) + for (int y = 0; y < 9; y++) + heights[x * 9 + y] = (byte)(8 - y); + + return new TerrainSurface(heights, heightTable); + } + + private static WorldSession.EntitySpawn Spawn() + { + var position = new CreateObject.ServerPosition( + LandblockId, + StartX, + StartY, + 0f, + 1f, + 0f, + 0f, + 0f); + var timestamps = new PhysicsTimestamps( + Position: 1, + Movement: 1, + State: 1, + Vector: 1, + Teleport: 0, + ServerControlledMove: 1, + ForcePosition: 0, + ObjDesc: 1, + Instance: 1); + const uint rawState = (uint)(PhysicsStateFlags.Gravity + | PhysicsStateFlags.ReportCollisions + | PhysicsStateFlags.EdgeSlide); + var physics = new PhysicsSpawnData( + RawState: rawState, + Position: position, + Movement: null, + AnimationFrame: null, + SetupTableId: 0x02000001u, + MotionTableId: 0x09000001u, + SoundTableId: null, + PhysicsScriptTableId: null, + Parent: null, + Children: null, + Scale: null, + Friction: null, + Elasticity: null, + Translucency: null, + Velocity: null, + Acceleration: null, + AngularVelocity: null, + DefaultScriptType: null, + DefaultScriptIntensity: null, + Timestamps: timestamps); + return new WorldSession.EntitySpawn( + 0x70000101u, + position, + 0x02000001u, + Array.Empty(), + Array.Empty(), + Array.Empty(), + null, + null, + "remote-ramp-fixture", + null, + null, + 0x09000001u, + PhysicsState: rawState, + InstanceSequence: 1, + MovementSequence: 1, + ServerControlSequence: 1, + PositionSequence: 1, + Physics: physics); + } + + public void Dispose() => _lifetime.Dispose(); +} diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSlopeProjectionTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSlopeProjectionTests.cs new file mode 100644 index 00000000..da7e1e5d --- /dev/null +++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSlopeProjectionTests.cs @@ -0,0 +1,160 @@ +using System.Numerics; +using AcDream.Core.Physics; + +namespace AcDream.Runtime.Tests.Physics; + +/// +/// AD-10 — the remote slope projection. Retail projects the per-sub-step +/// movement offset onto collision_info.contact_plane INSIDE the sweep +/// (CTransition::adjust_offset 0x0050a370, pc:272271-272393, +/// called once per step from find_transitional_position +/// 0x0050bdf0). acdream ports that faithfully in +/// Transition.AdjustOffset, and remotes run that sweep — so the extra +/// pre-sweep projection the remote tick used to apply at the combiner boundary +/// was a second copy of the same operation against a single-point terrain +/// sample. It was deleted 2026-08-06 after the measurement these tests carry. +/// +/// Every assertion here runs the production +/// tick and +/// takes its expected Z from the fixture's own terrain geometry, never from a +/// re-implementation of the projection formula — so a projection against the +/// WRONG plane produces a wrong answer rather than a self-consistent one. +/// +public sealed class RuntimeRemoteSlopeProjectionTests +{ + /// + /// A comfortably walkable ramp steep enough that a failure to track it is + /// unmistakable: plane normal Z = 1/sqrt(1.36) ≈ 0.8575 (30.96 degrees) + /// against retail's 0.6642 floor_z limit (48.4 degrees). The ramp descends + /// along +Y, so +Y root motion is downhill. + /// + private const float WalkableSlopeGradient = 0.6f; + + /// + /// Body-local root displacement per tick — the locomotion-cycle push that + /// moves a running remote between server position updates. 0.1 m at 30 Hz + /// is a 3 m/s run. + /// + private const float RootMotionPerTick = 0.10f; + + private const int TrackedTicks = 30; + + /// + /// How far the body's root may drift from the terrain surface directly + /// below it, relative to where the settled fixture put it. The measured + /// drift on this fixture is under 1e-4 m; 30 unprojected ticks down this + /// ramp accumulate about 1.8 m, so this band is ~350x below the failure it + /// must catch and ~50x above the float noise it must tolerate. + /// + private const float SurfaceTrackingToleranceMeters = 0.005f; + + /// + /// The ramp's own plane, derived from the fixture heightmap rather than + /// from anything the code under test computed. The ramp descends along +Y + /// at gradient, so its plane is gradient*y + z = c and its + /// unit normal is (0, gradient, 1) normalized. + /// + private static Vector3 RampNormal(float gradient) + => Vector3.Normalize(new Vector3(0f, gradient, 1f)); + + /// + /// Fixture validation, run before any motion assertion: the sweep really + /// does report this ramp's own geometric plane, and that plane is walkable. + /// A fixture whose contact plane were flat could not discriminate anything. + /// + [Fact] + public void TheFixtureRampIsWalkableAndItsPlaneIsTheGeometricOne() + { + using RemoteRampHarness harness = + RemoteRampHarness.OnRamp(WalkableSlopeGradient); + + Assert.True(harness.Remote.Body.OnWalkable); + Assert.True(harness.Remote.Body.ContactPlaneValid); + + Vector3 expected = RampNormal(WalkableSlopeGradient); + Vector3 actual = harness.Remote.Body.ContactPlane.Normal; + Assert.True( + Vector3.Distance(expected, actual) < 0.001f, + $"contact plane normal was {actual}, expected the ramp's {expected}"); + Assert.True(actual.Z >= PhysicsGlobals.FloorZ); + } + + /// + /// The artifact the deleted projection existed to remove: a remote running + /// across a slope must have its feet track the ground CONTINUOUSLY between + /// server position updates, not ratchet down in ~5 Hz steps. + /// + /// Asserted on every tick rather than at the end, because a + /// start/end comparison passes a staircase that happens to catch up on the + /// final tick. + /// + /// Sabotage-verified 2026-08-06: discarding the sweep's answer + /// (rm.Body.Position = postIntegratePos instead of + /// resolveResult.Position) reddens this at tick 1 with the body + /// 0.05999 m off the surface. + /// + /// What this test does NOT discriminate, stated so nobody infers + /// it later. Short-circuiting Transition.AdjustOffset to + /// return offset; leaves it GREEN. On terrain the sweep has a + /// second, independent way to put the body on the surface: + /// ValidateWalkable's push-out re-plants the sphere at its natural + /// resting distance from the terrain plane on every sub-step, so the Z + /// outcome survives even with the offset projection gone (what changes is + /// the XY, which adjust_offset shortens). Removing the step-down + /// probe does not change that either — measured. So this test asserts the + /// OUTCOME "a running remote's feet stay on the ground", which is what the + /// deleted projection was there for; it is not a unit test of + /// adjust_offset, and it must not be cited as one. + /// + [Fact] + public void TheRemoteTickTracksTheSurfaceWhileRunningDownhill() + { + using RemoteRampHarness harness = + RemoteRampHarness.OnRamp(WalkableSlopeGradient); + AssertTracksSurface(harness); + } + + /// + /// Anti-vacuity guard for the two tests above: on flat ground they pass + /// without Z ever having to move, so a fixture that quietly flattened + /// would make them meaningless. This asserts the ramp genuinely forces a + /// large Z excursion over the same number of ticks. + /// + [Fact] + public void TheTrackingFixtureActuallyRequiresTheBodyToChangeZ() + { + using RemoteRampHarness harness = + RemoteRampHarness.OnRamp(WalkableSlopeGradient); + float startZ = harness.Remote.Body.Position.Z; + + harness.Tick(TrackedTicks, new Vector3(0f, RootMotionPerTick, 0f)); + + float dz = harness.Remote.Body.Position.Z - startZ; + Assert.True( + dz < -1.0f, + $"fixture is not exercising slope descent: dz = {dz:F4} m"); + } + + private static void AssertTracksSurface(RemoteRampHarness harness) + { + Assert.True(harness.Remote.Body.OnWalkable); + + // The settled resting offset between the body's root and the terrain + // directly beneath it. Measured, not assumed: the spawn settle may put + // the root a hair off the sampled surface. + float restingOffset = + harness.Remote.Body.Position.Z - harness.SurfaceZUnderBody(); + + for (int tick = 1; tick <= TrackedTicks; tick++) + { + harness.Tick(1, new Vector3(0f, RootMotionPerTick, 0f)); + float offset = + harness.Remote.Body.Position.Z - harness.SurfaceZUnderBody(); + Assert.True( + MathF.Abs(offset - restingOffset) < SurfaceTrackingToleranceMeters, + $"tick {tick}: body root sits {offset:F5} m above the terrain " + + $"under it, expected {restingOffset:F5} m " + + $"(pos {harness.Remote.Body.Position})"); + } + } +} diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSteepContactSlideTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSteepContactSlideTests.cs index 9040ac64..7e1488be 100644 --- a/tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSteepContactSlideTests.cs +++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSteepContactSlideTests.cs @@ -53,7 +53,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests [Fact] public void SteepTerrainProducesANonWalkableContactPlane() { - using Harness harness = Harness.OnRamp(SteepGradient); + using RemoteRampHarness harness = RemoteRampHarness.OnRamp(SteepGradient); Assert.True(harness.Remote.Body.ContactPlaneValid); Assert.InRange( @@ -70,7 +70,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests [Fact] public void SteepContactDoesNotLatchALanding() { - using Harness harness = Harness.OnRamp(SteepGradient); + using RemoteRampHarness harness = RemoteRampHarness.OnRamp(SteepGradient); harness.Remote.Airborne = true; int groundEdges = 0; harness.Remote.Motion.RemoveLinkAnimations = () => groundEdges++; @@ -90,7 +90,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests [Fact] public void GravityPersistsAcrossTicksOnASteepContact() { - using Harness harness = Harness.OnRamp(SteepGradient); + using RemoteRampHarness harness = RemoteRampHarness.OnRamp(SteepGradient); harness.Remote.Airborne = true; harness.Tick(40); @@ -107,7 +107,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests [Fact] public void SteepContactKeepsTheBodySlidingDownhill() { - using Harness harness = Harness.OnRamp(SteepGradient); + using RemoteRampHarness harness = RemoteRampHarness.OnRamp(SteepGradient); Vector3 start = harness.Remote.Body.Position; harness.Tick(40); @@ -131,7 +131,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests [Fact] public void TheTickNeverAssertsContactOrWalkableWithoutASweep() { - using Harness harness = Harness.OnRamp(WalkableGradient); + using RemoteRampHarness harness = RemoteRampHarness.OnRamp(WalkableGradient); harness.Remote.CellId = 0u; harness.Remote.Body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable); @@ -155,7 +155,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests [Fact] public void AGroundedTickOnASteepFaceReleasesTheBodyInsteadOfPinningIt() { - using Harness harness = Harness.OnRamp(SteepGradient); + using RemoteRampHarness harness = RemoteRampHarness.OnRamp(SteepGradient); harness.Remote.Body.TransientState |= TransientStateFlags.Contact | TransientStateFlags.OnWalkable; harness.Remote.Airborne = false; @@ -181,7 +181,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests [Fact] public void AuthoritativeVelocityIsNotDiscardedOnAGroundedTick() { - using Harness harness = Harness.OnRamp(WalkableGradient); + using RemoteRampHarness harness = RemoteRampHarness.OnRamp(WalkableGradient); Assert.False(harness.Remote.Airborne); harness.Remote.Body.Velocity = new Vector3(2.146f, 2.264f, -3.549f); @@ -221,12 +221,12 @@ public sealed class RuntimeRemoteSteepContactSlideTests [Fact] public void CommittedTransientsAgreeWithTheCommittedContactPlane() { - using Harness steep = Harness.OnRamp(SteepGradient); + using RemoteRampHarness steep = RemoteRampHarness.OnRamp(SteepGradient); steep.Tick(20); AssertTransientsAreContactPlaneDerived( steep.Remote.Body, expectWalkable: false); - using Harness gentle = Harness.OnRamp(WalkableGradient); + using RemoteRampHarness gentle = RemoteRampHarness.OnRamp(WalkableGradient); gentle.Tick(20); AssertTransientsAreContactPlaneDerived( gentle.Remote.Body, expectWalkable: true); @@ -271,7 +271,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests [Fact] public void WalkableLandingStillLandsAndFiresTheGroundEdgeOnce() { - using Harness harness = Harness.Airborne(WalkableGradient, height: 3f); + using RemoteRampHarness harness = RemoteRampHarness.Airborne(WalkableGradient, height: 3f); int groundEdges = 0; harness.Remote.Motion.RemoveLinkAnimations = () => groundEdges++; @@ -293,226 +293,11 @@ public sealed class RuntimeRemoteSteepContactSlideTests [Fact] public void WalkableLandingDoesNotClearTheGravityStateBit() { - using Harness harness = Harness.Airborne(WalkableGradient, height: 3f); + using RemoteRampHarness harness = RemoteRampHarness.Airborne(WalkableGradient, height: 3f); harness.Tick(60); Assert.True(harness.Remote.Body.OnWalkable); Assert.True(harness.Remote.Body.HasGravity); } - - private sealed class Harness : IDisposable - { - private const uint LandblockId = 0x0101FFFFu; - - private readonly RuntimeEntityObjectLifetime _lifetime; - private readonly RuntimeEntityRecord _record; - private readonly RuntimeRemotePhysicsUpdater _updater; - - internal RemoteMotion Remote { get; } - - private Harness( - RuntimeEntityObjectLifetime lifetime, - RuntimeEntityRecord record, - RemoteMotion remote, - RuntimeRemotePhysicsUpdater updater) - { - _lifetime = lifetime; - _record = record; - Remote = remote; - _updater = updater; - } - - /// Body already resting on the ramp, contact established. - internal static Harness OnRamp(float gradient) - { - Harness harness = Create(gradient, heightAboveSurface: 0f); - // Retail gains spawn contact from the first gravity frame; the - // stationary-remote settle (SpawnPlacementSettler, #270) compresses - // it. Use the production seam so the fixture starts from exactly - // the state a live spawn would. - SpawnPlacementSettler.TrySettle( - harness._lifetime.Physics.Engine, - harness.Remote.Body, - harness.Remote.Body.Position, - harness.Remote.CellId, - sphereRadius: 0.48f, - sphereHeight: 1.835f, - ObjectInfoState.EdgeSlide, - harness._record.LocalEntityId!.Value, - harness.Remote.Movement.HitGround, - harness.Remote.Motion.LeaveGround); - harness.Remote.Airborne = !harness.Remote.Body.OnWalkable; - return harness; - } - - /// Body suspended above the ramp with no contact at all. - internal static Harness Airborne(float gradient, float height) - { - Harness harness = Create(gradient, heightAboveSurface: height); - harness.Remote.Body.TransientState &= ~(TransientStateFlags.Contact - | TransientStateFlags.OnWalkable); - harness.Remote.Body.ContactPlaneValid = false; - harness.Remote.Airborne = true; - return harness; - } - - private static Harness Create(float gradient, float heightAboveSurface) - { - var lifetime = new RuntimeEntityObjectLifetime(); - lifetime.Physics.Engine.AddLandblock( - LandblockId, - Ramp(gradient), - Array.Empty(), - Array.Empty(), - worldOffsetX: 0f, - worldOffsetY: 0f); - - RuntimeEntityRecord record = lifetime.Entities.AddActive(Spawn()); - var body = new PhysicsBody - { - // Retail CPhysicsObj constructor state 0x400C08 @0x00512508 - // (EdgeSlide | Lighting | Gravity | ReportCollisions), which - // ACE also sends for every creature (PhysicsGlobals.DefaultState). - State = PhysicsStateFlags.Gravity - | PhysicsStateFlags.ReportCollisions - | PhysicsStateFlags.EdgeSlide, - InWorld = true, - }; - var remote = new RemoteMotion(body); - lifetime.Entities.SetPhysicsBody(record, body); - lifetime.Entities.SetRemoteMotion(record, remote); - lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true); - - const float localX = 96f; - const float localY = 96f; - float surfaceZ = Ramp(gradient).SampleZ(localX, localY); - body.Position = new Vector3( - localX, - localY, - surfaceZ + heightAboveSurface); - body.Orientation = Quaternion.Identity; - remote.CellId = TerrainSurface.ComputeOutdoorCellId( - LandblockId, - localX, - localY); - remote.LastServerPos = body.Position; - remote.LastServerPosTime = 1.0; - - return new Harness( - lifetime, - record, - remote, - new RuntimeRemotePhysicsUpdater(lifetime.Physics)); - } - - internal void Tick(int count, float dt = 1f / 30f) - { - var frame = new MotionDeltaFrame(); - for (int i = 0; i < count; i++) - { - frame.Reset(); - _updater.Tick( - _record, - Remote, - objectScale: 1f, - sequencer: null, - dt, - _record.ObjectClockEpoch, - frame, - radius: 0.48f, - height: 1.835f, - liveCenterX: 1, - liveCenterY: 1); - } - } - - /// - /// A constant-gradient ramp climbing along +Y. The heightmap byte at - /// (x, y) indexes a table whose entries rise linearly, so every cell of - /// the landblock has the same plane normal and the sampled contact - /// plane is exactly normalize((0, -gradient, 1)). - /// - private static TerrainSurface Ramp(float gradient) - { - var heightTable = new float[256]; - for (int i = 0; i < heightTable.Length; i++) - heightTable[i] = i * gradient * TerrainSurface.CellSize; - - var heights = new byte[81]; - for (int x = 0; x < 9; x++) - for (int y = 0; y < 9; y++) - heights[x * 9 + y] = (byte)(8 - y); - - return new TerrainSurface(heights, heightTable); - } - - private static WorldSession.EntitySpawn Spawn() - { - var position = new CreateObject.ServerPosition( - LandblockId, - 96f, - 96f, - 0f, - 1f, - 0f, - 0f, - 0f); - var timestamps = new PhysicsTimestamps( - Position: 1, - Movement: 1, - State: 1, - Vector: 1, - Teleport: 0, - ServerControlledMove: 1, - ForcePosition: 0, - ObjDesc: 1, - Instance: 1); - const uint rawState = (uint)(PhysicsStateFlags.Gravity - | PhysicsStateFlags.ReportCollisions - | PhysicsStateFlags.EdgeSlide); - var physics = new PhysicsSpawnData( - RawState: rawState, - Position: position, - Movement: null, - AnimationFrame: null, - SetupTableId: 0x02000001u, - MotionTableId: 0x09000001u, - SoundTableId: null, - PhysicsScriptTableId: null, - Parent: null, - Children: null, - Scale: null, - Friction: null, - Elasticity: null, - Translucency: null, - Velocity: null, - Acceleration: null, - AngularVelocity: null, - DefaultScriptType: null, - DefaultScriptIntensity: null, - Timestamps: timestamps); - return new WorldSession.EntitySpawn( - 0x70000101u, - position, - 0x02000001u, - Array.Empty(), - Array.Empty(), - Array.Empty(), - null, - null, - "bug-b-fixture", - null, - null, - 0x09000001u, - PhysicsState: rawState, - InstanceSequence: 1, - MovementSequence: 1, - ServerControlSequence: 1, - PositionSequence: 1, - Physics: physics); - } - - public void Dispose() => _lifetime.Dispose(); - } }