Negotiate mapped provider asset bodies
This commit is contained in:
parent
ab05ac5712
commit
8c3028c60a
5 changed files with 126 additions and 6 deletions
|
|
@ -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<EveUnityPlayableWorldClientHost>();
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ namespace GameCult.Eve.UnityScene
|
|||
private readonly Dictionary<string, IReadOnlyDictionary<string, string>> _nativeAssetMetadata =
|
||||
new Dictionary<string, IReadOnlyDictionary<string, string>>(StringComparer.Ordinal);
|
||||
private readonly List<AssetBundle> _assetBundles = new List<AssetBundle>();
|
||||
private readonly List<ICultMeshBodyReadLease> _assetBodyLeases = new List<ICultMeshBodyReadLease>();
|
||||
private readonly Dictionary<string, int> _renderChannelLayers = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
private readonly ConcurrentQueue<object> _liveDocuments = new ConcurrentQueue<object>();
|
||||
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<EveUnitySceneProviderSurfaceDocument>? SurfaceDocumentAvailable;
|
||||
|
||||
public event Action<EveUnityPlayableWorldAssetManifestDocument>? 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)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,15 @@ namespace GameCult.Eve.UnityScene
|
|||
}
|
||||
}
|
||||
|
||||
public CultMeshBodyTransportKind? CurrentAssetBodyTransportKind
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureTransport();
|
||||
return _transport!.CurrentAssetBodyTransportKind;
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<EveUnitySceneProviderSurfaceDocument>? DocumentAvailable
|
||||
{
|
||||
add { Bridge.DocumentAvailable += value; }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Reference in a new issue