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]