diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index b236051..9e00282 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -262,6 +262,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(provider.Selection.VerseId, Is.Not.Empty); Assert.That(provider.Selection.ProviderId, Is.Not.Empty); Assert.That(provider.Selection.SurfaceId, Is.Not.Empty); + Assert.That(provider.CurrentAssetBodyTransportKind, + Is.EqualTo(CultMeshBodyTransportKind.SharedFileMapping), + "The generic client did not negotiate its verified provider bundle as a mapped body."); host = root.AddComponent(); host.Configure( root.transform, @@ -815,6 +818,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(fieldSplats, Is.Not.Null); WriteWitnessFacts( witnessProfile, + provider.CurrentAssetBodyTransportKind?.ToString() ?? "", initialVersion, runtime.ActiveVersion, movementDistance, @@ -894,6 +898,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests private static void WriteWitnessFacts( string witnessProfile, + string assetBodyTransport, long initialVersion, long finalVersion, float movementDistance, @@ -960,6 +965,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests witnessProfile = witnessProfile, providerAdvertisement = true, providerAssets = true, + assetBodyTransport = assetBodyTransport, environmentPresentation = environmentPresentation, fieldVolumeFrameId = fieldVolumeFrameId, fieldVolumeLayerCount = fieldVolumeLayerCount, @@ -1042,6 +1048,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public string witnessProfile; public bool providerAdvertisement; public bool providerAssets; + public string assetBodyTransport; public bool environmentPresentation; public long fieldVolumeFrameId; public int fieldVolumeLayerCount; 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 21c535b..9f67ee7 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -51,6 +51,7 @@ namespace GameCult.Eve.UnityScene private readonly Dictionary> _nativeAssetMetadata = new Dictionary>(StringComparer.Ordinal); private readonly List _assetBundles = new List(); + private readonly List _assetBodyLeases = new List(); private readonly Dictionary _renderChannelLayers = new Dictionary(StringComparer.Ordinal); private readonly ConcurrentQueue _liveDocuments = new ConcurrentQueue(); private CultMeshNode? _node; @@ -60,6 +61,7 @@ namespace GameCult.Eve.UnityScene private CultNetDatabaseSubscriptionClient? _subscriptions; private CultMeshClient? _meshClient; private CultMeshContentTransferService? _contentTransfer; + private CultMeshVerifiedBodyMappingBroker? _assetBodyMappings; private EveProviderAdvertisementDocument? _advertisement; private EveAdvertisedSurface? _advertisedSurface; private CultMeshBodyPublicationResolver? _bodyResolver; @@ -109,6 +111,8 @@ namespace GameCult.Eve.UnityScene public EveInputCapabilityDocument CurrentInputCapability { get; private set; } + public CultMeshBodyTransportKind? CurrentAssetBodyTransportKind { get; private set; } + public event Action? SurfaceDocumentAvailable; public event Action? AssetManifestDocumentAvailable; @@ -314,6 +318,8 @@ namespace GameCult.Eve.UnityScene { foreach (var bundle in _assetBundles) bundle.Unload(unloadAllLoadedObjects: false); _assetBundles.Clear(); + foreach (var lease in _assetBodyLeases) lease.Dispose(); + _assetBodyLeases.Clear(); _prefabs.Clear(); _nativeAssets.Clear(); _nativeAssetMetadata.Clear(); @@ -326,6 +332,7 @@ namespace GameCult.Eve.UnityScene _subscriptions = null; _meshClient = null; _contentTransfer = null; + _assetBodyMappings = null; _networkRegistry = null; _replicaShard = null; _pendingCommandIds.Clear(); @@ -688,6 +695,9 @@ namespace GameCult.Eve.UnityScene foreach (var bundle in _assetBundles) bundle.Unload(unloadAllLoadedObjects: false); _assetBundles.Clear(); + foreach (var lease in _assetBodyLeases) lease.Dispose(); + _assetBodyLeases.Clear(); + CurrentAssetBodyTransportKind = null; _prefabs.Clear(); _nativeAssets.Clear(); _nativeAssetMetadata.Clear(); @@ -724,8 +734,7 @@ namespace GameCult.Eve.UnityScene var descriptor = manifest.FirstOrDefault(); if (descriptor == null) throw new InvalidOperationException($"Provider asset bundle manifest '{variant.Uri}' was not available."); - var path = ReadVerifiedBundlePath(descriptor, variant); - var bundle = AssetBundle.LoadFromFile(path); + var bundle = LoadVerifiedBundle(descriptor, variant); if (bundle == null) throw new InvalidOperationException($"Unity could not load provider asset bundle '{variant.Uri}'."); _assetBundles.Add(bundle); @@ -815,6 +824,7 @@ namespace GameCult.Eve.UnityScene var cacheRoot = Environment.GetEnvironmentVariable("EVEUNITY_ASSET_CACHE_PATH"); if (string.IsNullOrWhiteSpace(cacheRoot)) cacheRoot = Path.Combine(Application.persistentDataPath, "EveUnity", "assets"); + _assetBodyMappings = new CultMeshVerifiedBodyMappingBroker(cacheRoot); _contentTransfer = new CultMeshContentTransferService( _node!.Cache, new[] @@ -824,11 +834,12 @@ namespace GameCult.Eve.UnityScene _advertisement.ServiceId, new CultMeshSessionContentProviderOptions { ResponseTimeout = TimeSpan.FromSeconds(30) }) }, - new CultMeshContentTransferOptions(cacheRoot)); + new CultMeshContentTransferOptions(cacheRoot), + _assetBodyMappings); return _contentTransfer; } - private string ReadVerifiedBundlePath(CultMeshCdnArtifactManifest manifest, EveAssetVariant variant) + private AssetBundle? LoadVerifiedBundle(CultMeshCdnArtifactManifest manifest, EveAssetVariant variant) { var hash = NormalizeContentHash(variant.ContentHash); if (manifest.SizeBytes != variant.SizeBytes) @@ -836,7 +847,74 @@ namespace GameCult.Eve.UnityScene if (!string.Equals(NormalizeContentHash(manifest.ContentHash), hash, StringComparison.Ordinal)) throw new InvalidOperationException($"Provider asset bundle '{variant.Uri}' hash did not match its catalog."); - return ContentTransfer().FetchAsync(manifest).GetAwaiter().GetResult(); + + var now = DateTimeOffset.UtcNow; + var network = NetworkArtifactDescriptor(manifest, now, TimeSpan.FromMinutes(5)); + var mapped = ContentTransfer() + .FetchMappedContentAsync(manifest, network, now, TimeSpan.FromMinutes(5)) + .GetAwaiter() + .GetResult(); + var request = new CultMeshBodyValidationRequest + { + BodyId = network.BodyId, + SchemaId = network.SchemaId, + LayoutVersion = network.LayoutVersion, + ProducerEpoch = network.ProducerEpoch, + Sequence = network.Sequence, + Capacity = network.Capacity, + AccessMode = CultMeshBodyAccessMode.ReadOnly, + NowUtc = now + }; + var transport = new CultMeshBodyTransportService( + new ICultMeshBodyTransportAdapter[] + { + new CultMeshMappedBodyAdapter(_assetBodyMappings!), + new CultMeshNetworkBodyAdapter(_ => ReadVerifiedArtifactBytes(manifest)) + }, + descriptor => string.Equals(descriptor.BodyId, manifest.ArtifactId, StringComparison.Ordinal) && + string.Equals(descriptor.SemanticHash, hash, StringComparison.Ordinal)); + var negotiated = transport.NegotiateReadOnly(mapped.Descriptor, network, request); + CurrentAssetBodyTransportKind = negotiated.SelectedTransport; + _assetBodyLeases.Add(negotiated.Lease); + if (negotiated.SelectedTransport == CultMeshBodyTransportKind.SharedFileMapping) + return AssetBundle.LoadFromFile(mapped.VerifiedPath); + + if (negotiated.Lease.Descriptor.ByteSize > int.MaxValue) + throw new InvalidOperationException("Unity cannot lower a network asset body larger than one managed byte array."); + var bytes = new byte[checked((int)negotiated.Lease.Descriptor.ByteSize)]; + negotiated.Lease.CopyTo(0, bytes, 0, bytes.Length); + return AssetBundle.LoadFromMemory(bytes); + } + + private byte[] ReadVerifiedArtifactBytes(CultMeshCdnArtifactManifest manifest) + { + var path = ContentTransfer().FetchAsync(manifest).GetAwaiter().GetResult(); + return File.ReadAllBytes(path); + } + + private static CultMeshBodyDescriptor NetworkArtifactDescriptor( + CultMeshCdnArtifactManifest manifest, + DateTimeOffset now, + TimeSpan leaseDuration) + { + if (manifest.SizeBytes > int.MaxValue) + throw new InvalidOperationException("CultMesh artifact capacity exceeds the current body descriptor bound."); + return new CultMeshBodyDescriptor + { + BodyId = manifest.ArtifactId, + SchemaId = "gamecult.mesh.cdn-artifact.v1", + LayoutVersion = 1, + ByteSize = manifest.SizeBytes, + Capacity = checked((int)manifest.SizeBytes), + ProducerEpoch = 1, + Sequence = 0, + AccessMode = CultMeshBodyAccessMode.ReadOnly, + Synchronization = CultMeshBodySynchronization.ImmutableSequence, + LeaseExpiresAtUnixMs = now.Add(leaseDuration).ToUnixTimeMilliseconds(), + TransportKind = CultMeshBodyTransportKind.Network, + CapabilityToken = CultMeshCdnArtifactManifest.CreateRecordKey(manifest).Value, + SemanticHash = NormalizeContentHash(manifest.ContentHash) + }; } private void PublishReceipt(EveCommandReceiptDocument receipt) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs index 6596fb7..725dc18 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -58,6 +58,15 @@ namespace GameCult.Eve.UnityScene } } + public CultMeshBodyTransportKind? CurrentAssetBodyTransportKind + { + get + { + EnsureTransport(); + return _transport!.CurrentAssetBodyTransportKind; + } + } + public event Action? DocumentAvailable { add { Bridge.DocumentAvailable += value; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 95ddb9f..319e1d0 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -106,6 +106,32 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(wireTypes.Any(type => type == typeof(CultMeshCdnArtifactChunk)), Is.False); } + [Test] + public void LiveTransportBuildsImmutableNetworkFallbackForMappedAssetContent() + { + var manifest = CultMeshCdn.PackArtifact( + "provider/world/windows", + Enumerable.Range(0, 64).Select(value => (byte)value).ToArray()).Manifest; + var now = DateTimeOffset.UtcNow; + var method = typeof(EveUnityCultMeshLiveProviderTransport) + .GetMethod("NetworkArtifactDescriptor", BindingFlags.NonPublic | BindingFlags.Static)!; + + var descriptor = (CultMeshBodyDescriptor)method.Invoke( + null, + new object[] { manifest, now, TimeSpan.FromMinutes(5) })!; + + Assert.That(descriptor.BodyId, Is.EqualTo(manifest.ArtifactId)); + Assert.That(descriptor.SchemaId, Is.EqualTo("gamecult.mesh.cdn-artifact.v1")); + Assert.That(descriptor.ByteSize, Is.EqualTo(manifest.SizeBytes)); + Assert.That(descriptor.Capacity, Is.EqualTo(manifest.SizeBytes)); + Assert.That(descriptor.AccessMode, Is.EqualTo(CultMeshBodyAccessMode.ReadOnly)); + Assert.That(descriptor.Synchronization, Is.EqualTo(CultMeshBodySynchronization.ImmutableSequence)); + Assert.That(descriptor.TransportKind, Is.EqualTo(CultMeshBodyTransportKind.Network)); + Assert.That(descriptor.SemanticHash, Is.EqualTo(manifest.ContentHash)); + Assert.That(descriptor.CapabilityToken, Is.EqualTo( + CultMeshCdnArtifactManifest.CreateRecordKey(manifest).Value)); + } + [Test] public void LiveTransportAuthorizesOnlyExplicitlyAdvertisedBodyProducers() { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index e752da8..0e32936 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -5,7 +5,7 @@ "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { - "org.gamecult.cultlib": "1.0.14", + "org.gamecult.cultlib": "1.0.15", "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2",