refactor(runtime): extract the live object frame
This commit is contained in:
parent
99a3e819c4
commit
4e4aac2c5a
27 changed files with 1217 additions and 371 deletions
|
|
@ -47,6 +47,10 @@ public sealed class GameWindowLiveEntityCompositionTests
|
|||
[InlineData("DispatchRemoteInboundMotion")]
|
||||
[InlineData("CreateRemoteMotion")]
|
||||
[InlineData("WillAdvanceRemoteMotion")]
|
||||
[InlineData("AdvanceLiveObjectRuntime")]
|
||||
[InlineData("AdvanceLiveObjectRuntimeCore")]
|
||||
[InlineData("ReconcileLiveObjectSpatialPresentation")]
|
||||
[InlineData("CaptureAnimationHooks")]
|
||||
public void GameWindow_DoesNotReacquireExtractedLiveEntityBodies(string methodName)
|
||||
{
|
||||
Assert.Null(typeof(GameWindow).GetMethod(methodName, PrivateImplementation));
|
||||
|
|
@ -73,6 +77,13 @@ public sealed class GameWindowLiveEntityCompositionTests
|
|||
[InlineData(typeof(LiveEntityMotionRuntimeController))]
|
||||
[InlineData(typeof(LiveEntityInboundAuthorityGate))]
|
||||
[InlineData(typeof(DeferredLiveEntityMotionRuntimeBindings))]
|
||||
[InlineData(typeof(AcDream.App.Update.LiveObjectFrameController))]
|
||||
[InlineData(typeof(AcDream.App.Update.LiveEffectFrameController))]
|
||||
[InlineData(typeof(AcDream.App.Update.LiveSpatialPresentationReconciler))]
|
||||
[InlineData(typeof(LiveEntityAnimationPresenter))]
|
||||
[InlineData(typeof(LiveAnimationPresentationContext))]
|
||||
[InlineData(typeof(StaticLiveRootCommitter))]
|
||||
[InlineData(typeof(RetailLiveFrameCoordinator))]
|
||||
public void ExtractedHelpers_DoNotOwnGuidIndexesOrBackendState(Type helperType)
|
||||
{
|
||||
foreach (FieldInfo field in helperType.GetFields(
|
||||
|
|
@ -83,6 +94,89 @@ public sealed class GameWindowLiveEntityCompositionTests
|
|||
string typeName = field.FieldType.FullName ?? field.FieldType.Name;
|
||||
Assert.DoesNotContain("Silk.NET", typeName, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("OpenGL", typeName, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.NotEqual(typeof(GameWindow), field.FieldType);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractedUpdateOwners_DoNotRetainAnonymousCallbacks()
|
||||
{
|
||||
Type[] owners =
|
||||
[
|
||||
typeof(AcDream.App.Update.LiveObjectFrameController),
|
||||
typeof(AcDream.App.Update.LiveEffectFrameController),
|
||||
typeof(AcDream.App.Update.LiveSpatialPresentationReconciler),
|
||||
typeof(LiveEntityAnimationScheduler),
|
||||
typeof(LiveEntityAnimationPresenter),
|
||||
typeof(LiveAnimationPresentationContext),
|
||||
typeof(StaticLiveRootCommitter),
|
||||
typeof(RetailLiveFrameCoordinator),
|
||||
];
|
||||
|
||||
foreach (Type owner in owners)
|
||||
{
|
||||
Assert.DoesNotContain(
|
||||
owner.GetFields(BindingFlags.Instance | BindingFlags.NonPublic),
|
||||
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||
}
|
||||
|
||||
Assert.False(typeof(ILiveAnimationPresentationContext).IsAssignableFrom(
|
||||
typeof(GameWindow)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimeViewsAndProjectileController_RetainTypedSourcesNotWindowClosures()
|
||||
{
|
||||
FieldInfo animationRuntime = Assert.Single(
|
||||
typeof(LiveEntityAnimationRuntimeView<LiveEntityAnimationState>)
|
||||
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic),
|
||||
field => field.Name == "_runtime");
|
||||
FieldInfo remoteRuntime = Assert.Single(
|
||||
typeof(LiveEntityRemoteMotionRuntimeView<RemoteMotion>)
|
||||
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic),
|
||||
field => field.Name == "_runtime");
|
||||
Assert.Equal(typeof(ILiveEntityRuntimeSource), animationRuntime.FieldType);
|
||||
Assert.Equal(typeof(ILiveEntityRuntimeSource), remoteRuntime.FieldType);
|
||||
|
||||
FieldInfo[] projectileFields = typeof(ProjectileController).GetFields(
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.Equal(
|
||||
typeof(IProjectileSetupResolver),
|
||||
Assert.Single(projectileFields, field => field.Name == "_setupResolver").FieldType);
|
||||
Assert.Equal(
|
||||
typeof(IEntityRootPosePublisher),
|
||||
Assert.Single(projectileFields, field => field.Name == "_rootPoses").FieldType);
|
||||
Assert.Equal(
|
||||
typeof(LiveWorldOriginState),
|
||||
Assert.Single(projectileFields, field => field.Name == "_origin").FieldType);
|
||||
Assert.DoesNotContain(
|
||||
projectileFields,
|
||||
field => typeof(Delegate).IsAssignableFrom(field.FieldType)
|
||||
&& !field.Name.Contains("DiagnosticSink", StringComparison.Ordinal));
|
||||
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs"));
|
||||
Assert.DoesNotContain(
|
||||
"new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(() =>",
|
||||
source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(() =>",
|
||||
source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("new AcDream.App.Physics.DatProjectileSetupResolver", source);
|
||||
}
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||
while (directory is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||
return directory.FullName;
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -202,10 +202,7 @@ public sealed class LiveEntityProjectionWithdrawalControllerTests
|
|||
lifecycle);
|
||||
Projectiles = new ProjectileController(
|
||||
Live,
|
||||
Physics,
|
||||
_ => null,
|
||||
_ => { },
|
||||
() => (0, 0));
|
||||
Physics);
|
||||
Controller = new LiveEntityProjectionWithdrawalController(
|
||||
Live,
|
||||
Projectiles,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,20 @@ namespace AcDream.App.Tests.World;
|
|||
|
||||
public sealed class LiveEntityRuntimeTests
|
||||
{
|
||||
[Fact]
|
||||
public void RuntimeSlot_BindsExactlyOnceWithoutOwningEntityState()
|
||||
{
|
||||
var spatial = new GpuWorldState();
|
||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||
var slot = new LiveEntityRuntimeSlot();
|
||||
|
||||
Assert.Null(slot.Current);
|
||||
slot.Bind(runtime);
|
||||
|
||||
Assert.Same(runtime, slot.Current);
|
||||
Assert.Throws<InvalidOperationException>(() => slot.Bind(runtime));
|
||||
}
|
||||
|
||||
private sealed class RecordingResources : ILiveEntityResourceLifecycle
|
||||
{
|
||||
public int RegisterCount { get; private set; }
|
||||
|
|
@ -245,11 +259,18 @@ public sealed class LiveEntityRuntimeTests
|
|||
landblockId: 0x01010000u,
|
||||
seedCellId: 0x01010001u);
|
||||
int poseUpdates = 0;
|
||||
var runtimeSlot = new LiveEntityRuntimeSlot();
|
||||
runtimeSlot.Bind(runtime);
|
||||
var origin = new LiveWorldOriginState();
|
||||
origin.Recenter(1, 1);
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
poses.PublishMeshRefs(entity);
|
||||
poses.EffectPoseChanged += _ => poseUpdates++;
|
||||
var committer = new StaticLiveRootCommitter(
|
||||
() => runtime,
|
||||
runtimeSlot,
|
||||
shadows,
|
||||
() => (1, 1),
|
||||
_ => poseUpdates++);
|
||||
origin,
|
||||
poses);
|
||||
|
||||
body.SetFrameInCurrentCell(
|
||||
body.Position,
|
||||
|
|
@ -282,13 +303,14 @@ public sealed class LiveEntityRuntimeTests
|
|||
[Fact]
|
||||
public void StaticRootCommit_BeforeLiveRuntimeCompositionIsSafeNoOp()
|
||||
{
|
||||
LiveEntityRuntime? runtime = null;
|
||||
int poseUpdates = 0;
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
poses.EffectPoseChanged += _ => poseUpdates++;
|
||||
var committer = new StaticLiveRootCommitter(
|
||||
() => runtime,
|
||||
new LiveEntityRuntimeSlot(),
|
||||
new ShadowObjectRegistry(),
|
||||
() => (0, 0),
|
||||
_ => poseUpdates++);
|
||||
new LiveWorldOriginState(),
|
||||
poses);
|
||||
|
||||
Assert.False(committer.Commit(
|
||||
Entity(0x7F000001u, 0x70000001u),
|
||||
|
|
@ -347,26 +369,33 @@ public sealed class LiveEntityRuntimeTests
|
|||
worldOffsetY: 0f,
|
||||
landblockId: 0x01010000u,
|
||||
seedCellId: 0x01010001u);
|
||||
var runtimeSlot = new LiveEntityRuntimeSlot();
|
||||
runtimeSlot.Bind(runtime);
|
||||
var origin = new LiveWorldOriginState();
|
||||
origin.Recenter(1, 1);
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
poses.PublishMeshRefs(oldEntity);
|
||||
poses.EffectPoseChanged += _ =>
|
||||
{
|
||||
Assert.True(shadows.Suspend(oldEntity.Id));
|
||||
Assert.True(runtime.UnregisterLiveEntity(
|
||||
new DeleteObject.Parsed(guid, InstanceSequence: 1),
|
||||
isLocalPlayer: false));
|
||||
runtime.RegisterLiveEntity(Spawn(
|
||||
guid,
|
||||
instance: 2,
|
||||
positionSequence: 1,
|
||||
cell: 0x01010001u));
|
||||
runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid));
|
||||
};
|
||||
var committer = new StaticLiveRootCommitter(
|
||||
() => runtime,
|
||||
runtimeSlot,
|
||||
shadows,
|
||||
() => (1, 1),
|
||||
_ =>
|
||||
{
|
||||
Assert.True(shadows.Suspend(oldEntity.Id));
|
||||
Assert.True(runtime.UnregisterLiveEntity(
|
||||
new DeleteObject.Parsed(guid, InstanceSequence: 1),
|
||||
isLocalPlayer: false));
|
||||
runtime.RegisterLiveEntity(Spawn(
|
||||
guid,
|
||||
instance: 2,
|
||||
positionSequence: 1,
|
||||
cell: 0x01010001u));
|
||||
runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid));
|
||||
});
|
||||
origin,
|
||||
poses);
|
||||
|
||||
oldBody.SetFrameInCurrentCell(
|
||||
oldBody.Position,
|
||||
|
|
@ -453,7 +482,7 @@ public sealed class LiveEntityRuntimeTests
|
|||
id => Entity(id, guid))!;
|
||||
var animation = new AnimationRuntime(entity);
|
||||
runtime.SetAnimationRuntime(guid, animation);
|
||||
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(() => runtime);
|
||||
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(BoundSlot(runtime));
|
||||
|
||||
Assert.Equal(1, view.Count);
|
||||
Assert.Equal(entity.Id, Assert.Single(view.Keys));
|
||||
|
|
@ -489,7 +518,7 @@ public sealed class LiveEntityRuntimeTests
|
|||
id => Entity(id, guid))!;
|
||||
runtime.SetAnimationRuntime(guid, new AnimationRuntime(entity));
|
||||
}
|
||||
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(() => runtime);
|
||||
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(BoundSlot(runtime));
|
||||
var visited = new List<uint>();
|
||||
|
||||
foreach (KeyValuePair<uint, AnimationRuntime> pair in view)
|
||||
|
|
@ -519,7 +548,7 @@ public sealed class LiveEntityRuntimeTests
|
|||
id => Entity(id, guid))!;
|
||||
runtime.SetAnimationRuntime(guid, new AnimationRuntime(entity));
|
||||
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(
|
||||
() => runtime);
|
||||
BoundSlot(runtime));
|
||||
var ids = new HashSet<uint>();
|
||||
|
||||
view.CopySpatialIdsTo(ids);
|
||||
|
|
@ -2803,6 +2832,13 @@ public sealed class LiveEntityRuntimeTests
|
|||
private static LoadedLandblock EmptyLandblock(uint canonicalId) =>
|
||||
new(canonicalId, new LandBlock(), Array.Empty<WorldEntity>());
|
||||
|
||||
private static LiveEntityRuntimeSlot BoundSlot(LiveEntityRuntime runtime)
|
||||
{
|
||||
var slot = new LiveEntityRuntimeSlot();
|
||||
slot.Bind(runtime);
|
||||
return slot;
|
||||
}
|
||||
|
||||
private static void ResolveParent(LiveEntityRuntime runtime, uint childGuid) =>
|
||||
runtime.ParentAttachments.Resolve(
|
||||
childGuid,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Update;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
using Silk.NET.Input;
|
||||
|
||||
namespace AcDream.App.Tests.World;
|
||||
|
||||
public sealed class LiveObjectFrameControllerTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(ParticleRange.Retail, 1f)]
|
||||
[InlineData(
|
||||
ParticleRange.Extended,
|
||||
ParticleVisibilityController.ExtendedRangeMultiplier)]
|
||||
public void ParticleRange_NullSettingsUsesConfiguredFallback(
|
||||
ParticleRange fallback,
|
||||
float expected)
|
||||
{
|
||||
var source = new SettingsParticleRangeSource(null, fallback);
|
||||
|
||||
Assert.Equal(expected, source.RangeMultiplier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParticleRange_LiveSettingsDraftOverridesFallbackAndUpdatesImmediately()
|
||||
{
|
||||
SettingsVM settings = CreateSettings();
|
||||
var source = new SettingsParticleRangeSource(settings, ParticleRange.Retail);
|
||||
|
||||
settings.SetDisplay(settings.DisplayDraft with { ParticleRange = ParticleRange.Retail });
|
||||
Assert.Equal(1f, source.RangeMultiplier);
|
||||
|
||||
settings.SetDisplay(settings.DisplayDraft with { ParticleRange = ParticleRange.Extended });
|
||||
Assert.Equal(
|
||||
ParticleVisibilityController.ExtendedRangeMultiplier,
|
||||
source.RangeMultiplier);
|
||||
}
|
||||
|
||||
private static SettingsVM CreateSettings()
|
||||
{
|
||||
var dispatcher = new InputDispatcher(
|
||||
new NullKeyboardSource(),
|
||||
new NullMouseSource(),
|
||||
new KeyBindings());
|
||||
return new SettingsVM(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { },
|
||||
DisplaySettings.Default,
|
||||
static _ => { },
|
||||
AudioSettings.Default,
|
||||
static _ => { },
|
||||
GameplaySettings.Default,
|
||||
static _ => { },
|
||||
ChatSettings.Default,
|
||||
static _ => { },
|
||||
CharacterSettings.Default,
|
||||
static _ => { });
|
||||
}
|
||||
|
||||
private sealed class NullKeyboardSource : IKeyboardSource
|
||||
{
|
||||
#pragma warning disable CS0067
|
||||
public event Action<Key, ModifierMask>? KeyDown;
|
||||
public event Action<Key, ModifierMask>? KeyUp;
|
||||
#pragma warning restore CS0067
|
||||
public bool IsHeld(Key key) => false;
|
||||
public ModifierMask CurrentModifiers => ModifierMask.None;
|
||||
}
|
||||
|
||||
private sealed class NullMouseSource : IMouseSource
|
||||
{
|
||||
#pragma warning disable CS0067
|
||||
public event Action<MouseButton, ModifierMask>? MouseDown;
|
||||
public event Action<MouseButton, ModifierMask>? MouseUp;
|
||||
public event Action<float, float>? MouseMove;
|
||||
public event Action<float>? Scroll;
|
||||
#pragma warning restore CS0067
|
||||
public bool IsHeld(MouseButton button) => false;
|
||||
public bool WantCaptureMouse => false;
|
||||
public bool WantCaptureKeyboard => false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.Update;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Content.Vfx;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Options;
|
||||
|
|
@ -25,10 +27,11 @@ public sealed class RecallTeleportAnimationTests
|
|||
{
|
||||
var calls = new List<string>();
|
||||
var frame = new RetailLiveFrameCoordinator(
|
||||
_ => calls.Add("objects"),
|
||||
() => calls.Add("network"),
|
||||
() => calls.Add("commands"),
|
||||
() => calls.Add("spatial"));
|
||||
new TestLiveObjectFramePhase(_ => calls.Add("objects")),
|
||||
new GpuWorldState(),
|
||||
new TestLiveSessionFramePhase(() => calls.Add("network")),
|
||||
new TestPostNetworkCommandFramePhase(() => calls.Add("commands")),
|
||||
new TestLiveSpatialReconcilePhase(() => calls.Add("spatial")));
|
||||
|
||||
frame.Tick(1f / 60f);
|
||||
|
||||
|
|
@ -39,25 +42,96 @@ public sealed class RecallTeleportAnimationTests
|
|||
public void LiveFrame_InvalidHostDeltaCannotBlockNetworkDispatch()
|
||||
{
|
||||
var deltas = new List<float>();
|
||||
int networkDispatches = 0;
|
||||
var calls = new List<string>();
|
||||
var frame = new RetailLiveFrameCoordinator(
|
||||
deltas.Add,
|
||||
() => networkDispatches++,
|
||||
() => { },
|
||||
() => { });
|
||||
new TestLiveObjectFramePhase(delta =>
|
||||
{
|
||||
deltas.Add(delta);
|
||||
calls.Add("objects");
|
||||
}),
|
||||
new GpuWorldState(),
|
||||
new TestLiveSessionFramePhase(() => calls.Add("network")),
|
||||
new TestPostNetworkCommandFramePhase(() => calls.Add("commands")),
|
||||
new TestLiveSpatialReconcilePhase(() => calls.Add("spatial")));
|
||||
|
||||
frame.Tick(float.NaN);
|
||||
frame.Tick(float.PositiveInfinity);
|
||||
frame.Tick(-1f);
|
||||
|
||||
Assert.Equal([0f, 0f, 0f], deltas);
|
||||
Assert.Equal(3, networkDispatches);
|
||||
Assert.Equal(
|
||||
Enumerable.Repeat(new[] { "objects", "network", "commands", "spatial" }, 3)
|
||||
.SelectMany(static frameCalls => frameCalls),
|
||||
calls);
|
||||
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.NaN));
|
||||
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.NegativeInfinity));
|
||||
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.PositiveInfinity));
|
||||
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.MaxValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LiveFrame_CommitsTheInboundMutationBatchBeforeCommandsAndReconcile()
|
||||
{
|
||||
const uint landblock = 0x0101FFFFu;
|
||||
var calls = new List<string>();
|
||||
var world = new GpuWorldState();
|
||||
long before = world.VisibilityCommitCount;
|
||||
var frame = new RetailLiveFrameCoordinator(
|
||||
new TestLiveObjectFramePhase(_ => calls.Add("objects")),
|
||||
world,
|
||||
new TestLiveSessionFramePhase(() =>
|
||||
{
|
||||
calls.Add("network");
|
||||
world.PlaceLiveEntityProjection(landblock, Entity(1));
|
||||
Assert.Equal(before, world.VisibilityCommitCount);
|
||||
}),
|
||||
new TestPostNetworkCommandFramePhase(() =>
|
||||
{
|
||||
calls.Add("commands");
|
||||
Assert.Equal(before + 1, world.VisibilityCommitCount);
|
||||
}),
|
||||
new TestLiveSpatialReconcilePhase(() =>
|
||||
{
|
||||
calls.Add("spatial");
|
||||
Assert.Equal(before + 1, world.VisibilityCommitCount);
|
||||
}));
|
||||
|
||||
frame.Tick(1f / 60f);
|
||||
|
||||
Assert.Equal(["objects", "network", "commands", "spatial"], calls);
|
||||
Assert.Equal(before + 1, world.VisibilityCommitCount);
|
||||
Assert.Equal(1, world.PendingLiveEntityCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LiveFrame_SessionFailureDisposesMutationBatchAndSuppressesLaterPhases()
|
||||
{
|
||||
const uint landblock = 0x0101FFFFu;
|
||||
var calls = new List<string>();
|
||||
var world = new GpuWorldState();
|
||||
long before = world.VisibilityCommitCount;
|
||||
var frame = new RetailLiveFrameCoordinator(
|
||||
new TestLiveObjectFramePhase(_ => calls.Add("objects")),
|
||||
world,
|
||||
new TestLiveSessionFramePhase(() =>
|
||||
{
|
||||
calls.Add("network");
|
||||
world.PlaceLiveEntityProjection(landblock, Entity(2));
|
||||
Assert.Equal(before, world.VisibilityCommitCount);
|
||||
throw new InvalidOperationException("inbound failure");
|
||||
}),
|
||||
new TestPostNetworkCommandFramePhase(() => calls.Add("commands")),
|
||||
new TestLiveSpatialReconcilePhase(() => calls.Add("spatial")));
|
||||
|
||||
InvalidOperationException error = Assert.Throws<InvalidOperationException>(
|
||||
() => frame.Tick(1f / 60f));
|
||||
|
||||
Assert.Equal("inbound failure", error.Message);
|
||||
Assert.Equal(["objects", "network"], calls);
|
||||
Assert.Equal(before + 1, world.VisibilityCommitCount);
|
||||
Assert.Equal(1, world.PendingLiveEntityCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstalledRecall_HiddenEnterWorldPublishesFinishedCyclicPoseWithoutAdvancingTime()
|
||||
{
|
||||
|
|
@ -108,4 +182,14 @@ public sealed class RecallTeleportAnimationTests
|
|||
Quaternion.Normalize(recallTail[index].Orientation),
|
||||
Quaternion.Normalize(finishedPose[index].Orientation))) < 0.999999f);
|
||||
}
|
||||
|
||||
private static WorldEntity Entity(uint id) => new()
|
||||
{
|
||||
Id = id,
|
||||
ServerGuid = 0x70000000u + id,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -228,6 +228,16 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
FieldInfo[] ownerFields = owner.GetFields(
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.DoesNotContain(ownerFields, field => field.FieldType == typeof(GameWindow));
|
||||
if (owner == typeof(AcDream.App.Input.RetailLocalPlayerFrameController))
|
||||
{
|
||||
// Checkpoint B deliberately leaves this single legacy callback
|
||||
// composition for D/F, which own input capture and the mutable
|
||||
// player-mode/controller slot. No other phase owner may add one.
|
||||
Assert.Contains(
|
||||
ownerFields,
|
||||
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||
continue;
|
||||
}
|
||||
Assert.DoesNotContain(
|
||||
ownerFields,
|
||||
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||
|
|
@ -274,6 +284,71 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
Assert.Equal(1, source.Split("PublishTime(", StringSplitOptions.None).Length - 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractedLiveObjectSource_PinsRetailAndRegisteredAdaptationOrder()
|
||||
{
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Update",
|
||||
"LiveObjectFrameController.cs"));
|
||||
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"_localPlayerFrame.AdvanceBeforeNetwork",
|
||||
"_selectionInteractions?.DrainOutbound",
|
||||
"_animations.Tick",
|
||||
"_staticAnimations.Tick",
|
||||
"_animationPresenter.Present",
|
||||
"_equippedChildren.Tick",
|
||||
"_staticAnimations.ProcessHooks",
|
||||
"_effects.Tick");
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"_translucencyFades.AdvanceAll",
|
||||
"_animationHooks.Drain",
|
||||
"_entityEffects.RefreshLiveOwnerPoses",
|
||||
"_particleSink.RefreshAttachedEmitters",
|
||||
"_lights.Refresh",
|
||||
"_particleVisibility.Apply",
|
||||
"_particles.Tick",
|
||||
"_scripts.Tick");
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"public void Reconcile()",
|
||||
"_entityEffects.RefreshLiveOwnerPoses",
|
||||
"_equippedChildren.Tick",
|
||||
"_particleSink.RefreshAttachedEmitters",
|
||||
"_lights.Refresh");
|
||||
|
||||
Assert.Equal(1, CountOccurrences(source, "_staticAnimations.Tick("));
|
||||
Assert.Equal(1, CountOccurrences(source, "_staticAnimations.ProcessHooks("));
|
||||
Assert.Equal(1, CountOccurrences(source, "_effects.Tick("));
|
||||
Assert.Equal(1, CountOccurrences(source, "_particles.Tick("));
|
||||
Assert.Equal(1, CountOccurrences(source, "_scripts.Tick("));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GameWindow_ComposesTheLiveFrameOwnersWithoutOwningTheirBodies()
|
||||
{
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
|
||||
Assert.Contains("new AcDream.App.Update.LiveObjectFrameController(", source);
|
||||
Assert.Contains("new AcDream.App.Update.LiveSpatialPresentationReconciler(", source);
|
||||
Assert.Contains("new AcDream.App.World.RetailLiveFrameCoordinator(", source);
|
||||
Assert.DoesNotContain("AdvanceLiveObjectRuntime", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("ReconcileLiveObjectSpatialPresentation", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("ILiveAnimationPresentationContext", source,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static UpdateFrameOrchestrator Create(
|
||||
List<string> calls,
|
||||
RecordingTeardown? teardown = null,
|
||||
|
|
@ -432,4 +507,19 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
|
||||
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
||||
}
|
||||
|
||||
private static void AssertAppearsInOrder(string source, params string[] markers)
|
||||
{
|
||||
int previous = -1;
|
||||
foreach (string marker in markers)
|
||||
{
|
||||
int current = source.IndexOf(marker, previous + 1, StringComparison.Ordinal);
|
||||
Assert.True(current >= 0, $"Missing source marker: {marker}");
|
||||
Assert.True(current > previous, $"Out-of-order source marker: {marker}");
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
|
||||
private static int CountOccurrences(string source, string marker) =>
|
||||
source.Split(marker, StringSplitOptions.None).Length - 1;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue