feat(vfx): port retail hidden and teleport presentation
Preserve canonical live-object ownership across Hidden transitions and remote teleport placement so effects, collision, streaming, and targeting remain synchronized.
This commit is contained in:
parent
a51ebc66e9
commit
1e98d81448
46 changed files with 4883 additions and 127 deletions
82
tests/AcDream.App.Tests/Physics/PlayerMovementHiddenTests.cs
Normal file
82
tests/AcDream.App.Tests/Physics/PlayerMovementHiddenTests.cs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Tests.Physics;
|
||||
|
||||
public sealed class PlayerMovementHiddenTests
|
||||
{
|
||||
[Fact]
|
||||
public void TickHidden_PositionManagerMovesRootAndEffectPoseWithoutPhysicsInput()
|
||||
{
|
||||
const uint playerGuid = 0x50000001u;
|
||||
const uint targetGuid = 0x50000002u;
|
||||
const uint cellId = 0x01010001u;
|
||||
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
controller.SetPosition(Vector3.Zero, cellId, Vector3.Zero);
|
||||
|
||||
var hosts = new Dictionary<uint, IPhysicsObjHost>();
|
||||
EntityPhysicsHost? playerHost = null;
|
||||
playerHost = new EntityPhysicsHost(
|
||||
playerGuid,
|
||||
() => controller.CellPosition,
|
||||
() => controller.BodyVelocity,
|
||||
() => 0.48f,
|
||||
() => controller.BodyInContact,
|
||||
() => 3f,
|
||||
() => 0.1,
|
||||
() => 0.1,
|
||||
id => hosts.GetValueOrDefault(id),
|
||||
info => playerHost!.PositionManager.HandleUpdateTarget(info),
|
||||
() => { });
|
||||
var targetPosition = new Position(
|
||||
cellId,
|
||||
new Vector3(5f, 0f, 0f),
|
||||
Quaternion.Identity);
|
||||
var targetHost = new EntityPhysicsHost(
|
||||
targetGuid,
|
||||
() => targetPosition,
|
||||
() => Vector3.Zero,
|
||||
() => 0.48f,
|
||||
() => true,
|
||||
() => null,
|
||||
() => 0.1,
|
||||
() => 0.1,
|
||||
id => hosts.GetValueOrDefault(id),
|
||||
_ => { },
|
||||
() => { });
|
||||
hosts.Add(playerGuid, playerHost);
|
||||
hosts.Add(targetGuid, targetHost);
|
||||
controller.PositionManager = playerHost.PositionManager;
|
||||
playerHost.PositionManager.StickTo(targetGuid, radius: 0.48f, height: 1.835f);
|
||||
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 7u,
|
||||
ServerGuid = playerGuid,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
ParentCellId = cellId,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
poses.Publish(entity, Array.Empty<Matrix4x4>());
|
||||
|
||||
MovementResult result = controller.TickHidden(0.1f);
|
||||
entity.SetPosition(result.Position);
|
||||
entity.ParentCellId = result.CellId;
|
||||
Assert.True(poses.UpdateRoot(entity));
|
||||
|
||||
Assert.InRange(result.Position.X, 0.01f, 1.5f);
|
||||
Assert.Equal(0f, result.Position.Z);
|
||||
Assert.Equal(Vector3.Zero, controller.BodyVelocity);
|
||||
Assert.True(poses.TryGetRootPose(entity.Id, out Matrix4x4 root));
|
||||
Assert.Equal(result.Position, root.Translation);
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,96 @@ public sealed class ProjectileControllerTests
|
|||
Assert.Same(record.ProjectileRuntime!.Body, record.PhysicsBody);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hidden_PausesProjectileWithoutClearingActiveOrReplayingClockBacklog()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
LiveEntityRecord record = fixture.Spawn(instance: 1);
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
fixture.Engine.ShadowObjects.Register(
|
||||
entity.Id,
|
||||
entity.SourceGfxObjOrSetupId,
|
||||
entity.Position,
|
||||
entity.Rotation,
|
||||
radius: 0.5f,
|
||||
worldOffsetX: 0f,
|
||||
worldOffsetY: 0f,
|
||||
landblockId: 0x01010000u,
|
||||
state: (uint)MissileState,
|
||||
seedCellId: CellA);
|
||||
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0, 1, 1));
|
||||
PhysicsBody body = record.PhysicsBody!;
|
||||
Assert.True(body.IsActive);
|
||||
|
||||
record.FinalPhysicsState = MissileState
|
||||
| PhysicsStateFlags.Hidden
|
||||
| PhysicsStateFlags.IgnoreCollisions;
|
||||
Assert.True(fixture.Controller.ApplyAuthoritativeState(
|
||||
Guid, record.FinalPhysicsState, 1.1, 1, 1));
|
||||
Vector3 hiddenPosition = entity.Position;
|
||||
fixture.Controller.Tick(5.0, 1, 1, playerWorldPosition: null);
|
||||
|
||||
Assert.Equal(hiddenPosition, entity.Position);
|
||||
Assert.True(body.InWorld);
|
||||
Assert.True(body.IsActive);
|
||||
Assert.Equal(5.0, body.LastUpdateTime, 8);
|
||||
Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered);
|
||||
|
||||
record.FinalPhysicsState = MissileState;
|
||||
Assert.True(fixture.Controller.ApplyAuthoritativeState(
|
||||
Guid, record.FinalPhysicsState, 5.0, 1, 1));
|
||||
fixture.Controller.Tick(5.1, 1, 1, playerWorldPosition: null);
|
||||
|
||||
Assert.True(entity.Position.X > hiddenPosition.X);
|
||||
Assert.True(entity.Position.X < hiddenPosition.X + 2f);
|
||||
Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HiddenSharedRemoteProjectile_UsesPositionManagerNarrowTickExactlyOnce()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
LiveEntityRecord record = fixture.Spawn(instance: 1);
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
Assert.Null(record.AnimationRuntime);
|
||||
var remote = new GameWindow.RemoteMotion();
|
||||
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
|
||||
remote.Body.Position = entity.Position;
|
||||
remote.Body.Orientation = entity.Rotation;
|
||||
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
|
||||
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0, 1, 1));
|
||||
Assert.Same(remote.Body, record.ProjectileRuntime!.Body);
|
||||
|
||||
record.FinalPhysicsState = MissileState
|
||||
| PhysicsStateFlags.Hidden
|
||||
| PhysicsStateFlags.IgnoreCollisions;
|
||||
Assert.True(fixture.Controller.ApplyAuthoritativeState(
|
||||
Guid, record.FinalPhysicsState, 1.1, 1, 1));
|
||||
remote.Interp.Enqueue(
|
||||
entity.Position + Vector3.UnitX,
|
||||
heading: 0f,
|
||||
isMovingTo: false,
|
||||
currentBodyPosition: remote.Body.Position);
|
||||
var updater = new RemotePhysicsUpdater(
|
||||
fixture.Engine,
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
|
||||
int publishedRoots = 0;
|
||||
updater.TickHiddenEntities(
|
||||
fixture.Live,
|
||||
localPlayerServerGuid: 0x50000001u,
|
||||
dt: 0.1f,
|
||||
_ => publishedRoots++);
|
||||
Vector3 afterNarrowTick = entity.Position;
|
||||
fixture.Controller.Tick(1.2, 1, 1, playerWorldPosition: null);
|
||||
|
||||
Assert.True(afterNarrowTick.X > 10f);
|
||||
Assert.Equal(afterNarrowTick, entity.Position);
|
||||
Assert.Equal(1, publishedRoots);
|
||||
Assert.True(fixture.Controller.HandlesMovement(Guid));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingDestinationAndLaterHydration_RetainIdentityAndProjectileOwner()
|
||||
{
|
||||
|
|
|
|||
166
tests/AcDream.App.Tests/Physics/RemotePhysicsUpdaterTests.cs
Normal file
166
tests/AcDream.App.Tests/Physics/RemotePhysicsUpdaterTests.cs
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Tests.Physics;
|
||||
|
||||
public sealed class RemotePhysicsUpdaterTests
|
||||
{
|
||||
[Fact]
|
||||
public void TickHidden_AppliesPositionManagerOffsetWithoutAdvancingPhysics()
|
||||
{
|
||||
var motion = new GameWindow.RemoteMotion();
|
||||
motion.Body.Position = Vector3.Zero;
|
||||
motion.Body.Orientation = Quaternion.Identity;
|
||||
motion.Body.Velocity = new Vector3(4f, 0f, 5f);
|
||||
motion.Body.Acceleration = new Vector3(0f, 0f, -9.8f);
|
||||
motion.CellId = 0x01010001u;
|
||||
motion.Interp.Enqueue(
|
||||
new Vector3(1f, 0f, 0f),
|
||||
heading: 0f,
|
||||
isMovingTo: false,
|
||||
currentBodyPosition: Vector3.Zero);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 1u,
|
||||
ServerGuid = 0x70000001u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
|
||||
var updater = new RemotePhysicsUpdater(
|
||||
new PhysicsEngine(),
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
poses.Publish(entity, Array.Empty<Matrix4x4>());
|
||||
updater.TickHidden(motion, entity, 0.1f);
|
||||
Assert.True(poses.UpdateRoot(entity));
|
||||
|
||||
Assert.InRange(motion.Body.Position.X, 0.01f, 1f);
|
||||
Assert.Equal(0f, motion.Body.Position.Z);
|
||||
Assert.Equal(new Vector3(4f, 0f, 5f), motion.Body.Velocity);
|
||||
Assert.Equal(motion.Body.Position, entity.Position);
|
||||
Assert.Equal(motion.CellId, entity.ParentCellId);
|
||||
Assert.True(poses.TryGetRootPose(entity.Id, out Matrix4x4 root));
|
||||
Assert.Equal(entity.Position, root.Translation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TickHidden_CrossCellCommitUpdatesCanonicalCellBeforeUnhide()
|
||||
{
|
||||
var engine = BuildBoundaryEngine();
|
||||
var updater = new RemotePhysicsUpdater(
|
||||
engine,
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
var motion = new GameWindow.RemoteMotion();
|
||||
motion.Body.Position = new Vector3(191f, 10f, 50f);
|
||||
motion.Body.Orientation = Quaternion.Identity;
|
||||
motion.Body.TransientState = TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable;
|
||||
uint canonicalCell = 0xA9B40039u;
|
||||
int canonicalWrites = 0;
|
||||
motion.BindCanonicalCell(
|
||||
() => canonicalCell,
|
||||
value =>
|
||||
{
|
||||
canonicalCell = value;
|
||||
canonicalWrites++;
|
||||
});
|
||||
motion.CellId = canonicalCell;
|
||||
canonicalWrites = 0;
|
||||
motion.Interp.Enqueue(
|
||||
new Vector3(193f, 10f, 50f),
|
||||
heading: 0f,
|
||||
isMovingTo: false,
|
||||
currentBodyPosition: motion.Body.Position);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 1u,
|
||||
ServerGuid = 0x70000002u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = motion.Body.Position,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
|
||||
updater.TickHidden(motion, entity, 2f);
|
||||
|
||||
Assert.Equal(0xAAB40001u, canonicalCell);
|
||||
Assert.Equal(canonicalCell, entity.ParentCellId);
|
||||
Assert.True(canonicalWrites > 0);
|
||||
Assert.True(entity.Position.X > 192f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TickHidden_LandingSynchronizesRemoteAirborneState()
|
||||
{
|
||||
var engine = BuildBoundaryEngine();
|
||||
var updater = new RemotePhysicsUpdater(
|
||||
engine,
|
||||
(_, _) => (0.48f, 1.835f),
|
||||
(_, _, _, _) => { });
|
||||
var motion = new GameWindow.RemoteMotion
|
||||
{
|
||||
Airborne = true,
|
||||
};
|
||||
motion.Body.Position = new Vector3(10f, 10f, 50f);
|
||||
motion.Body.Orientation = Quaternion.Identity;
|
||||
motion.Body.TransientState = TransientStateFlags.Active;
|
||||
motion.CellId = 0xA9B40001u;
|
||||
motion.Interp.Enqueue(
|
||||
new Vector3(11f, 10f, 50f),
|
||||
heading: 0f,
|
||||
isMovingTo: false,
|
||||
currentBodyPosition: motion.Body.Position);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 3u,
|
||||
ServerGuid = 0x70000003u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = motion.Body.Position,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
|
||||
updater.TickHidden(motion, entity, 0.1f);
|
||||
|
||||
Assert.True(motion.Body.InContact);
|
||||
Assert.True(motion.Body.OnWalkable);
|
||||
Assert.False(motion.Airborne);
|
||||
}
|
||||
|
||||
private static PhysicsEngine BuildBoundaryEngine()
|
||||
{
|
||||
static TerrainSurface FlatTerrain()
|
||||
{
|
||||
var heights = new byte[81];
|
||||
var table = new float[256];
|
||||
Array.Fill(table, 50f);
|
||||
return new TerrainSurface(heights, table);
|
||||
}
|
||||
|
||||
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
|
||||
engine.AddLandblock(
|
||||
0xA9B4FFFFu,
|
||||
FlatTerrain(),
|
||||
Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(),
|
||||
0f,
|
||||
0f);
|
||||
engine.AddLandblock(
|
||||
0xAAB4FFFFu,
|
||||
FlatTerrain(),
|
||||
Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(),
|
||||
192f,
|
||||
0f);
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
1288
tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs
Normal file
1288
tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs
Normal file
File diff suppressed because it is too large
Load diff
38
tests/AcDream.App.Tests/Physics/RemoteTeleportHookTests.cs
Normal file
38
tests/AcDream.App.Tests/Physics/RemoteTeleportHookTests.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using AcDream.App.Physics;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Tests.Physics;
|
||||
|
||||
public sealed class RemoteTeleportHookTests
|
||||
{
|
||||
[Fact]
|
||||
public void Execute_UsesRetailOrderAndExactCancelContext()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
WeenieError cancelContext = WeenieError.None;
|
||||
|
||||
RemoteTeleportHook.Execute(new RemoteTeleportHookActions(
|
||||
CancelMoveTo: error =>
|
||||
{
|
||||
cancelContext = error;
|
||||
calls.Add("cancel");
|
||||
},
|
||||
UnStick: () => calls.Add("unstick"),
|
||||
StopInterpolating: () => calls.Add("stop-interpolating"),
|
||||
UnConstrain: () => calls.Add("unconstrain"),
|
||||
NotifyTeleported: () => calls.Add("notify-teleported"),
|
||||
ReportCollisionEnd: () => calls.Add("collision-end")));
|
||||
|
||||
Assert.Equal(0x3Cu, (uint)cancelContext);
|
||||
Assert.Equal(
|
||||
[
|
||||
"cancel",
|
||||
"unstick",
|
||||
"stop-interpolating",
|
||||
"unconstrain",
|
||||
"notify-teleported",
|
||||
"collision-end",
|
||||
],
|
||||
calls);
|
||||
}
|
||||
}
|
||||
194
tests/AcDream.App.Tests/Physics/RemoteTeleportPlacementTests.cs
Normal file
194
tests/AcDream.App.Tests/Physics/RemoteTeleportPlacementTests.cs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.Physics;
|
||||
|
||||
public sealed class RemoteTeleportPlacementTests
|
||||
{
|
||||
[Fact]
|
||||
public void Apply_HardSnapsFullFrameAndPhysicsClock()
|
||||
{
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
Orientation = Quaternion.Identity,
|
||||
LastUpdateTime = 2.0,
|
||||
};
|
||||
body.SnapToCell(
|
||||
0x01010001u,
|
||||
new Vector3(3f, 4f, 5f),
|
||||
new Vector3(3f, 4f, 5f));
|
||||
var remote = new AcDream.App.Rendering.GameWindow.RemoteMotion(body);
|
||||
Quaternion orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f);
|
||||
|
||||
RemoteTeleportPlacement.Apply(
|
||||
remote,
|
||||
body,
|
||||
new ResolveResult(
|
||||
new Vector3(210f, 220f, 12f),
|
||||
0x02020123u,
|
||||
IsOnGround: false),
|
||||
new Vector3(18f, 28f, 12f),
|
||||
orientation,
|
||||
17.5,
|
||||
previousContact: body.InContact,
|
||||
previousOnWalkable: body.OnWalkable);
|
||||
|
||||
Assert.Equal(new Vector3(210f, 220f, 12f), body.Position);
|
||||
Assert.Equal(0x02020123u, body.CellPosition.ObjCellId);
|
||||
Assert.Equal(new Vector3(18f, 28f, 12f), body.CellPosition.Frame.Origin);
|
||||
Assert.Equal(orientation, body.Orientation);
|
||||
Assert.Equal(17.5, body.LastUpdateTime);
|
||||
Assert.True(body.InWorld);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_InstallsPlacementDerivedContactAndSynchronizesAirborneState()
|
||||
{
|
||||
var velocity = new Vector3(1f, 2f, 3f);
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
Velocity = velocity,
|
||||
TransientState = TransientStateFlags.Active,
|
||||
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
|
||||
};
|
||||
var contactPlane = new Plane(Vector3.UnitZ, 0f);
|
||||
var remote = new AcDream.App.Rendering.GameWindow.RemoteMotion(body)
|
||||
{
|
||||
Airborne = true,
|
||||
};
|
||||
|
||||
RemoteTeleportPlacement.Apply(
|
||||
remote,
|
||||
body,
|
||||
new ResolveResult(
|
||||
new Vector3(200f, 200f, 50f),
|
||||
0x02020001u,
|
||||
IsOnGround: true,
|
||||
InContact: true,
|
||||
OnWalkable: true,
|
||||
ContactPlane: contactPlane,
|
||||
ContactPlaneCellId: 0x02020001u),
|
||||
new Vector3(8f, 8f, 50f),
|
||||
Quaternion.Identity,
|
||||
9.0,
|
||||
previousContact: false,
|
||||
previousOnWalkable: false);
|
||||
|
||||
Assert.Equal(Vector3.Zero, body.Velocity);
|
||||
Assert.True(body.InContact);
|
||||
Assert.True(body.OnWalkable);
|
||||
Assert.True(body.ContactPlaneValid);
|
||||
Assert.Equal(contactPlane, body.ContactPlane);
|
||||
Assert.False(remote.Airborne);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_PendingHydrationUsesPreservedSourceFlagsBeforeCollisionResponse()
|
||||
{
|
||||
var originalVelocity = new Vector3(-3f, 0f, 0f);
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
Velocity = originalVelocity,
|
||||
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
|
||||
};
|
||||
// ParkPending deliberately clears these flags while the destination
|
||||
// landblock is absent. SetPositionInternal must still receive the
|
||||
// grounded source edge captured before parking.
|
||||
body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable);
|
||||
var remote = new AcDream.App.Rendering.GameWindow.RemoteMotion(body)
|
||||
{
|
||||
Airborne = true,
|
||||
};
|
||||
|
||||
RemoteTeleportPlacement.Apply(
|
||||
remote,
|
||||
body,
|
||||
new ResolveResult(
|
||||
new Vector3(200f, 200f, 50f),
|
||||
0x02020001u,
|
||||
IsOnGround: true,
|
||||
CollisionNormalValid: true,
|
||||
CollisionNormal: Vector3.UnitX,
|
||||
InContact: true,
|
||||
OnWalkable: true,
|
||||
ContactPlane: new Plane(Vector3.UnitZ, 0f),
|
||||
ContactPlaneCellId: 0x02020001u),
|
||||
new Vector3(8f, 8f, 50f),
|
||||
Quaternion.Identity,
|
||||
9.0,
|
||||
previousContact: true,
|
||||
previousOnWalkable: true);
|
||||
|
||||
// Staying on walkable ground suppresses reflection. If the parked
|
||||
// false/false flags were used, the -X velocity would reflect to +X.
|
||||
Assert.Equal(originalVelocity, body.Velocity);
|
||||
Assert.True(body.OnWalkable);
|
||||
Assert.False(remote.Airborne);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_PendingGroundToSteepContact_RestoresSourceWalkabilityForFirstAcceleration()
|
||||
{
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
Omega = new Vector3(0f, 0f, 3f),
|
||||
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
|
||||
};
|
||||
// The unloaded-destination parking frame cleared both bits. Retail
|
||||
// restores the captured source OnWalkable bit through the first
|
||||
// calc_acceleration, which clears grounded angular drift, before
|
||||
// set_on_walkable installs the steep destination's false value.
|
||||
body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable);
|
||||
var remote = new AcDream.App.Rendering.GameWindow.RemoteMotion(body);
|
||||
|
||||
RemoteTeleportPlacement.Apply(
|
||||
remote,
|
||||
body,
|
||||
new ResolveResult(
|
||||
new Vector3(200f, 200f, 50f),
|
||||
0x02020001u,
|
||||
IsOnGround: false,
|
||||
InContact: true,
|
||||
OnWalkable: false,
|
||||
ContactPlane: new Plane(Vector3.Normalize(new Vector3(1f, 0f, 0.2f)), 0f),
|
||||
ContactPlaneCellId: 0x02020001u),
|
||||
new Vector3(8f, 8f, 50f),
|
||||
Quaternion.Identity,
|
||||
9.0,
|
||||
previousContact: true,
|
||||
previousOnWalkable: true);
|
||||
|
||||
Assert.Equal(Vector3.Zero, body.Omega);
|
||||
Assert.True(body.InContact);
|
||||
Assert.False(body.OnWalkable);
|
||||
Assert.True(remote.Airborne);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_RejectsMalformedPlacementWithoutMutatingBody()
|
||||
{
|
||||
var original = new Vector3(7f, 8f, 9f);
|
||||
var body = new PhysicsBody { Position = original };
|
||||
var remote = new AcDream.App.Rendering.GameWindow.RemoteMotion(body);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
RemoteTeleportPlacement.Apply(
|
||||
remote,
|
||||
body,
|
||||
new ResolveResult(
|
||||
new Vector3(float.NaN, 0f, 0f),
|
||||
0,
|
||||
IsOnGround: false,
|
||||
Ok: false),
|
||||
Vector3.Zero,
|
||||
Quaternion.Identity,
|
||||
double.NaN,
|
||||
previousContact: false,
|
||||
previousOnWalkable: false));
|
||||
|
||||
Assert.Equal(original, body.Position);
|
||||
Assert.Equal(0u, body.CellPosition.ObjCellId);
|
||||
}
|
||||
}
|
||||
|
|
@ -88,6 +88,38 @@ public sealed class AttachmentUpdateOrderTests
|
|||
Assert.Equal(new Vector3(100f, 2f, 0f), renderedOrigin, new Vector3ToleranceComparer());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestedDrawable_InheritsAncestorSuppressionWithoutMutatingOwnState()
|
||||
{
|
||||
var root = Entity(1u);
|
||||
var child = Entity(2u);
|
||||
var grandchild = Entity(3u);
|
||||
root.IsDrawVisible = false;
|
||||
|
||||
EquippedChildRenderController.ApplyParentDrawVisibility(child, root);
|
||||
EquippedChildRenderController.ApplyParentDrawVisibility(grandchild, child);
|
||||
|
||||
Assert.True(child.IsDrawVisible);
|
||||
Assert.False(child.IsAncestorDrawVisible);
|
||||
Assert.True(grandchild.IsDrawVisible);
|
||||
Assert.False(grandchild.IsAncestorDrawVisible);
|
||||
|
||||
root.IsDrawVisible = true;
|
||||
EquippedChildRenderController.ApplyParentDrawVisibility(child, root);
|
||||
EquippedChildRenderController.ApplyParentDrawVisibility(grandchild, child);
|
||||
Assert.True(child.IsAncestorDrawVisible);
|
||||
Assert.True(grandchild.IsAncestorDrawVisible);
|
||||
}
|
||||
|
||||
private static AcDream.Core.World.WorldEntity Entity(uint id) => new()
|
||||
{
|
||||
Id = id,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<AcDream.Core.World.MeshRef>(),
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void AncestorTeardownCollectsEntireAttachmentSubtreePostOrder()
|
||||
{
|
||||
|
|
|
|||
65
tests/AcDream.App.Tests/Rendering/EntityPhysicsHostTests.cs
Normal file
65
tests/AcDream.App.Tests/Rendering/EntityPhysicsHostTests.cs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class EntityPhysicsHostTests
|
||||
{
|
||||
[Fact]
|
||||
public void NotifyHidden_FansNonOkStatusAndClearsWatcherSubscription()
|
||||
{
|
||||
const uint watcherId = 0x50000001u;
|
||||
const uint targetId = 0x70000001u;
|
||||
var hosts = new Dictionary<uint, IPhysicsObjHost>();
|
||||
var watcherUpdates = new List<TargetInfo>();
|
||||
|
||||
EntityPhysicsHost watcher = CreateHost(
|
||||
watcherId,
|
||||
hosts,
|
||||
watcherUpdates.Add);
|
||||
EntityPhysicsHost target = CreateHost(targetId, hosts, _ => { });
|
||||
hosts.Add(watcherId, watcher);
|
||||
hosts.Add(targetId, target);
|
||||
watcher.SetTarget(0u, targetId, 1f, 0.1);
|
||||
watcherUpdates.Clear();
|
||||
|
||||
target.NotifyHidden();
|
||||
|
||||
TargetInfo hidden = Assert.Single(watcherUpdates);
|
||||
Assert.Equal(TargetStatus.ExitWorld, hidden.Status);
|
||||
Assert.Null(watcher.TargetManager.TargetInfo);
|
||||
Assert.False(target.TargetManager.VoyeurTable!.ContainsKey(watcherId));
|
||||
|
||||
watcherUpdates.Clear();
|
||||
watcher.SetTarget(0u, targetId, 1f, 0.1);
|
||||
|
||||
TargetInfo restored = Assert.Single(watcherUpdates);
|
||||
Assert.Equal(TargetStatus.Ok, restored.Status);
|
||||
Assert.Equal(targetId, restored.ObjectId);
|
||||
Assert.True(target.TargetManager.VoyeurTable.ContainsKey(watcherId));
|
||||
}
|
||||
|
||||
private static EntityPhysicsHost CreateHost(
|
||||
uint id,
|
||||
IReadOnlyDictionary<uint, IPhysicsObjHost> hosts,
|
||||
Action<TargetInfo> update)
|
||||
{
|
||||
return new EntityPhysicsHost(
|
||||
id,
|
||||
getPosition: () => new Position(
|
||||
0x01010001u,
|
||||
new Vector3(id == 0x50000001u ? 0f : 2f, 0f, 0f),
|
||||
Quaternion.Identity),
|
||||
getVelocity: () => Vector3.Zero,
|
||||
getRadius: () => 0.5f,
|
||||
inContact: () => true,
|
||||
minterpMaxSpeed: () => 1f,
|
||||
curTime: () => 1.0,
|
||||
physicsTimerTime: () => 1.0,
|
||||
getObjectA: objectId => hosts.GetValueOrDefault(objectId),
|
||||
handleUpdateTarget: update,
|
||||
interruptCurrentMovement: () => { });
|
||||
}
|
||||
}
|
||||
|
|
@ -166,6 +166,28 @@ public sealed class EntityEffectControllerTests
|
|||
Assert.DoesNotContain(fixture.Sink.Calls, call => call.EntityId == Guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreparedOwner_DoesNotReplayPendingPacketsUntilConstructionBarrierOpens()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
fixture.Controller.HandleDirect(new PlayPhysicsScript(Guid, DirectDid));
|
||||
WorldSession.EntitySpawn spawn = Spawn(Guid, 1);
|
||||
fixture.Runtime.RegisterLiveEntity(spawn);
|
||||
fixture.Runtime.SetEffectProfile(Guid, Fixture.LiveProfile());
|
||||
fixture.Runtime.MaterializeLiveEntity(
|
||||
Guid,
|
||||
spawn.Position!.Value.LandblockId,
|
||||
id => Entity(id, Guid));
|
||||
|
||||
Assert.True(fixture.Controller.PrepareLiveEntityOwner(Guid));
|
||||
Assert.Equal(1, fixture.Controller.PendingPacketCount);
|
||||
Assert.Equal(0, fixture.Runner.ActiveScriptCount);
|
||||
|
||||
Assert.True(fixture.Controller.ReplayPendingForLiveEntity(Guid));
|
||||
Assert.Equal(0, fixture.Controller.PendingPacketCount);
|
||||
Assert.Equal(1, fixture.Runner.ActiveScriptCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadyOwnerReceivesDirectAndTypedPacketsImmediately()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -96,6 +96,46 @@ public sealed class RadarSnapshotProviderTests
|
|||
Assert.True(snapshot.UiLocked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildSnapshot_UsesCanonicalPlayerWhileHiddenTargetsStayExcluded()
|
||||
{
|
||||
const uint player = 20u;
|
||||
const uint hiddenTarget = 21u;
|
||||
var objects = new ClientObjectTable();
|
||||
objects.Ingest(Weenie(player, "Player", ItemType.Creature));
|
||||
objects.Ingest(Weenie(hiddenTarget, "Hidden target", ItemType.Creature) with
|
||||
{
|
||||
RadarBehavior = (byte)RadarBehavior.ShowAlways,
|
||||
});
|
||||
var canonical = new Dictionary<uint, WorldEntity>
|
||||
{
|
||||
[player] = Entity(player, Vector3.Zero, Quaternion.Identity),
|
||||
[hiddenTarget] = Entity(hiddenTarget, new Vector3(5f, 0f, 0f), Quaternion.Identity),
|
||||
};
|
||||
var interactionVisible = new Dictionary<uint, WorldEntity>();
|
||||
var spawns = new Dictionary<uint, WorldSession.EntitySpawn>
|
||||
{
|
||||
[player] = Spawn(player),
|
||||
[hiddenTarget] = Spawn(hiddenTarget),
|
||||
};
|
||||
var provider = new RadarSnapshotProvider(
|
||||
objects,
|
||||
interactionVisible,
|
||||
spawns,
|
||||
playerGuid: () => player,
|
||||
playerYawRadians: () => 0f,
|
||||
playerCellId: () => 0xA9B40001u,
|
||||
selectedGuid: () => hiddenTarget,
|
||||
coordinatesOnRadar: () => true,
|
||||
uiLocked: () => false,
|
||||
playerEntities: canonical);
|
||||
|
||||
var snapshot = provider.BuildSnapshot();
|
||||
|
||||
Assert.NotNull(snapshot.CoordinatesText);
|
||||
Assert.Empty(snapshot.Blips);
|
||||
}
|
||||
|
||||
private static WorldEntity Entity(uint guid, Vector3 position, Quaternion rotation) => new()
|
||||
{
|
||||
Id = guid,
|
||||
|
|
|
|||
|
|
@ -180,9 +180,10 @@ public sealed class InboundPhysicsStateControllerTests
|
|||
liveVelocity,
|
||||
out PositionTimestampDisposition normalDisposition,
|
||||
out WorldSession.EntitySpawn normal,
|
||||
out _));
|
||||
out AcceptedPhysicsTimestamps normalTimestamps));
|
||||
Assert.Equal(PositionTimestampDisposition.Apply, normalDisposition);
|
||||
Assert.Equal(liveVelocity, normal.Physics!.Value.Velocity);
|
||||
Assert.False(normalTimestamps.TeleportAdvanced);
|
||||
|
||||
Assert.True(controller.TryApplyPosition(
|
||||
new WorldSession.EntityPositionUpdate(
|
||||
|
|
@ -200,9 +201,10 @@ public sealed class InboundPhysicsStateControllerTests
|
|||
liveVelocity,
|
||||
out PositionTimestampDisposition teleportDisposition,
|
||||
out WorldSession.EntitySpawn teleported,
|
||||
out _));
|
||||
out AcceptedPhysicsTimestamps teleportTimestamps));
|
||||
Assert.Equal(PositionTimestampDisposition.Apply, teleportDisposition);
|
||||
Assert.Equal(Vector3.Zero, teleported.Physics!.Value.Velocity);
|
||||
Assert.True(teleportTimestamps.TeleportAdvanced);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,273 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Tests.World;
|
||||
|
||||
public sealed class LiveEntityPresentationControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public void InitialVisibleObject_DoesNotPlayUnHide()
|
||||
{
|
||||
Fixture fixture = new(PhysicsStateFlags.ReportCollisions);
|
||||
|
||||
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
|
||||
|
||||
Assert.Empty(fixture.TypedPlays);
|
||||
Assert.True(fixture.Entity.IsDrawVisible);
|
||||
Assert.Equal(1, fixture.Shadows.TotalRegistered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HiddenThenVisible_PlaysTypedEffectsMutatesChildrenAndRestoresShadow()
|
||||
{
|
||||
Fixture fixture = new(
|
||||
PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions);
|
||||
|
||||
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
|
||||
|
||||
Assert.Equal(
|
||||
[(fixture.Entity.Id, LiveEntityPresentationController.HiddenScriptType, 1f)],
|
||||
fixture.TypedPlays);
|
||||
Assert.Equal([(Fixture.Guid, true)], fixture.ChildNoDraw);
|
||||
Assert.Equal([Fixture.Guid], fixture.InvalidTargets);
|
||||
Assert.Equal(0, fixture.Shadows.TotalRegistered);
|
||||
Assert.False(fixture.Entity.IsDrawVisible);
|
||||
|
||||
Assert.True(fixture.Runtime.TryApplyState(
|
||||
new SetState.Parsed(Fixture.Guid, 0u, 1, 2),
|
||||
out _,
|
||||
out _));
|
||||
Assert.True(fixture.Controller.OnStateAccepted(Fixture.Guid));
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
(fixture.Entity.Id, LiveEntityPresentationController.HiddenScriptType, 1f),
|
||||
(fixture.Entity.Id, LiveEntityPresentationController.UnHideScriptType, 1f),
|
||||
],
|
||||
fixture.TypedPlays);
|
||||
Assert.Equal([(Fixture.Guid, true), (Fixture.Guid, false)], fixture.ChildNoDraw);
|
||||
Assert.Equal(1, fixture.Shadows.TotalRegistered);
|
||||
Assert.True(fixture.Entity.IsDrawVisible);
|
||||
Assert.Same(fixture.Entity, Assert.Single(fixture.Runtime.WorldEntities).Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IdenticalOrStaleState_DoesNotReplayHiddenEffect()
|
||||
{
|
||||
Fixture fixture = new(PhysicsStateFlags.ReportCollisions);
|
||||
fixture.Controller.OnLiveEntityReady(Fixture.Guid);
|
||||
var hidden = new SetState.Parsed(
|
||||
Fixture.Guid,
|
||||
(uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions),
|
||||
1,
|
||||
2);
|
||||
|
||||
Assert.True(fixture.Runtime.TryApplyState(hidden, out _, out _));
|
||||
fixture.Controller.OnStateAccepted(Fixture.Guid);
|
||||
Assert.False(fixture.Runtime.TryApplyState(hidden, out _, out _));
|
||||
fixture.Controller.OnStateAccepted(Fixture.Guid);
|
||||
|
||||
Assert.Single(fixture.TypedPlays);
|
||||
Assert.Equal(LiveEntityPresentationController.HiddenScriptType,
|
||||
fixture.TypedPlays[0].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnHideWhileLandblockPending_RestoresShadowWhenProjectionReturns()
|
||||
{
|
||||
Fixture fixture = new(PhysicsStateFlags.ReportCollisions);
|
||||
fixture.Controller.OnLiveEntityReady(Fixture.Guid);
|
||||
|
||||
Assert.True(fixture.Runtime.TryApplyState(
|
||||
new SetState.Parsed(
|
||||
Fixture.Guid,
|
||||
(uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions),
|
||||
1,
|
||||
2),
|
||||
out _,
|
||||
out _));
|
||||
fixture.Controller.OnStateAccepted(Fixture.Guid);
|
||||
Assert.Equal(0, fixture.Shadows.TotalRegistered);
|
||||
|
||||
Assert.True(fixture.Runtime.RebucketLiveEntity(Fixture.Guid, 0x02020001u));
|
||||
Assert.False(fixture.Runtime.WorldEntities.ContainsKey(Fixture.Guid));
|
||||
|
||||
Assert.True(fixture.Runtime.TryApplyState(
|
||||
new SetState.Parsed(Fixture.Guid, 0u, 1, 3),
|
||||
out _,
|
||||
out _));
|
||||
fixture.Controller.OnStateAccepted(Fixture.Guid);
|
||||
Assert.Equal(0, fixture.Shadows.TotalRegistered);
|
||||
|
||||
fixture.Spatial.AddLandblock(new LoadedLandblock(
|
||||
0x0202FFFFu,
|
||||
new LandBlock(),
|
||||
Array.Empty<WorldEntity>()));
|
||||
|
||||
Assert.True(fixture.Runtime.WorldEntities.ContainsKey(Fixture.Guid));
|
||||
Assert.Equal(1, fixture.Shadows.TotalRegistered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActivePlacement_IsGenerationScopedAndClearsOnTeardownAndReset()
|
||||
{
|
||||
Fixture fixture = new(PhysicsStateFlags.ReportCollisions);
|
||||
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
|
||||
Assert.True(fixture.Controller.BeginAuthoritativePlacement(Fixture.Guid, 1));
|
||||
Assert.True(fixture.Controller.HasActivePlacement(Fixture.Guid));
|
||||
Assert.True(fixture.Runtime.TryGetRecord(Fixture.Guid, out LiveEntityRecord oldRecord));
|
||||
|
||||
fixture.Controller.Forget(oldRecord);
|
||||
Assert.True(fixture.Runtime.UnregisterLiveEntity(
|
||||
new DeleteObject.Parsed(Fixture.Guid, 1),
|
||||
isLocalPlayer: false));
|
||||
Assert.False(fixture.Controller.HasActivePlacement(Fixture.Guid));
|
||||
|
||||
fixture.Runtime.RegisterLiveEntity(Fixture.Spawn(
|
||||
PhysicsStateFlags.ReportCollisions,
|
||||
instanceSequence: 2));
|
||||
fixture.Runtime.MaterializeLiveEntity(
|
||||
Fixture.Guid,
|
||||
0x01010001u,
|
||||
id => new WorldEntity
|
||||
{
|
||||
Id = id,
|
||||
ServerGuid = Fixture.Guid,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = new Vector3(10f, 10f, 5f),
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
});
|
||||
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
|
||||
|
||||
Assert.False(fixture.Controller.CompleteAuthoritativePlacement(
|
||||
Fixture.Guid,
|
||||
generation: 1,
|
||||
deferShadowRestore: true));
|
||||
Assert.False(fixture.Controller.HasDeferredShadowRestore(Fixture.Guid));
|
||||
Assert.True(fixture.Controller.BeginAuthoritativePlacement(Fixture.Guid, 2));
|
||||
Assert.True(fixture.Controller.HasActivePlacement(Fixture.Guid));
|
||||
|
||||
fixture.Controller.Clear();
|
||||
|
||||
Assert.False(fixture.Controller.HasActivePlacement(Fixture.Guid));
|
||||
Assert.False(fixture.Controller.HasDeferredShadowRestore(Fixture.Guid));
|
||||
}
|
||||
|
||||
private sealed class Fixture
|
||||
{
|
||||
public const uint Guid = 0x70000051u;
|
||||
public List<(uint Owner, uint Type, float Intensity)> TypedPlays { get; } = [];
|
||||
public List<(uint Parent, bool NoDraw)> ChildNoDraw { get; } = [];
|
||||
public List<uint> InvalidTargets { get; } = [];
|
||||
public GpuWorldState Spatial { get; }
|
||||
public LiveEntityRuntime Runtime { get; }
|
||||
public ShadowObjectRegistry Shadows { get; } = new();
|
||||
public WorldEntity Entity { get; }
|
||||
public LiveEntityPresentationController Controller { get; }
|
||||
|
||||
public Fixture(PhysicsStateFlags initialState)
|
||||
{
|
||||
Spatial = new GpuWorldState();
|
||||
Spatial.AddLandblock(new LoadedLandblock(
|
||||
0x0101FFFFu,
|
||||
new LandBlock(),
|
||||
Array.Empty<WorldEntity>()));
|
||||
Runtime = new LiveEntityRuntime(
|
||||
Spatial,
|
||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||
WorldSession.EntitySpawn spawn = Spawn(initialState);
|
||||
Runtime.RegisterLiveEntity(spawn);
|
||||
Entity = Runtime.MaterializeLiveEntity(
|
||||
Guid,
|
||||
0x01010001u,
|
||||
id => new WorldEntity
|
||||
{
|
||||
Id = id,
|
||||
ServerGuid = Guid,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = new Vector3(10f, 10f, 5f),
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
})!;
|
||||
Shadows.Register(
|
||||
Entity.Id,
|
||||
0x01000001u,
|
||||
Entity.Position,
|
||||
Entity.Rotation,
|
||||
1f,
|
||||
0f,
|
||||
0f,
|
||||
0x01010000u,
|
||||
state: (uint)initialState,
|
||||
seedCellId: 0x01010001u);
|
||||
|
||||
Controller = new LiveEntityPresentationController(
|
||||
Runtime,
|
||||
Shadows,
|
||||
(owner, type, intensity) =>
|
||||
{
|
||||
TypedPlays.Add((owner, type, intensity));
|
||||
return true;
|
||||
},
|
||||
(parent, noDraw) => ChildNoDraw.Add((parent, noDraw)),
|
||||
InvalidTargets.Add,
|
||||
() => (1, 1));
|
||||
}
|
||||
|
||||
internal static WorldSession.EntitySpawn Spawn(
|
||||
PhysicsStateFlags state,
|
||||
ushort instanceSequence = 1)
|
||||
{
|
||||
var position = new CreateObject.ServerPosition(
|
||||
0x01010001u, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
|
||||
var timestamps = new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1);
|
||||
var physics = new PhysicsSpawnData(
|
||||
RawState: (uint)state,
|
||||
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(
|
||||
Guid,
|
||||
position,
|
||||
0x02000001u,
|
||||
Array.Empty<CreateObject.AnimPartChange>(),
|
||||
Array.Empty<CreateObject.TextureChange>(),
|
||||
Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||
null,
|
||||
null,
|
||||
"fixture",
|
||||
null,
|
||||
null,
|
||||
MotionTableId: 0x09000001u,
|
||||
PhysicsState: (uint)state,
|
||||
InstanceSequence: instanceSequence,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
PositionSequence: 1,
|
||||
Physics: physics);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,11 @@ public sealed class LiveEntityRuntimeTests
|
|||
|
||||
private sealed class EffectProfile : ILiveEntityEffectProfile { }
|
||||
|
||||
private sealed class RemoteMotionRuntime : ILiveEntityRemoteMotionRuntime
|
||||
{
|
||||
public PhysicsBody Body { get; } = new();
|
||||
}
|
||||
|
||||
private sealed class FailingRegisterResources : ILiveEntityResourceLifecycle
|
||||
{
|
||||
public int RegisterCount { get; private set; }
|
||||
|
|
@ -73,6 +78,8 @@ public sealed class LiveEntityRuntimeTests
|
|||
spawn.Position!.Value.LandblockId,
|
||||
id => Entity(id, spawn.Guid));
|
||||
runtime.SetAnimationRuntime(spawn.Guid, new AnimationRuntime(entity!));
|
||||
var visibilityEdges = new List<bool>();
|
||||
runtime.ProjectionVisibilityChanged += (_, visible) => visibilityEdges.Add(visible);
|
||||
|
||||
Assert.True(registered.LogicalRegistrationCreated);
|
||||
Assert.Equal(1, resources.RegisterCount);
|
||||
|
|
@ -90,6 +97,7 @@ public sealed class LiveEntityRuntimeTests
|
|||
Assert.Single(spatial.Entities);
|
||||
Assert.Equal(1, resources.RegisterCount);
|
||||
Assert.True(runtime.TryGetAnimationRuntime(entity!.Id, out _));
|
||||
Assert.Equal(new[] { false, true }, visibilityEdges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -296,6 +304,7 @@ public sealed class LiveEntityRuntimeTests
|
|||
Assert.True(runtime.TryGetWorldEntity(spawn.Guid, out WorldEntity resolved));
|
||||
Assert.Same(attached, resolved);
|
||||
Assert.Equal(1, resources.RegisterCount);
|
||||
attached.IsAncestorDrawVisible = false;
|
||||
|
||||
WorldEntity same = runtime.MaterializeLiveEntity(
|
||||
spawn.Guid,
|
||||
|
|
@ -304,6 +313,7 @@ public sealed class LiveEntityRuntimeTests
|
|||
LiveEntityProjectionKind.World)!;
|
||||
|
||||
Assert.Same(attached, same);
|
||||
Assert.True(same.IsAncestorDrawVisible);
|
||||
Assert.Same(attached, Assert.Single(runtime.WorldEntities).Value);
|
||||
Assert.True(runtime.TryMarkWorldSpawnPublished(spawn.Guid));
|
||||
Assert.False(runtime.TryMarkWorldSpawnPublished(spawn.Guid));
|
||||
|
|
@ -500,6 +510,210 @@ public sealed class LiveEntityRuntimeTests
|
|||
Assert.True(runtime.ShouldAdvanceRootRuntime(guid));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhysicsStateAndRemoteBodyStaySynchronizedAcrossEitherArrivalOrder()
|
||||
{
|
||||
const uint stateBeforeBindGuid = 0x70000037u;
|
||||
const uint bindBeforeStateGuid = 0x70000038u;
|
||||
var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources());
|
||||
runtime.RegisterLiveEntity(Spawn(stateBeforeBindGuid, 1, 1, 0x01010001u));
|
||||
runtime.RegisterLiveEntity(Spawn(bindBeforeStateGuid, 1, 1, 0x01010001u));
|
||||
|
||||
PhysicsStateFlags firstState = PhysicsStateFlags.Hidden
|
||||
| PhysicsStateFlags.Gravity
|
||||
| PhysicsStateFlags.ReportCollisions;
|
||||
Assert.True(runtime.TryApplyState(
|
||||
new SetState.Parsed(stateBeforeBindGuid, (uint)firstState, 1, 2),
|
||||
out _));
|
||||
var lateBody = new RemoteMotionRuntime();
|
||||
runtime.SetRemoteMotionRuntime(stateBeforeBindGuid, lateBody);
|
||||
|
||||
var earlyBody = new RemoteMotionRuntime();
|
||||
runtime.SetRemoteMotionRuntime(bindBeforeStateGuid, earlyBody);
|
||||
PhysicsStateFlags secondState = PhysicsStateFlags.Static
|
||||
| PhysicsStateFlags.Ethereal
|
||||
| PhysicsStateFlags.NoDraw;
|
||||
Assert.True(runtime.TryApplyState(
|
||||
new SetState.Parsed(bindBeforeStateGuid, (uint)secondState, 1, 2),
|
||||
out _));
|
||||
|
||||
Assert.Equal((firstState & ~PhysicsStateFlags.ReportCollisions)
|
||||
| PhysicsStateFlags.IgnoreCollisions,
|
||||
lateBody.Body.State);
|
||||
Assert.Equal(secondState, earlyBody.Body.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParentDrivenChildNoDrawKeepsBoundBodyInCanonicalState()
|
||||
{
|
||||
const uint guid = 0x70000039u;
|
||||
var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources());
|
||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
||||
var remote = new RemoteMotionRuntime();
|
||||
runtime.SetRemoteMotionRuntime(guid, remote);
|
||||
|
||||
Assert.True(runtime.SetAttachedChildNoDraw(guid, true));
|
||||
Assert.True(remote.Body.State.HasFlag(PhysicsStateFlags.NoDraw));
|
||||
|
||||
Assert.True(runtime.SetAttachedChildNoDraw(guid, false));
|
||||
Assert.False(remote.Body.State.HasFlag(PhysicsStateFlags.NoDraw));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitiallyVisibleCreate_DoesNotManufactureUnHideTransition()
|
||||
{
|
||||
const uint guid = 0x70000040u;
|
||||
var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources());
|
||||
|
||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
||||
|
||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
||||
Assert.True(record.TryDequeueStateTransition(out RetailPhysicsStateTransition transition));
|
||||
Assert.Equal(RetailHiddenTransition.None, transition.HiddenTransition);
|
||||
Assert.Equal(PhysicsStateFlags.ReportCollisions, record.FinalPhysicsState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HiddenCreate_SuppressesMeshInteractionButRetainsNarrowObjectTick()
|
||||
{
|
||||
const uint guid = 0x70000041u;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||
runtime.RegisterLiveEntity(Spawn(
|
||||
guid,
|
||||
1,
|
||||
1,
|
||||
0x01010001u,
|
||||
PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions));
|
||||
WorldEntity entity = runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid))!;
|
||||
|
||||
Assert.False(entity.IsDrawVisible);
|
||||
Assert.Empty(runtime.WorldEntities);
|
||||
Assert.Same(entity, Assert.Single(runtime.MaterializedWorldEntities).Value);
|
||||
Assert.Single(spatial.Entities);
|
||||
Assert.True(runtime.ShouldAdvanceRootRuntime(guid));
|
||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
||||
Assert.Equal(PhysicsStateFlags.None,
|
||||
record.FinalPhysicsState & PhysicsStateFlags.ReportCollisions);
|
||||
Assert.NotEqual(PhysicsStateFlags.None,
|
||||
record.FinalPhysicsState & PhysicsStateFlags.IgnoreCollisions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HiddenThenVisibleState_RestoresSameMeshAndInteractionIdentity()
|
||||
{
|
||||
const uint guid = 0x70000042u;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
||||
WorldEntity entity = runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid))!;
|
||||
|
||||
Assert.True(runtime.TryApplyState(new SetState.Parsed(
|
||||
guid,
|
||||
(uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions),
|
||||
1,
|
||||
2), out _, out RetailPhysicsStateTransition hidden));
|
||||
Assert.Equal(RetailHiddenTransition.BecameHidden, hidden.HiddenTransition);
|
||||
Assert.False(entity.IsDrawVisible);
|
||||
Assert.Empty(runtime.WorldEntities);
|
||||
Assert.False(runtime.TryGetInteractionEligibleEntity(guid, out _));
|
||||
|
||||
Assert.True(runtime.TryApplyState(new SetState.Parsed(
|
||||
guid,
|
||||
0u,
|
||||
1,
|
||||
3), out _, out RetailPhysicsStateTransition visible));
|
||||
Assert.Equal(RetailHiddenTransition.BecameVisible, visible.HiddenTransition);
|
||||
Assert.True(entity.IsDrawVisible);
|
||||
Assert.True(runtime.TryGetInteractionEligibleEntity(guid, out var eligible));
|
||||
Assert.Same(entity, eligible);
|
||||
Assert.Same(entity, Assert.Single(runtime.WorldEntities).Value);
|
||||
Assert.Same(entity, Assert.Single(spatial.Entities));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositionAfterPickup_RequiresTeleportHookEvenWithEqualTeleportStamp()
|
||||
{
|
||||
const uint guid = 0x70000043u;
|
||||
var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources());
|
||||
WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u);
|
||||
runtime.RegisterLiveEntity(spawn);
|
||||
Assert.True(runtime.TryApplyPickup(
|
||||
new PickupEvent.Parsed(guid, 1, 2),
|
||||
out _));
|
||||
|
||||
var update = new WorldSession.EntityPositionUpdate(
|
||||
guid,
|
||||
new CreateObject.ServerPosition(
|
||||
0x01010002u, 12f, 13f, 5f, 1f, 0f, 0f, 0f),
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
1,
|
||||
3,
|
||||
0,
|
||||
0);
|
||||
|
||||
Assert.True(runtime.TryApplyPosition(
|
||||
update,
|
||||
isLocalPlayer: false,
|
||||
forcePositionRotation: null,
|
||||
currentLocalVelocity: null,
|
||||
out PositionTimestampDisposition disposition,
|
||||
out _,
|
||||
out AcceptedPhysicsTimestamps timestamps));
|
||||
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
||||
Assert.False(timestamps.TeleportAdvanced);
|
||||
Assert.True(timestamps.TeleportHookRequired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositionFromPendingProjection_RequiresTeleportHookWithEqualTeleportStamp()
|
||||
{
|
||||
const uint guid = 0x70000044u;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||
WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u);
|
||||
runtime.RegisterLiveEntity(spawn);
|
||||
runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid));
|
||||
spatial.RemoveLandblock(0x0101FFFFu);
|
||||
|
||||
Assert.True(runtime.TryApplyPosition(
|
||||
new WorldSession.EntityPositionUpdate(
|
||||
guid,
|
||||
new CreateObject.ServerPosition(
|
||||
0x01010002u, 12f, 13f, 5f, 1f, 0f, 0f, 0f),
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0),
|
||||
isLocalPlayer: false,
|
||||
forcePositionRotation: null,
|
||||
currentLocalVelocity: null,
|
||||
out PositionTimestampDisposition disposition,
|
||||
out _,
|
||||
out AcceptedPhysicsTimestamps timestamps));
|
||||
|
||||
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
||||
Assert.False(timestamps.TeleportAdvanced);
|
||||
Assert.True(timestamps.TeleportHookRequired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LandblockUnload_HidesTopLevelViewAndReloadRestoresSameIdentity()
|
||||
{
|
||||
|
|
@ -792,14 +1006,15 @@ public sealed class LiveEntityRuntimeTests
|
|||
uint guid,
|
||||
ushort instance,
|
||||
ushort positionSequence,
|
||||
uint cell)
|
||||
uint cell,
|
||||
PhysicsStateFlags state = PhysicsStateFlags.ReportCollisions)
|
||||
{
|
||||
var position = new CreateObject.ServerPosition(
|
||||
cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
|
||||
var timestamps = new PhysicsTimestamps(
|
||||
positionSequence, 1, 1, 1, 0, 1, 0, 1, instance);
|
||||
var physics = new PhysicsSpawnData(
|
||||
RawState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||
RawState: (uint)state,
|
||||
Position: position,
|
||||
Movement: null,
|
||||
AnimationFrame: null,
|
||||
|
|
@ -832,7 +1047,7 @@ public sealed class LiveEntityRuntimeTests
|
|||
null,
|
||||
null,
|
||||
0x09000001u,
|
||||
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||
PhysicsState: (uint)state,
|
||||
InstanceSequence: instance,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
|
|
|
|||
|
|
@ -145,4 +145,62 @@ public class HandleAllCollisionsTests
|
|||
// dot = -5 < 0 → k = -(-5*1.05) = 5.25 → v.z = -5 + 5.25 = 0.25 (tiny bounce)
|
||||
Assert.Equal(0.25f, b.Velocity.Z, precision: 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetPositionTransition_HiddenLandingCallbackRunsBeforeCollisionResponse()
|
||||
{
|
||||
var body = Airborne(Vector3.Zero);
|
||||
int hitGroundCalls = 0;
|
||||
|
||||
PhysicsObjUpdate.CommitSetPositionTransition(
|
||||
body,
|
||||
inContact: true,
|
||||
onWalkable: true,
|
||||
collisionNormalValid: true,
|
||||
collisionNormal: Vector3.UnitZ,
|
||||
previousContact: false,
|
||||
previousOnWalkable: false,
|
||||
hitGround: () =>
|
||||
{
|
||||
hitGroundCalls++;
|
||||
body.Velocity = new Vector3(0f, 0f, -4f);
|
||||
});
|
||||
|
||||
Assert.Equal(1, hitGroundCalls);
|
||||
Assert.True(body.InContact);
|
||||
Assert.True(body.OnWalkable);
|
||||
// Callback -4 then retail elasticity 0.05 reflection => +0.2.
|
||||
// Running the callback after collision response would leave -4.
|
||||
Assert.Equal(0.2f, body.Velocity.Z, precision: 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetPositionTransition_HiddenWalkOffCallbackRunsBeforeCollisionResponse()
|
||||
{
|
||||
var body = Airborne(Vector3.Zero);
|
||||
body.TransientState = TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable;
|
||||
int leaveGroundCalls = 0;
|
||||
|
||||
PhysicsObjUpdate.CommitSetPositionTransition(
|
||||
body,
|
||||
inContact: false,
|
||||
onWalkable: false,
|
||||
collisionNormalValid: true,
|
||||
collisionNormal: -Vector3.UnitX,
|
||||
previousContact: true,
|
||||
previousOnWalkable: true,
|
||||
leaveGround: () =>
|
||||
{
|
||||
leaveGroundCalls++;
|
||||
body.Velocity = new Vector3(3f, 0f, 0f);
|
||||
});
|
||||
|
||||
Assert.Equal(1, leaveGroundCalls);
|
||||
Assert.False(body.InContact);
|
||||
Assert.False(body.OnWalkable);
|
||||
// Callback +3 into a -X normal then reflection => -0.15.
|
||||
// Running the callback after response would leave +3.
|
||||
Assert.Equal(-0.15f, body.Velocity.X, precision: 3);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ public sealed class InitialPlacementOverlapTests
|
|||
movingEntityId: PlayerId);
|
||||
|
||||
Assert.True(result.Ok);
|
||||
Assert.True(result.InContact);
|
||||
Assert.True(result.OnWalkable);
|
||||
Assert.Equal(Cell, result.ContactPlaneCellId);
|
||||
Assert.NotEqual(savedFeet, result.Position);
|
||||
float centerDistance = Vector3.Distance(
|
||||
result.Position + new Vector3(0f, 0f, Radius),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
public sealed class RetailPhysicsStateTransitionTests
|
||||
{
|
||||
[Fact]
|
||||
public void ConstructorState_MatchesRetailAndIsNotHidden()
|
||||
{
|
||||
Assert.Equal(0x00400C08u, (uint)RetailPhysicsStateTransitions.ConstructorState);
|
||||
Assert.Equal(
|
||||
PhysicsStateFlags.None,
|
||||
RetailPhysicsStateTransitions.ConstructorState & PhysicsStateFlags.Hidden);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VisibleToHidden_PlaysHiddenShapeAndRewritesCollisionBits()
|
||||
{
|
||||
PhysicsStateFlags requested = PhysicsStateFlags.Hidden
|
||||
| PhysicsStateFlags.ReportCollisions
|
||||
| PhysicsStateFlags.Lighting;
|
||||
|
||||
RetailPhysicsStateTransition result = RetailPhysicsStateTransitions.Apply(
|
||||
RetailPhysicsStateTransitions.ConstructorState,
|
||||
requested);
|
||||
|
||||
Assert.Equal(RetailHiddenTransition.BecameHidden, result.HiddenTransition);
|
||||
Assert.False(result.LightingChanged);
|
||||
Assert.Equal(PhysicsStateFlags.None, result.FinalState & PhysicsStateFlags.ReportCollisions);
|
||||
Assert.NotEqual(PhysicsStateFlags.None, result.FinalState & PhysicsStateFlags.IgnoreCollisions);
|
||||
Assert.NotEqual(PhysicsStateFlags.None, result.FinalState & PhysicsStateFlags.Hidden);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HiddenToVisible_RestoresReportCollisionsEvenWhenWireOmitsIt()
|
||||
{
|
||||
PhysicsStateFlags hidden = PhysicsStateFlags.Hidden
|
||||
| PhysicsStateFlags.IgnoreCollisions;
|
||||
|
||||
RetailPhysicsStateTransition result = RetailPhysicsStateTransitions.Apply(
|
||||
hidden,
|
||||
PhysicsStateFlags.None);
|
||||
|
||||
Assert.Equal(RetailHiddenTransition.BecameVisible, result.HiddenTransition);
|
||||
Assert.Equal(PhysicsStateFlags.None, result.FinalState & PhysicsStateFlags.Hidden);
|
||||
Assert.Equal(PhysicsStateFlags.None, result.FinalState & PhysicsStateFlags.IgnoreCollisions);
|
||||
Assert.NotEqual(PhysicsStateFlags.None, result.FinalState & PhysicsStateFlags.ReportCollisions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IdenticalHiddenState_DoesNotReplayTransition()
|
||||
{
|
||||
PhysicsStateFlags state = PhysicsStateFlags.Hidden
|
||||
| PhysicsStateFlags.IgnoreCollisions;
|
||||
|
||||
RetailPhysicsStateTransition result = RetailPhysicsStateTransitions.Apply(state, state);
|
||||
|
||||
Assert.Equal(RetailHiddenTransition.None, result.HiddenTransition);
|
||||
Assert.False(result.LightingChanged);
|
||||
Assert.False(result.NoDrawChanged);
|
||||
Assert.Equal(state, result.FinalState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LightingNoDrawAndHiddenChangeFlags_AreDerivedFromLowWord()
|
||||
{
|
||||
RetailPhysicsStateTransition result = RetailPhysicsStateTransitions.Apply(
|
||||
PhysicsStateFlags.None,
|
||||
PhysicsStateFlags.Lighting
|
||||
| PhysicsStateFlags.NoDraw
|
||||
| PhysicsStateFlags.Hidden);
|
||||
|
||||
Assert.True(result.LightingChanged);
|
||||
Assert.True(result.NoDrawChanged);
|
||||
Assert.Equal(RetailHiddenTransition.BecameHidden, result.HiddenTransition);
|
||||
}
|
||||
}
|
||||
|
|
@ -102,6 +102,46 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
Assert.Empty(result.ToDraw);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WalkEntities_NoDrawHiddenOrSuppressedAncestor_SkipsMeshWithoutRemovingEntity()
|
||||
{
|
||||
WorldEntity suppressed = MakeEntityWithMesh(7u, Vector3.Zero);
|
||||
suppressed.IsDrawVisible = false;
|
||||
var entities = new[] { suppressed };
|
||||
var entries = new[]
|
||||
{
|
||||
new WbDrawDispatcher.LandblockEntry(
|
||||
0xAAAA_FFFFu,
|
||||
new Vector3(-10f),
|
||||
new Vector3(10f),
|
||||
entities,
|
||||
BuildById(entities)),
|
||||
};
|
||||
|
||||
var result = WbDrawDispatcher.WalkEntities(
|
||||
entries,
|
||||
frustum: null,
|
||||
neverCullLandblockId: null,
|
||||
visibleCellIds: null,
|
||||
animatedEntityIds: new HashSet<uint> { suppressed.Id });
|
||||
|
||||
Assert.Equal(0, result.EntitiesWalked);
|
||||
Assert.Empty(result.ToDraw);
|
||||
Assert.Same(suppressed, Assert.Single(entries[0].Entities));
|
||||
|
||||
suppressed.IsDrawVisible = true;
|
||||
suppressed.IsAncestorDrawVisible = false;
|
||||
result = WbDrawDispatcher.WalkEntities(
|
||||
entries,
|
||||
frustum: null,
|
||||
neverCullLandblockId: null,
|
||||
visibleCellIds: null,
|
||||
animatedEntityIds: new HashSet<uint> { suppressed.Id });
|
||||
|
||||
Assert.Equal(0, result.EntitiesWalked);
|
||||
Assert.Empty(result.ToDraw);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WalkEntities_InvisibleLb_AnimatedSet_WalksOnlyAnimatedEntities()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue