From 476c2e6de1df057b71f7349c58de6a607110b7c7 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 22 Jul 2026 09:27:55 +0200 Subject: [PATCH] test(app): freeze GameWindow lifecycle boundaries Pin the accepted startup, input, frame, resize, shutdown, and native-window order before Slice 8 moves those edges, while deleting only unread duplicate state and test-only GameWindow facades. Co-authored-by: Codex --- ...amewindow-slice-8-composition-lifecycle.md | 8 +- src/AcDream.App/Rendering/GameWindow.cs | 43 --- .../LocalPlayerOutboundCombatStyleTests.cs | 5 +- .../Input/PlayerMouseLookMovementTests.cs | 5 +- .../GameWindowSlice8BoundaryTests.cs | 277 ++++++++++++++++++ .../ResourceShutdownTransactionTests.cs | 81 +++++ .../Vfx/EntityScriptActivatorTests.cs | 17 +- 7 files changed, 380 insertions(+), 56 deletions(-) create mode 100644 tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs diff --git a/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md b/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md index e42161a5..61eccef9 100644 --- a/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md +++ b/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md @@ -14,7 +14,7 @@ audit. ## Progress ledger -- [ ] A — freeze construction/callback/shutdown order and delete proven dead or +- [x] A — freeze construction/callback/shutdown order and delete proven dead or test-facade residue. - [ ] B — make native-window callback intake an explicit reversible owner and define host-quiescence failure semantics. @@ -401,6 +401,12 @@ fixes these audited gaps: `_streamingRadius`, unused snap constants, `IsPlayerGuid`, `IsDoorName`, and test-only forwarding helpers. Tests call canonical owners directly. +Result: the exact native attributes/callbacks, input subscription priority, +frame-root/session-start boundary, framebuffer behavior, shutdown stages, and +native-window-last edge are frozen. Dead duplicate state and two test-only +window facades are removed. Three corrected-diff reviews are clean; 56 focused +tests and the complete App suite (2,991 pass / 3 intentional skips) pass. + ### B — native window binding and host quiescence - Implement exact attach/reverse-detach/rollback and the no-throw quiescence diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 8161ebe6..e94cbb6d 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -30,7 +30,6 @@ public sealed class GameWindow : IDisposable /// at draw time but allocated + disposed here. private Shader? _terrainModernShader; private CameraController? _cameraController; - private IMouse? _capturedMouse; private IDatReaderWriter? _dats; private readonly AcDream.App.Input.PointerPositionState _pointerPosition = new(); private float _lastMouseX @@ -118,7 +117,6 @@ public sealed class GameWindow : IDisposable _localPlayerTeleportSink = new(); private AcDream.App.Streaming.LocalPlayerTeleportController? _localPlayerTeleport; - private int _streamingRadius = 2; // default 5×5 (kept for debug overlay getStreamingRadius callback) private readonly AcDream.App.Rendering.WorldRenderRangeState _renderRange = new(nearRadius: 4, farRadius: 12); private int _nearRadius @@ -317,37 +315,6 @@ public sealed class GameWindow : IDisposable private AcDream.App.World.LiveEntityRuntime? _liveEntities; private AcDream.App.World.LiveEntityLivenessController? _liveEntityLiveness; - /// Soft-snap decay rate (1/sec). At this rate the residual - /// halves every 1/rate seconds. 8.0 → ~100ms half-life, so even a - /// 2m residual fades within ~300ms without visible snap. - private const float SnapResidualDecayRate = 8.0f; - /// - /// When the prediction error exceeds this many meters, we treat the - /// update as a teleport / rubber-band and hard-snap (no soft lerp). - /// Prevents the soft-snap logic from trying to smooth a genuine portal - /// or force-move event. - /// - /// - /// Matches retail's GetAutonomyBlipDistance (ACE - /// PhysicsObj.cs:545): 20m for creatures, 25m for players. - /// We use 20m as a conservative default — any delta larger than this - /// must be a teleport (portal, recall, spawn). A running character - /// with 1-second UpdatePosition cadence at 9.5 m/s produces deltas - /// of ~9.5m, well below this threshold, so normal movement flows - /// through the interpolation queue instead of hard-snapping. - /// - /// - private const float SnapHardSnapThreshold = 20.0f; - - /// - /// Soft-snap window in seconds: after an UpdatePosition arrives for a - /// remote entity, dead-reckoning continues but the "origin" for - /// predicted position is the server pos. This matches retail's snap - /// behavior — the server is authoritative, we just interpolate between - /// authoritative samples. - /// - private const float DeadReckonMaxPredictSeconds = 1.0f; - // Phase F.1-H.1 — client-side state classes fed by GameEventWiring. // Exposed publicly so plugins + UI panels can bind directly. public readonly AcDream.Core.Chat.ChatLog Chat = new(); @@ -611,8 +578,6 @@ public sealed class GameWindow : IDisposable // path. Populated for remotes plus the PlayerModeController local entry // (player); pruned only by logical LiveEntityRuntime teardown. - private static bool IsPlayerGuid(uint guid) => (guid & 0xFF000000u) == 0x50000000u; - private static bool IsDoorName(string? name) => name == "Door"; // ServerControlledVelocityStaleSeconds moved to RemotePhysicsUpdater (#184 // Slice 2a — the DR tick's stale-velocity anim-stop was its only user). public GameWindow( @@ -2349,7 +2314,6 @@ public sealed class GameWindow : IDisposable if (_options.LegacyStreamRadius is { } sr) { _nearRadius = sr; - _streamingRadius = sr; // keep debug overlay in sync _farRadius = System.Math.Max(sr, _farRadius); } Console.WriteLine( @@ -3340,8 +3304,6 @@ public sealed class GameWindow : IDisposable session.SendTurbineChatTo( roomId, chatType, dispatchType, senderGuid, text, cookie), Log: Console.WriteLine); - internal static uint ParticleEntityKey(AcDream.Core.World.WorldEntity entity) => - entity.Id; /// /// Phase 5d — retail AdminEnvirons (0xEA60) dispatcher. /// Routes fog presets into the weather system's sticky override @@ -3427,7 +3389,6 @@ public sealed class GameWindow : IDisposable // held is handled separately by the gameplay input owner. bool needsRawCursor = _cameraController?.IsFlyMode == true; mouse.Cursor.CursorMode = needsRawCursor ? CursorMode.Raw : CursorMode.Normal; - _capturedMouse = needsRawCursor ? mouse : null; } // Performance overlay state — updated every ~0.5s and written to the @@ -4217,10 +4178,6 @@ public sealed class GameWindow : IDisposable _debugVm?.AddToast(text); } - internal static AcDream.Core.Physics.RawMotionState BuildOutboundRawMotionState( - AcDream.App.Input.MovementResult result) => - AcDream.App.Input.LocalPlayerOutboundController.BuildRawMotionState(result); - /// /// Item-target-mode world pick at the current cursor. The renderer supplies the /// exact visible CPhysicsPart equivalents and RetailWorldPicker performs diff --git a/tests/AcDream.App.Tests/Input/LocalPlayerOutboundCombatStyleTests.cs b/tests/AcDream.App.Tests/Input/LocalPlayerOutboundCombatStyleTests.cs index 2dfa18fa..1103bb6b 100644 --- a/tests/AcDream.App.Tests/Input/LocalPlayerOutboundCombatStyleTests.cs +++ b/tests/AcDream.App.Tests/Input/LocalPlayerOutboundCombatStyleTests.cs @@ -1,6 +1,5 @@ using System.Buffers.Binary; using AcDream.App.Input; -using AcDream.App.Rendering; using AcDream.Core.Net.Messages; using AcDream.Core.Net.Packets; using AcDream.Core.Physics; @@ -21,7 +20,7 @@ public sealed class LocalPlayerOutboundCombatStyleTests MovementResult movement = controller.CaptureMovementResult( mouseLookEvent: false); - RawMotionState outbound = GameWindow.BuildOutboundRawMotionState( + RawMotionState outbound = LocalPlayerOutboundController.BuildRawMotionState( movement); Assert.Equal(combatStyle, movement.CurrentStyle); @@ -43,7 +42,7 @@ public sealed class LocalPlayerOutboundCombatStyleTests { var controller = new PlayerMovementController(new PhysicsEngine()); - RawMotionState outbound = GameWindow.BuildOutboundRawMotionState( + RawMotionState outbound = LocalPlayerOutboundController.BuildRawMotionState( controller.CaptureMovementResult(mouseLookEvent: false)); Assert.Equal(RawMotionState.Default.CurrentStyle, outbound.CurrentStyle); diff --git a/tests/AcDream.App.Tests/Input/PlayerMouseLookMovementTests.cs b/tests/AcDream.App.Tests/Input/PlayerMouseLookMovementTests.cs index a93c51ec..c0946d21 100644 --- a/tests/AcDream.App.Tests/Input/PlayerMouseLookMovementTests.cs +++ b/tests/AcDream.App.Tests/Input/PlayerMouseLookMovementTests.cs @@ -1,6 +1,5 @@ using System.Numerics; using AcDream.App.Input; -using AcDream.App.Rendering; using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; using Xunit; @@ -259,7 +258,7 @@ public sealed class PlayerMouseLookMovementTests controller.BeginMouseLook(new MovementInput(Run: true)); MovementResult press = controller.CaptureMovementResult(mouseLookEvent: false); - RawMotionState wire = GameWindow.BuildOutboundRawMotionState(press); + RawMotionState wire = LocalPlayerOutboundController.BuildRawMotionState(press); Assert.Equal(HoldKey.Run, controller.Motion.RawState.CurrentHoldKey); Assert.True(press.IsRunning); @@ -325,7 +324,7 @@ public sealed class PlayerMouseLookMovementTests controller.BeginMouseLook(held); MovementResult press = controller.CaptureMovementResult(mouseLookEvent: false); - RawMotionState wire = GameWindow.BuildOutboundRawMotionState(press); + RawMotionState wire = LocalPlayerOutboundController.BuildRawMotionState(press); Assert.Equal(MotionCommand.SideStepRight, press.SidestepCommand); Assert.True(press.SidestepUsesRunHold); diff --git a/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs new file mode 100644 index 00000000..f466e66b --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs @@ -0,0 +1,277 @@ +namespace AcDream.App.Tests.Rendering; + +/// +/// Temporary source-shape freeze for the Slice 8 cutover. Each later checkpoint +/// replaces the corresponding assertion with functional tests on the extracted +/// owner; this file prevents a mechanical move from silently reordering host +/// behavior before that owner exists. +/// +public sealed class GameWindowSlice8BoundaryTests +{ + [Fact] + public void Run_PreservesNativeAttributeAndCallbackOrder() + { + string body = MethodBody("public void Run()", "private void OnLoad()"); + + AssertAppearsInOrder( + body, + "startupStore.LoadDisplay()", + "QualitySettings.WithEnvOverrides(startupBase)", + "_displayFramePacing.InitializeStartup(startupDisplay.VSync)", + "VSync = startupPacing.UseVSync", + "Samples = startupQuality.MsaaSamples", + "Window.Create(options)", + "_displayFramePacing.BindSurface(", + "_window.Load += OnLoad;", + "_window.Update += OnUpdate;", + "_window.Render += OnRender;", + "_window.Render += _displayFramePacing.OnFrameRendered;", + "_window.Closing += OnClosing;", + "_window.FocusChanged += OnFocusChanged;", + "_window.Move += _displayFramePacing.OnWindowMoved;", + "_window.StateChanged += _displayFramePacing.OnWindowStateChanged;", + "_window.FramebufferResize += OnFramebufferResize;", + "_window.Run();"); + + string[] registrations = + [ + "_window.Load += OnLoad;", + "_window.Update += OnUpdate;", + "_window.Render += OnRender;", + "_window.Render += _displayFramePacing.OnFrameRendered;", + "_window.Closing += OnClosing;", + "_window.FocusChanged += OnFocusChanged;", + "_window.Move += _displayFramePacing.OnWindowMoved;", + "_window.StateChanged += _displayFramePacing.OnWindowStateChanged;", + "_window.FramebufferResize += OnFramebufferResize;", + ]; + foreach (string registration in registrations) + Assert.Equal(1, CountOccurrences(body, registration)); + } + + [Fact] + public void SessionStart_FollowsFrameGraphPublication() + { + string body = MethodBody( + "private void OnLoad()", + "private AcDream.App.Net.LiveSessionLifecycleHost"); + + AssertAppearsInOrder( + body, + "_gl = GL.GetApi(_window!);", + "_input = _window!.CreateInput();", + "_kbSource = new AcDream.App.Input.SilkKeyboardSource(firstKb);", + "_mouseSource = new AcDream.App.Input.SilkMouseSource(", + "_inputDispatcher = new AcDream.UI.Abstractions.Input.InputDispatcher(", + "_movementInput.Bind(_inputDispatcher);", + "_cameraInput.Bind(_inputDispatcher);", + "_inputDispatcher.Fired += OnInputAction;", + "mouse.MouseMove +=", + "_cameraController = new CameraController(orbit, fly);", + "_dats = RuntimeDatCollectionFactory.OpenReadOnly(_datDir);", + "LoadAndApplyPersistedSettings();", + "_uiHost = new AcDream.App.UI.UiHost(", + "_uiHost.WireMouse(m)", + "_uiHost.WireKeyboard(kb)", + "_retailUiRuntime = AcDream.App.UI.RetailUiRuntime.Mount(", + "_liveEntities = new AcDream.App.World.LiveEntityRuntime(", + "_renderFrameOrchestrator =", + "_updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(", + "_liveSessionController.Start("); + Assert.Equal(1, CountOccurrences(body, "_liveSessionController.Start(")); + int start = body.IndexOf("_liveSessionController.Start(", StringComparison.Ordinal); + string postStart = body[start..]; + Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal); + Assert.Empty(System.Text.RegularExpressions.Regex.Matches( + postStart, + @"(?m)^\s+_[A-Za-z]\w*\s*(?:\?\?=|=)")); + Assert.DoesNotContain("new ", postStart, StringComparison.Ordinal); + Assert.DoesNotContain(".Mount(", postStart, StringComparison.Ordinal); + Assert.DoesNotContain(".Bind(", postStart, StringComparison.Ordinal); + Assert.DoesNotContain(".Attach(", postStart, StringComparison.Ordinal); + Assert.DoesNotContain(".Compose(", postStart, StringComparison.Ordinal); + Assert.DoesNotContain("PrepareResources(", postStart, StringComparison.Ordinal); + } + + [Fact] + public void InputAction_PreservesRetailAcceptedPriority() + { + string body = MethodBody( + "private void OnInputAction(", + "private void ToggleLiveCombatMode()"); + + AssertAppearsInOrder( + body, + "HandlePointerAction(action, activation)", + "action == AcDream.UI.Abstractions.Input.InputAction.ScrollUp", + "HandleScrollAction(action);", + "HandleCombatAction(action, activation)", + "activation != AcDream.UI.Abstractions.Input.ActivationType.Press", + "_retailUiRuntime?.HandleInputAction(action)", + "_selectionInteractions?.HandleInputAction(action)", + "HandlePressedMovementAction(action)", + "switch (action)"); + } + + [Fact] + public void FramebufferResize_PreservesViewportCameraAndDevtoolsOrder() + { + string body = MethodBody( + "private void OnFramebufferResize(", + "private void ApplyDisplayWindowState("); + + AssertAppearsInOrder( + body, + "if (newSize.X <= 0 || newSize.Y <= 0) return;", + "_gl?.Viewport(", + "_viewportAspect.Update(", + "_cameraController?.SetAspect(", + "_devToolsFramePresenter?.ResetLayout("); + Assert.DoesNotContain("_uiHost", body, StringComparison.Ordinal); + } + + [Fact] + public void UpdateRenderFocusAndCloseRemainNarrowHostEdges() + { + string source = GameWindowSource(); + string update = Slice( + source, + "private void OnUpdate(double dt)", + "private void OnCameraModeChanged("); + string render = Slice( + source, + "private void OnRender(double deltaSeconds)", + "private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot"); + string focus = Slice( + source, + "private void OnFocusChanged(bool focused)", + "public void Dispose()"); + string close = Slice( + source, + "private void OnClosing()", + "private ResourceShutdownTransaction CreateShutdownTransaction()"); + + Assert.Equal(1, CountOccurrences(update, "_updateFrameOrchestrator.Tick(")); + Assert.Equal(1, CountOccurrences(render, "_renderFrameOrchestrator!.Render(")); + AssertAppearsInOrder( + render, + "Vector2D size = _window!.Size;", + "_renderFrameOrchestrator!.Render(", + "new AcDream.App.Rendering.RenderFrameInput("); + Assert.DoesNotContain("FramebufferSize", render, StringComparison.Ordinal); + AssertAppearsInOrder(focus, "if (!focused)", "_gameplayInputFrame?.EndMouseLook();"); + AssertAppearsInOrder(close, "=> CompleteShutdown();", "_shutdown.CompleteOrThrow();"); + } + + [Fact] + public void Shutdown_PreservesDependencyStagesAndNativeWindowLast() + { + string source = GameWindowSource(); + string manifest = Slice( + source, + "private ResourceShutdownTransaction CreateShutdownTransaction()", + "private void OnFocusChanged(bool focused)"); + string dispose = Slice( + source, + "public void Dispose()", + "private sealed class NullAnimLoader"); + + string[] stages = + [ + "new ResourceShutdownStage(\"session lifetime\"", + "new ResourceShutdownStage(\"frame borrowers\"", + "new ResourceShutdownStage(\"session dependents\"", + "new ResourceShutdownStage(\"live entities\"", + "new ResourceShutdownStage(\"live entity dependents\"", + "new ResourceShutdownStage(\"submitted GPU work\"", + "new ResourceShutdownStage(\"render frontends\"", + "new ResourceShutdownStage(\"shared texture owners\"", + "new ResourceShutdownStage(\"mesh adapter\"", + "new ResourceShutdownStage(\"remaining render owners\"", + "new ResourceShutdownStage(\"frame flight owner\"", + "new ResourceShutdownStage(\"content mappings\"", + "new ResourceShutdownStage(\"input\"", + "new ResourceShutdownStage(\"OpenGL context\"", + ]; + AssertAppearsInOrder(manifest, stages); + Assert.Equal(stages.Length, CountOccurrences(manifest, "new ResourceShutdownStage(")); + foreach (string stage in stages) + Assert.Equal(1, CountOccurrences(manifest, stage)); + AssertAppearsInOrder( + dispose, + "CompleteShutdown();", + "_window?.Dispose();", + "_window = null;"); + Assert.Equal(1, CountOccurrences(dispose, "CompleteShutdown();")); + Assert.Equal(1, CountOccurrences(dispose, "_window?.Dispose();")); + Assert.Equal(1, CountOccurrences(dispose, "_window = null;")); + int nativeReleased = dispose.IndexOf("_window = null;", StringComparison.Ordinal) + + "_window = null;".Length; + string afterNativeRelease = dispose[nativeReleased..]; + int fallbackDocumentation = afterNativeRelease.IndexOf( + "/// ", + StringComparison.Ordinal); + if (fallbackDocumentation >= 0) + afterNativeRelease = afterNativeRelease[..fallbackDocumentation]; + Assert.All( + afterNativeRelease, + character => Assert.True(char.IsWhiteSpace(character) || character == '}')); + } + + private static string MethodBody(string start, string end) => + Slice(GameWindowSource(), start, end); + + private static string Slice(string source, string start, string end) + { + int first = source.IndexOf(start, StringComparison.Ordinal); + int last = source.IndexOf(end, first + 1, StringComparison.Ordinal); + Assert.True(first >= 0, $"Missing source boundary: {start}"); + Assert.True(last > first, $"Missing source boundary: {end}"); + return source[first..last]; + } + + private static int CountOccurrences(string source, string value) + { + int count = 0; + int cursor = 0; + while ((cursor = source.IndexOf(value, cursor, StringComparison.Ordinal)) >= 0) + { + count++; + cursor += value.Length; + } + + return count; + } + + private static void AssertAppearsInOrder(string source, params string[] fragments) + { + int cursor = -1; + foreach (string fragment in fragments) + { + int next = source.IndexOf(fragment, cursor + 1, StringComparison.Ordinal); + Assert.True(next >= 0, $"Missing expected source fragment: {fragment}"); + Assert.True(next > cursor, $"Out-of-order source fragment: {fragment}"); + cursor = next; + } + } + + private static string GameWindowSource() => File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Rendering", + "GameWindow.cs")); + + 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."); + } +} diff --git a/tests/AcDream.App.Tests/Rendering/ResourceShutdownTransactionTests.cs b/tests/AcDream.App.Tests/Rendering/ResourceShutdownTransactionTests.cs index 3bdfcfc9..20e56a53 100644 --- a/tests/AcDream.App.Tests/Rendering/ResourceShutdownTransactionTests.cs +++ b/tests/AcDream.App.Tests/Rendering/ResourceShutdownTransactionTests.cs @@ -78,4 +78,85 @@ public sealed class ResourceShutdownTransactionTests Assert.Equal(1, calls); Assert.True(transaction.IsComplete); } + + [Fact] + public void PersistentFailuresInOneStageAreAllAttemptedAndNamed() + { + int firstCalls = 0; + int secondCalls = 0; + var transaction = new ResourceShutdownTransaction( + new ResourceShutdownStage("owners", + [ + new("first", () => + { + firstCalls++; + throw new InvalidOperationException("first failure"); + }), + new("second", () => + { + secondCalls++; + throw new InvalidOperationException("second failure"); + }), + ])); + + AggregateException error = Assert.Throws( + transaction.CompleteOrThrow); + + Assert.True(firstCalls > 0); + Assert.Equal(firstCalls, secondCalls); + Assert.Contains( + error.InnerExceptions, + exception => exception.Message.Contains("first", StringComparison.Ordinal)); + Assert.Contains( + error.InnerExceptions, + exception => exception.Message.Contains("second", StringComparison.Ordinal)); + Assert.False(transaction.IsComplete); + } + + [Fact] + public void ExplicitRetryAfterFailedCompletionDoesNotReplaySuccessfulOperations() + { + int completeCalls = 0; + int pendingCalls = 0; + bool allowPending = false; + var transaction = new ResourceShutdownTransaction( + new ResourceShutdownStage("owners", + [ + new("complete", () => completeCalls++), + new("pending", () => + { + pendingCalls++; + if (!allowPending) + throw new InvalidOperationException("not yet"); + }), + ])); + + Assert.Throws(transaction.CompleteOrThrow); + int pendingCallsBeforeRetry = pendingCalls; + allowPending = true; + + transaction.CompleteOrThrow(); + + Assert.Equal(1, completeCalls); + Assert.Equal(pendingCallsBeforeRetry + 1, pendingCalls); + Assert.True(transaction.IsComplete); + } + + [Fact] + public void EmptyStagesConvergeWithoutSkippingLaterOperations() + { + int calls = 0; + var transaction = new ResourceShutdownTransaction( + new ResourceShutdownStage("empty", []), + new ResourceShutdownStage("owner", + [ + new("owner", () => calls++), + ]), + new ResourceShutdownStage("empty tail", [])); + + transaction.CompleteOrThrow(); + + Assert.Equal(1, calls); + Assert.True(transaction.IsComplete); + } } diff --git a/tests/AcDream.App.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs b/tests/AcDream.App.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs index 65c76a20..7af60e5e 100644 --- a/tests/AcDream.App.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Numerics; -using AcDream.App.Rendering; using AcDream.App.Rendering.Vfx; using AcDream.Core.Physics; using AcDream.Core.Vfx; @@ -472,7 +471,6 @@ public sealed class EntityScriptActivatorTests Assert.Contains("globally unique", error.Message, StringComparison.Ordinal); Assert.Equal(1, p.Runner.ActiveOwnerCount); - Assert.Equal(first.Id, GameWindow.ParticleEntityKey(first)); } [Fact] @@ -569,8 +567,13 @@ public sealed class EntityScriptActivatorTests [Fact] public void LiveParticleFilterUsesCanonicalLocalEffectOwnerNotServerGuid() { - var p = BuildPipeline(); - var activator = new EntityScriptActivator(p.Runner, p.Sink, p.Poses, _ => null); + var p = BuildPipeline( + (0xAAu, BuildScript((0.0, new CreateParticleHook { EmitterInfoId = 100 })))); + var activator = new EntityScriptActivator( + p.Runner, + p.Sink, + p.Poses, + StaticResolver(0xAAu)); var entity = new WorldEntity { Id = 1_000_123u, @@ -582,9 +585,11 @@ public sealed class EntityScriptActivatorTests }; activator.OnCreate(entity); + p.Runner.Tick(0.001f); - Assert.Equal(entity.Id, GameWindow.ParticleEntityKey(entity)); - Assert.NotEqual(entity.ServerGuid, GameWindow.ParticleEntityKey(entity)); + Assert.Single(p.Recording.Calls); + Assert.Equal(entity.Id, p.Recording.Calls[0].EntityId); + Assert.NotEqual(entity.ServerGuid, p.Recording.Calls[0].EntityId); } [Fact]