diff --git a/src/AcDream.App/Rendering/Selection/RetailSelectionScene.cs b/src/AcDream.App/Rendering/Selection/RetailSelectionScene.cs index b9e7082a..963b3e85 100644 --- a/src/AcDream.App/Rendering/Selection/RetailSelectionScene.cs +++ b/src/AcDream.App/Rendering/Selection/RetailSelectionScene.cs @@ -31,6 +31,7 @@ internal sealed class RetailSelectionScene : private readonly HashSet _buildingKeys = new(); private FrustumPlanes? _viewFrustum; private ICurrentRenderSelectionObserver? _currentRenderSceneObserver; + private bool _frameOpen; private readonly record struct PartKey(uint LocalEntityId, int PartIndex, uint GfxObjId); @@ -66,6 +67,7 @@ internal sealed class RetailSelectionScene : public void Reset() { _currentRenderSceneObserver?.AbortSelectionFrame(); + _frameOpen = false; _building.Clear(); _published.Clear(); _buildingKeys.Clear(); @@ -75,6 +77,13 @@ internal sealed class RetailSelectionScene : public void BeginFrame() { + if (_frameOpen) + { + throw new InvalidOperationException( + "The retail selection scene cannot begin a second frame before completing or aborting the first."); + } + + _frameOpen = true; _building.Clear(); _buildingKeys.Clear(); _viewFrustum = null; @@ -95,6 +104,12 @@ internal sealed class RetailSelectionScene : uint gfxObjId, Matrix4x4 partWorld) { + // WbDrawDispatcher is also shared by sealed CreatureMode-style UI + // viewports (paperdoll and appraisal). Those draws occur after the + // normal-world selection transaction has published and must never + // enter the world picking product. + if (!_frameOpen) + return; if (entity.ServerGuid == 0u) return; @@ -124,6 +139,13 @@ internal sealed class RetailSelectionScene : public void CompleteFrame() { + if (!_frameOpen) + { + throw new InvalidOperationException( + "The retail selection scene cannot complete without an open frame."); + } + + _frameOpen = false; (_published, _building) = (_building, _published); _currentRenderSceneObserver?.CompleteSelectionFrame(); } @@ -132,6 +154,10 @@ internal sealed class RetailSelectionScene : /// completely published selection product. public void AbortFrame() { + if (!_frameOpen) + return; + + _frameOpen = false; _currentRenderSceneObserver?.AbortSelectionFrame(); _building.Clear(); _buildingKeys.Clear(); diff --git a/tests/AcDream.App.Tests/Rendering/RetailSelectionSceneTests.cs b/tests/AcDream.App.Tests/Rendering/RetailSelectionSceneTests.cs index 88ed009a..dd32ad9c 100644 --- a/tests/AcDream.App.Tests/Rendering/RetailSelectionSceneTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RetailSelectionSceneTests.cs @@ -94,6 +94,32 @@ public sealed class RetailSelectionSceneTests Assert.Empty(oracle.SelectionParts); } + [Fact] + public void PrivateViewportPartsOutsideWorldFrame_DoNotEnterPickingOrReferee() + { + var oracle = new CurrentRenderSceneOracle(); + var scene = CreateScene(); + scene.SetCurrentRenderSceneObserver(oracle); + var world = Entity(localId: 49u, serverGuid: 0x5000_0049u); + var privateViewport = + Entity(localId: 50u, serverGuid: 0x5000_0050u); + Publish(scene, world); + + scene.AddVisiblePart( + privateViewport, + partIndex: 0, + gfxObjId: 0x0100_0001u, + Matrix4x4.CreateTranslation(0f, 0f, -5f)); + + RetailSelectionHit? hit = PickCenter(scene); + Assert.NotNull(hit); + Assert.Equal(world.Id, hit.Value.LocalEntityId); + CurrentRenderSelectionFingerprint fingerprint = + Assert.Single(oracle.SelectionParts); + Assert.Equal(world.Id, fingerprint.LocalEntityId); + Assert.Equal(1, oracle.Snapshot.SelectionPartCount); + } + private static RetailSelectionScene CreateScene( RetailSelectionLightingPulse? pulse = null) { diff --git a/tests/AcDream.App.Tests/Streaming/LandblockPresentationPipelineTests.cs b/tests/AcDream.App.Tests/Streaming/LandblockPresentationPipelineTests.cs index 8835ed7e..c6be6df5 100644 --- a/tests/AcDream.App.Tests/Streaming/LandblockPresentationPipelineTests.cs +++ b/tests/AcDream.App.Tests/Streaming/LandblockPresentationPipelineTests.cs @@ -900,7 +900,8 @@ public sealed class LandblockPresentationPipelineTests nearRadius: 0, farRadius: 0, onLandblockLoaded: _ => liveRecoveryCalls++, - ensureEnvCellMeshes: _ => replayCalls++); + ensureEnvCellMeshes: _ => replayCalls++, + workBudgetOptions: GenerousWorkBudget()); Assert.Throws(() => controller.Tick(0x60, 0x60)); Assert.True(state.IsNearTier(landblockId));