diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 53a0989..5cfc17e 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -162,6 +162,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } [UnityTest] + [Timeout(300000)] public IEnumerator GenericCultMeshClientLowersAndMovesAdvertisedWorld() { var endpoint = Environment.GetEnvironmentVariable("EVEUNITY_PROVIDER_ENDPOINT"); @@ -244,6 +245,40 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(runtime.ActiveVersion, Is.GreaterThan(initialVersion)); Assert.That(Vector3.Distance(FindMarker(root, playerId).transform.position, initialPosition), Is.GreaterThan(0.01f)); + var movementVersion = runtime.ActiveVersion; + var focus = runtime.SubmitFocusIntent(playerId); + var focusDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < focusDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != focus.CommandId || + runtime.ActiveVersion < runtime.LastReceipt.SourceVersion)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + Assert.That(runtime.LastReceipt, Is.Not.Null); + Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(focus.CommandId)); + Assert.That(runtime.LastReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled")); + Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(movementVersion)); + + var focusVersion = runtime.ActiveVersion; + var action = runtime.SubmitActionIntent(playerId, "0"); + var actionDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < actionDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != action.CommandId || + runtime.ActiveVersion < runtime.LastReceipt.SourceVersion)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + Assert.That(runtime.LastReceipt, Is.Not.Null); + Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(action.CommandId)); + Assert.That( + runtime.LastReceipt.State, + Is.EqualTo("accepted").Or.EqualTo("reconciled"), + runtime.LastReceipt.Message); + Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(focusVersion)); + Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); + var light = lightObject.AddComponent(); light.type = LightType.Directional; light.intensity = 1.4f; @@ -263,11 +298,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } } var playerMarker = FindMarker(root, playerId); - var playerRenderer = playerMarker.GetComponentInChildren(); - Assert.That(playerRenderer, Is.Not.Null, "The provider-authored player prefab has no renderable visual."); - var playerBounds = playerRenderer.bounds; - var cameraDistance = Mathf.Max(8f, playerBounds.extents.magnitude * 4f); - camera.transform.position = playerBounds.center + new Vector3(1f, 0.8f, -1.2f).normalized * cameraDistance; + var playerRenderers = playerMarker.GetComponentsInChildren(); + Assert.That(playerRenderers, Is.Not.Empty, "The provider-authored player prefab has no renderable visual."); + var playerBounds = playerRenderers[0].bounds; + foreach (var renderer in playerRenderers) + playerBounds.Encapsulate(renderer.bounds); + var halfFovRadians = camera.fieldOfView * 0.5f * Mathf.Deg2Rad; + var cameraDistance = Mathf.Max(8f, playerBounds.extents.magnitude / Mathf.Tan(halfFovRadians) * 1.35f); + camera.nearClipPlane = Mathf.Max(0.01f, cameraDistance * 0.001f); + camera.farClipPlane = Mathf.Max(1000f, cameraDistance + playerBounds.size.magnitude * 4f); + camera.transform.position = playerBounds.center + new Vector3(1f, 0.65f, -1.2f).normalized * cameraDistance; camera.transform.LookAt(playerBounds.center); target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); camera.targetTexture = target; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index b5bb58c..3a63ba9 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -43,6 +43,7 @@ namespace GameCult.Eve.UnityScene private readonly List _assetBundles = new List(); private CultMeshNode? _node; private CultMeshSnapshotEndpoint? _snapshot; + private CultMeshSnapshotSession? _assetSession; private CultNetDocumentRegistry? _networkRegistry; private CultNetShardDescriptor? _replicaShard; private EveProviderAdvertisementDocument? _advertisement; @@ -148,8 +149,10 @@ namespace GameCult.Eve.UnityScene _assetBundles.Clear(); _prefabs.Clear(); _node?.Dispose(); + _assetSession?.Dispose(); _node = null; _snapshot = null; + _assetSession = null; _networkRegistry = null; _replicaShard = null; _pendingCommandIds.Clear(); @@ -215,6 +218,19 @@ namespace GameCult.Eve.UnityScene RudpMaxFragmentBytes = 2048 } }); + _assetSession = CultMesh.SnapshotSession( + _endpoint, + new CultMeshSnapshotRequestOptions + { + ShardId = RemoteShardId, + ShardEpoch = 1, + ConnectTimeout = TimeSpan.FromSeconds(5), + ResponseTimeout = TimeSpan.FromSeconds(10), + MessageIdPrefix = "eve-unity-assets", + RudpRuntimeId = $"{_runtimeId}.assets", + RudpMaxFragmentBytes = 2048 + }, + _networkRegistry); } private void ResolveAdvertisement() @@ -294,7 +310,7 @@ namespace GameCult.Eve.UnityScene foreach (var group in selected.GroupBy(selection => selection.Variant!.Uri, StringComparer.Ordinal)) { var variant = group.First().Variant!; - var manifest = _snapshot.FetchDocumentsAsync( + var manifest = _assetSession!.FetchDocumentsAsync( recordKeys: new[] { variant.Uri }, schemaIds: new[] { CultMeshCdnSchemaVersions.ArtifactManifest }) .GetAwaiter() @@ -332,20 +348,26 @@ namespace GameCult.Eve.UnityScene private byte[] ReadBundle(CultMeshCdnArtifactManifest manifest) { var bytes = new byte[checked((int)manifest.SizeBytes)]; - foreach (var reference in manifest.Chunks.OrderBy(chunk => chunk.Offset)) + var references = manifest.Chunks.OrderBy(chunk => chunk.Offset).ToArray(); + for (var index = 0; index < references.Length; index += 2) { - var chunk = _snapshot! + var batch = references.Skip(index).Take(2).ToArray(); + var chunks = _assetSession! .FetchDocumentsAsync( - recordKeys: new[] { reference.RecordKey }, + recordKeys: batch.Select(reference => reference.RecordKey).ToArray(), schemaIds: new[] { CultMeshCdnSchemaVersions.ArtifactChunk }) .GetAwaiter() - .GetResult() - .FirstOrDefault(); - if (chunk == null) - throw new InvalidOperationException($"Provider asset chunk '{reference.RecordKey}' was not available."); - if (chunk.SizeBytes != reference.SizeBytes || chunk.Payload.Length != reference.SizeBytes) - throw new InvalidDataException($"Provider asset chunk '{reference.RecordKey}' size did not match its manifest."); - Buffer.BlockCopy(chunk.Payload, 0, bytes, checked((int)reference.Offset), reference.SizeBytes); + .GetResult(); + foreach (var reference in batch) + { + var chunk = chunks.FirstOrDefault(candidate => + string.Equals(candidate.ChunkHash, reference.ChunkHash, StringComparison.Ordinal)); + if (chunk == null) + throw new InvalidOperationException($"Provider asset chunk '{reference.RecordKey}' was not available."); + if (chunk.SizeBytes != reference.SizeBytes || chunk.Payload.Length != reference.SizeBytes) + throw new InvalidDataException($"Provider asset chunk '{reference.RecordKey}' size did not match its manifest."); + Buffer.BlockCopy(chunk.Payload, 0, bytes, checked((int)reference.Offset), reference.SizeBytes); + } } return bytes; } diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index a846a4a..90bb7fc 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -69,6 +69,8 @@ $daemonArguments = @( "--client-cultmesh-host", "127.0.0.1", "--client-cultmesh-advertise-host", "127.0.0.1", "--client-cultmesh-port", $Port, + "--tick-interval-ms", 250, + "--fixed-delta-ms", 20, "--no-odin-announcements" ) $env:AETHERIA_TRACE_EVE_SNAPSHOTS = "1"