diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index de7ff4be7..69e8f89b7 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -3285,7 +3285,7 @@ static void RequireEveRuntimeBootstrap(string root) "EveSurfaceCommandRequest request", "request.ProviderId, \"aetheria.daemon\"", "AetheriaClient", - "client.State.CurrentObservedDaemon()", + "client.CurrentObservedDaemon()", "new AetheriaRuntimeDaemonOperationClient(", "AetheriaRuntimeDaemonSurfaceCommandCatalog.TrySubmitArgumentless(", "AetheriaRuntimeDaemonSurfaceCommandCatalog.CommandPrefix" @@ -3304,6 +3304,7 @@ static void RequireEveRuntimeBootstrap(string root) daemonSurfaceCommands.Contains("client.State.ReactiveDaemonFrame()", StringComparison.Ordinal) || daemonSurfaceCommands.Contains("TryReactiveDaemonSoaView(client)", StringComparison.Ordinal) || daemonSurfaceCommands.Contains("client.State.ReactiveZoneRender()", StringComparison.Ordinal) || + daemonSurfaceCommands.Contains("client.State.CurrentObservedDaemon()", StringComparison.Ordinal) || daemonSurfaceCommands.Contains("client.State.ReactiveObservedDaemon()", StringComparison.Ordinal) || daemonSurfaceCommands.Contains("observedState.TryCurrent(out var current)", StringComparison.Ordinal)) { @@ -8248,7 +8249,7 @@ static void RequireAetheriaManagedStateAccessorsCoverDomainDocuments(string root if (survivingObservedDaemonConvenienceWrappers.Length > 0) { throw new InvalidOperationException( - "AetheriaClientState still exposes one-shot observed-daemon aggregate wrappers; use CurrentObservedDaemon() or generic typed document reads: " + + "AetheriaClientState still exposes one-shot observed-daemon aggregate wrappers; use caller-owned managed typed documents: " + string.Join(", ", survivingObservedDaemonConvenienceWrappers)); } var survivingFixedReactiveWrappers = forbiddenFixedReactiveWrappers @@ -15979,8 +15980,10 @@ static void RequireRuntimeStateReaderOwnsUnityStateAcquisition(string root) !observedDaemonState.Contains("CultMeshReactiveDocument zoneRender", StringComparison.Ordinal) || !observedDaemonState.Contains("var currentZoneRender = zoneRender?.Current;", StringComparison.Ordinal) || !observedDaemonState.Contains("new AetheriaRuntimeObservedDaemonState(currentFrame, currentSoaView, currentZoneRender)", StringComparison.Ordinal) || - !aetheriaClientState.Contains("using var zoneRender = Reactive(options);", StringComparison.Ordinal) || - !aetheriaClientState.Contains("AetheriaRuntimeObservedDaemonState.TryCreateCurrent(frame, soaView, zoneRender, out var current)", StringComparison.Ordinal)) + !aetheriaClient.Contains("private CultMeshReactiveDocument? _observedFrame;", StringComparison.Ordinal) || + !aetheriaClient.Contains("private CultMeshReactiveDocument? _observedSoaView;", StringComparison.Ordinal) || + !aetheriaClient.Contains("private CultMeshReactiveDocument? _observedZoneRender;", StringComparison.Ordinal) || + !aetheriaClient.Contains("AetheriaRuntimeObservedDaemonState.TryCreateCurrent(", StringComparison.Ordinal)) { throw new InvalidOperationException( "Observed zone-render state acquisition must sample managed typed zone-render documents directly."); @@ -16033,7 +16036,7 @@ static void RequireRuntimeStateReaderOwnsUnityStateAcquisition(string root) var requiredManagedClientConnectionSymbols = new[] { "public AetheriaClientState State => _state;", - "var observed = State.CurrentObservedDaemon();" + "var observed = CurrentObservedDaemon();" }; var missingManagedClientConnectionSymbols = requiredManagedClientConnectionSymbols .Where(symbol => !aetheriaClient.Contains(symbol, StringComparison.Ordinal)) @@ -16048,6 +16051,7 @@ static void RequireRuntimeStateReaderOwnsUnityStateAcquisition(string root) if (aetheriaClient.Contains("State.ReactiveDaemonFrame()", StringComparison.Ordinal) || aetheriaClient.Contains("TryReactiveDaemonSoaView()", StringComparison.Ordinal) || aetheriaClient.Contains("State.ReactiveZoneRender()", StringComparison.Ordinal) || + aetheriaClient.Contains("State.CurrentObservedDaemon()", StringComparison.Ordinal) || aetheriaClient.Contains("State.ReactiveObservedDaemon()", StringComparison.Ordinal) || aetheriaClient.Contains("Aetheria() => State", StringComparison.Ordinal) || aetheriaClient.Contains("observedState.TryCurrent(out var current)", StringComparison.Ordinal)) @@ -16063,8 +16067,7 @@ static void RequireRuntimeStateReaderOwnsUnityStateAcquisition(string root) "public Task LatestAsync()", "public Observable Watch()", "public Task> ReactiveAsync(", - "public CultMeshReactiveDocument Reactive(", - "public AetheriaRuntimeObservedDaemonState? CurrentObservedDaemon(" + "public CultMeshReactiveDocument Reactive(" }; var missingManagedStateAccessSymbols = requiredManagedStateAccessSymbols .Where(symbol => !aetheriaClientState.Contains(symbol, StringComparison.Ordinal)) @@ -16076,21 +16079,30 @@ static void RequireRuntimeStateReaderOwnsUnityStateAcquisition(string root) string.Join(", ", missingManagedStateAccessSymbols)); } + if (aetheriaClientState.Contains("public AetheriaRuntimeObservedDaemonState? CurrentObservedDaemon(", StringComparison.Ordinal) || + aetheriaClientState.Contains("AetheriaRuntimeObservedDaemonState.TryCreateCurrent(frame, soaView, zoneRender", StringComparison.Ordinal)) + { + throw new InvalidOperationException( + "AetheriaClientState must not expose one-shot observed daemon aggregation; command owners should hold managed typed daemon documents."); + } + var requiredManagedObservedDaemonSymbols = new[] { - "public AetheriaRuntimeObservedDaemonState? CurrentObservedDaemon(", - "using var frame = Reactive(options);", - "using var soaView = TryReactive(options);", - "using var zoneRender = Reactive(options);", - "AetheriaRuntimeObservedDaemonState.TryCreateCurrent(frame, soaView, zoneRender, out var current)" + "private CultMeshReactiveDocument? _observedFrame;", + "private CultMeshReactiveDocument? _observedSoaView;", + "private CultMeshReactiveDocument? _observedZoneRender;", + "_observedFrame ??= State.Reactive();", + "_observedSoaView ??= State.Reactive();", + "_observedZoneRender ??= State.Reactive();", + "AetheriaRuntimeObservedDaemonState.TryCreateCurrent(" }; var missingManagedObservedDaemonSymbols = requiredManagedObservedDaemonSymbols - .Where(symbol => !aetheriaClientState.Contains(symbol, StringComparison.Ordinal)) + .Where(symbol => !aetheriaClient.Contains(symbol, StringComparison.Ordinal)) .ToArray(); if (missingManagedObservedDaemonSymbols.Length > 0) { throw new InvalidOperationException( - "AetheriaClientState must compose current observed daemon state from direct managed reactive typed documents: " + + "AetheriaClient must compose current observed daemon state from owned managed reactive typed documents: " + string.Join(", ", missingManagedObservedDaemonSymbols)); } @@ -16099,10 +16111,10 @@ static void RequireRuntimeStateReaderOwnsUnityStateAcquisition(string root) !daemonRuntimeDocumentTests.Contains("using var observedSoaView = client.State.Reactive();", StringComparison.Ordinal) || !daemonRuntimeDocumentTests.Contains("using var observedZoneRender = client.State.Reactive();", StringComparison.Ordinal) || !daemonRuntimeDocumentTests.Contains("AetheriaRuntimeObservedDaemonState.TryCreateCurrent(", StringComparison.Ordinal) || - !daemonRuntimeDocumentTests.Contains("var sampledObserved = client.State.CurrentObservedDaemon();", StringComparison.Ordinal)) + daemonRuntimeDocumentTests.Contains("client.State.CurrentObservedDaemon()", StringComparison.Ordinal)) { throw new InvalidOperationException( - "Daemon runtime document tests must teach observed daemon sampling through direct reactive daemon documents and CurrentObservedDaemon()."); + "Daemon runtime document tests must teach observed daemon sampling through direct reactive daemon documents."); } var requiredObservedDaemonStateSymbols = new[] diff --git a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs index fa816a1e2..f50aa045e 100644 --- a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs +++ b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs @@ -205,11 +205,15 @@ public class DaemonRuntimeDocumentTests .GetResult(); using var zoneRenderReactive = client.State .Reactive(); - var observed = client.State.CurrentObservedDaemon(); var observedAuthoritativeFrame = client.State.Latest(); using var catalogReactive = client.State.Reactive(); using var daemonFrameReactive = client.State.Reactive(); using var daemonSoaViewReactive = client.State.Reactive(); + Assert.IsTrue(AetheriaRuntimeObservedDaemonState.TryCreateCurrent( + daemonFrameReactive, + daemonSoaViewReactive, + zoneRenderReactive, + out var observed)); using var loadoutTemplatesReactive = client.State.Reactive(); using var sectorMapReactive = client.State.Reactive(); using var playerSettingsReactive = client.State.Reactive(); @@ -2220,7 +2224,14 @@ public class DaemonRuntimeDocumentTests .OpenAsync(statePath, "unity-observer-test", pullOnOpen: true) .GetAwaiter() .GetResult(); - var observed = client.State.CurrentObservedDaemon(); + using var observedFrame = client.State.Reactive(); + using var observedSoaView = client.State.Reactive(); + using var observedZoneRender = client.State.Reactive(); + Assert.IsTrue(AetheriaRuntimeObservedDaemonState.TryCreateCurrent( + observedFrame, + observedSoaView, + observedZoneRender, + out var observed)); Assert.IsNotNull(observed); Assert.IsTrue(observed.IsAuthoritative); @@ -2281,12 +2292,9 @@ public class DaemonRuntimeDocumentTests out var currentObserved) ? currentObserved : null; - var sampledObserved = client.State.CurrentObservedDaemon(); Assert.IsNotNull(observed); - Assert.IsNotNull(sampledObserved); Assert.IsTrue(observed.IsAuthoritative); Assert.AreEqual("reactive-daemon-run", observed.Run.RunId); - Assert.AreEqual(observed.Run.RunId, sampledObserved.Run.RunId); Assert.AreEqual("entity:reactive-observer-target", observed.Run.CurrentEntityKey); Assert.AreEqual(330, observed.Frame.FrameId); Assert.AreEqual(331, observed.SoaView.Generation); @@ -2321,7 +2329,14 @@ public class DaemonRuntimeDocumentTests .OpenAsync(statePath, "unity-frame-only-observer-test", pullOnOpen: true) .GetAwaiter() .GetResult(); - var observed = client.State.CurrentObservedDaemon(); + using var observedFrame = client.State.Reactive(); + using var observedSoaView = client.State.Reactive(); + using var observedZoneRender = client.State.Reactive(); + Assert.IsTrue(AetheriaRuntimeObservedDaemonState.TryCreateCurrent( + observedFrame, + observedSoaView, + observedZoneRender, + out var observed)); Assert.IsNotNull(observed); Assert.IsTrue(observed.IsAuthoritative); diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClient.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClient.cs index 195a06e5f..92b1f67cf 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClient.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClient.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Threading.Tasks; using GameCult.Eve.Surface; +using GameCult.Mesh; #nullable enable @@ -16,6 +17,9 @@ namespace GameCult.Aetheria.State.Verse private readonly AetheriaControl _control; private readonly AetheriaUi _ui; private readonly AetheriaClientState _state; + private CultMeshReactiveDocument? _observedFrame; + private CultMeshReactiveDocument? _observedSoaView; + private CultMeshReactiveDocument? _observedZoneRender; private bool _disposed; private AetheriaClient(AetheriaRuntimeVerseClient verse, string clientId, string sessionId) @@ -150,6 +154,9 @@ namespace GameCult.Aetheria.State.Verse return; _disposed = true; + _observedFrame?.Dispose(); + _observedSoaView?.Dispose(); + _observedZoneRender?.Dispose(); _verse.Dispose(); } @@ -159,7 +166,7 @@ namespace GameCult.Aetheria.State.Verse ThrowIfDisposed(); if (submit == null) throw new ArgumentNullException(nameof(submit)); - var observed = State.CurrentObservedDaemon(); + var observed = CurrentObservedDaemon(); var operationClient = new AetheriaRuntimeDaemonOperationClient( StatePath, _clientId, @@ -172,6 +179,21 @@ namespace GameCult.Aetheria.State.Verse return submit(operationClient, observed); } + internal AetheriaRuntimeObservedDaemonState? CurrentObservedDaemon() + { + ThrowIfDisposed(); + _observedFrame ??= State.Reactive(); + _observedSoaView ??= State.Reactive(); + _observedZoneRender ??= State.Reactive(); + return AetheriaRuntimeObservedDaemonState.TryCreateCurrent( + _observedFrame, + _observedSoaView, + _observedZoneRender, + out var current) + ? current + : null; + } + private void ThrowIfDisposed() { if (_disposed) diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs index 0407fd48f..43cb58cd5 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs @@ -215,17 +215,6 @@ namespace GameCult.Aetheria.State.Verse return Document().LatestAsync().ConfigureAwait(false).GetAwaiter().GetResult(); } - public AetheriaRuntimeObservedDaemonState? CurrentObservedDaemon( - CultMeshReactiveDocumentOptions? options = null) - { - using var frame = Reactive(options); - using var soaView = TryReactive(options); - using var zoneRender = Reactive(options); - return AetheriaRuntimeObservedDaemonState.TryCreateCurrent(frame, soaView, zoneRender, out var current) - ? current - : null; - } - public CultMeshDocumentHandle Document(AetheriaClientEveSurface surface) where TDocument : class { diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSurfaceCommands.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSurfaceCommands.cs index ea5ab98bf..d78acb3ef 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSurfaceCommands.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSurfaceCommands.cs @@ -21,7 +21,7 @@ namespace GameCult.Aetheria.State.Verse return false; } - var observed = client.State.CurrentObservedDaemon(); + var observed = client.CurrentObservedDaemon(); var operationClient = new AetheriaRuntimeDaemonOperationClient( client.StatePath, string.IsNullOrWhiteSpace(request.ClientId) ? AetheriaRuntimeDaemonOperationClient.DefaultClientId : request.ClientId,