From dfd1c4cfaeaa55aa823a6d148727dde1de569030 Mon Sep 17 00:00:00 2001 From: "JSD\\13999" <1399945104@qq.com> Date: Mon, 18 May 2026 11:54:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20P0=20=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E5=92=8C=E5=BC=95=E7=94=A8=E8=AE=A1=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- My project/Assets/FlowScope/Runtime.meta | 8 + .../Resources/AddressablesResourceService.cs | 235 ++++++++++++++++++ .../AddressablesResourceService.cs.meta | 11 + .../Runtime/Resources/ResourceEntry.cs | 61 +++++ .../Runtime/Resources/ResourceEntry.cs.meta | 11 + .../Runtime/Resources/ResourceGroup.cs | 44 ++++ .../Runtime/Resources/ResourceGroup.cs.meta | 11 + .../Runtime/Resources/ResourceHandle.cs | 51 ++++ .../Runtime/Resources/ResourceHandle.cs.meta | 11 + My project/Assets/FlowScope/Tests.meta | 8 + .../Assets/FlowScope/Tests/EditMode.meta | 8 + .../FlowScope/Tests/EditMode/Resources.meta | 8 + .../EditMode/Resources/ResourceGroupTests.cs | 56 +++++ .../Resources/ResourceGroupTests.cs.meta | 11 + .../Assets/FlowScope/Tests/PlayMode.meta | 8 + .../FlowScope/Tests/PlayMode/Resources.meta | 8 + .../AddressablesResourceServiceTests.cs | 59 +++++ .../AddressablesResourceServiceTests.cs.meta | 11 + 18 files changed, 620 insertions(+) create mode 100644 My project/Assets/FlowScope/Runtime.meta create mode 100644 My project/Assets/FlowScope/Runtime/Resources/AddressablesResourceService.cs create mode 100644 My project/Assets/FlowScope/Runtime/Resources/AddressablesResourceService.cs.meta create mode 100644 My project/Assets/FlowScope/Runtime/Resources/ResourceEntry.cs create mode 100644 My project/Assets/FlowScope/Runtime/Resources/ResourceEntry.cs.meta create mode 100644 My project/Assets/FlowScope/Runtime/Resources/ResourceGroup.cs create mode 100644 My project/Assets/FlowScope/Runtime/Resources/ResourceGroup.cs.meta create mode 100644 My project/Assets/FlowScope/Runtime/Resources/ResourceHandle.cs create mode 100644 My project/Assets/FlowScope/Runtime/Resources/ResourceHandle.cs.meta create mode 100644 My project/Assets/FlowScope/Tests.meta create mode 100644 My project/Assets/FlowScope/Tests/EditMode.meta create mode 100644 My project/Assets/FlowScope/Tests/EditMode/Resources.meta create mode 100644 My project/Assets/FlowScope/Tests/EditMode/Resources/ResourceGroupTests.cs create mode 100644 My project/Assets/FlowScope/Tests/EditMode/Resources/ResourceGroupTests.cs.meta create mode 100644 My project/Assets/FlowScope/Tests/PlayMode.meta create mode 100644 My project/Assets/FlowScope/Tests/PlayMode/Resources.meta create mode 100644 My project/Assets/FlowScope/Tests/PlayMode/Resources/AddressablesResourceServiceTests.cs create mode 100644 My project/Assets/FlowScope/Tests/PlayMode/Resources/AddressablesResourceServiceTests.cs.meta diff --git a/My project/Assets/FlowScope/Runtime.meta b/My project/Assets/FlowScope/Runtime.meta new file mode 100644 index 0000000..2854f26 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8eaa19d06f5e46268a8362862c912da8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Runtime/Resources/AddressablesResourceService.cs b/My project/Assets/FlowScope/Runtime/Resources/AddressablesResourceService.cs new file mode 100644 index 0000000..532891c --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Resources/AddressablesResourceService.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace FlowScope.Resources +{ + public sealed class AddressablesResourceService : IResourceService + { + private readonly object _gate = new(); + private readonly Dictionary _entries = new(); + private readonly Dictionary> _loads = new(); + private readonly Func> _loadAssetAsync; + private readonly Action _releaseAsset; + + public AddressablesResourceService() + : this(AddressablesReflectionBackend.LoadAsync, AddressablesReflectionBackend.Release) + { + } + + public AddressablesResourceService( + Func> loadAssetAsync, + Action releaseAsset) + { + _loadAssetAsync = loadAssetAsync ?? throw new ArgumentNullException(nameof(loadAssetAsync)); + _releaseAsset = releaseAsset ?? throw new ArgumentNullException(nameof(releaseAsset)); + } + + public async Task> LoadAsync( + string key, + CancellationToken cancellationToken) + where T : class + { + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentException("Resource key is required.", nameof(key)); + } + + cancellationToken.ThrowIfCancellationRequested(); + var resourceKey = new ResourceKey(key, typeof(T)); + Task loadTask; + + lock (_gate) + { + if (_entries.TryGetValue(resourceKey, out var entry)) + { + return entry.CreateHandle(); + } + + if (!_loads.TryGetValue(resourceKey, out loadTask)) + { + loadTask = LoadEntryAsync(key, cancellationToken); + _loads.Add(resourceKey, loadTask); + } + } + + try + { + var loaded = await loadTask; + lock (_gate) + { + if (!_entries.ContainsKey(resourceKey)) + { + _entries.Add(resourceKey, loaded); + } + + _loads.Remove(resourceKey); + return _entries[resourceKey].CreateHandle(); + } + } + catch + { + lock (_gate) + { + _loads.Remove(resourceKey); + } + + throw; + } + } + + public IResourceGroup CreateGroup() + { + return new ResourceGroup(); + } + + private async Task LoadEntryAsync(string key, CancellationToken cancellationToken) + where T : class + { + object asset; + try + { + asset = await _loadAssetAsync(key, typeof(T), cancellationToken); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception exception) + { + throw new InvalidOperationException( + $"Failed to load Addressables resource '{key}' as {typeof(T).Name}.", + exception); + } + + if (asset == null) + { + throw new InvalidOperationException( + $"Addressables resource '{key}' returned null for {typeof(T).Name}."); + } + + return new ResourceEntry(key, typeof(T), asset, ReleaseEntry); + } + + private void ReleaseEntry(string key, Type assetType, object asset) + { + lock (_gate) + { + _entries.Remove(new ResourceKey(key, assetType)); + } + + _releaseAsset(key, asset); + } + + private readonly struct ResourceKey : IEquatable + { + private readonly string _key; + private readonly Type _type; + + public ResourceKey(string key, Type type) + { + _key = key; + _type = type; + } + + public bool Equals(ResourceKey other) + { + return _key == other._key && _type == other._type; + } + + public override bool Equals(object obj) + { + return obj is ResourceKey other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return ((_key != null ? _key.GetHashCode() : 0) * 397) ^ + (_type != null ? _type.GetHashCode() : 0); + } + } + } + + private static class AddressablesReflectionBackend + { + private const string AddressablesTypeName = + "UnityEngine.AddressableAssets.Addressables, Unity.Addressables"; + + public static async Task LoadAsync( + string key, + Type assetType, + CancellationToken cancellationToken) + { + var addressablesType = Type.GetType(AddressablesTypeName); + if (addressablesType == null) + { + throw new InvalidOperationException( + "Unity Addressables package is not available. P0 ResourceService supports Addressables only."); + } + + var method = FindLoadAssetMethod(addressablesType).MakeGenericMethod(assetType); + var handle = method.Invoke(null, new object[] { key }); + var task = (Task)handle.GetType().GetProperty("Task", BindingFlags.Instance | BindingFlags.Public) + ?.GetValue(handle); + if (task == null) + { + throw new InvalidOperationException("Addressables LoadAssetAsync handle did not expose Task."); + } + + await WaitWithCancellation(task, cancellationToken); + return task.GetType().GetProperty("Result")?.GetValue(task); + } + + public static void Release(string key, object asset) + { + var addressablesType = Type.GetType(AddressablesTypeName); + var release = addressablesType?.GetMethod( + "Release", + BindingFlags.Static | BindingFlags.Public, + null, + new[] { typeof(object) }, + null); + release?.Invoke(null, new[] { asset }); + } + + private static MethodInfo FindLoadAssetMethod(Type addressablesType) + { + foreach (var method in addressablesType.GetMethods(BindingFlags.Static | BindingFlags.Public)) + { + if (method.Name == "LoadAssetAsync" && + method.IsGenericMethodDefinition && + method.GetParameters().Length == 1) + { + return method; + } + } + + throw new InvalidOperationException("Addressables.LoadAssetAsync(key) was not found."); + } + + private static async Task WaitWithCancellation(Task task, CancellationToken cancellationToken) + { + if (!cancellationToken.CanBeCanceled) + { + await task; + return; + } + + var cancellation = new TaskCompletionSource(); + using (cancellationToken.Register(() => cancellation.TrySetResult(true))) + { + if (await Task.WhenAny(task, cancellation.Task) == cancellation.Task) + { + throw new OperationCanceledException(cancellationToken); + } + } + + await task; + } + } + } +} diff --git a/My project/Assets/FlowScope/Runtime/Resources/AddressablesResourceService.cs.meta b/My project/Assets/FlowScope/Runtime/Resources/AddressablesResourceService.cs.meta new file mode 100644 index 0000000..3a210c0 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Resources/AddressablesResourceService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c9294e0aadd246de906d8cfb96d81878 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Runtime/Resources/ResourceEntry.cs b/My project/Assets/FlowScope/Runtime/Resources/ResourceEntry.cs new file mode 100644 index 0000000..02195cc --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Resources/ResourceEntry.cs @@ -0,0 +1,61 @@ +using System; + +namespace FlowScope.Resources +{ + internal sealed class ResourceEntry + { + private readonly Action _release; + private int _referenceCount; + private bool _released; + + public ResourceEntry(string key, object asset, Action release) + : this(key, asset?.GetType(), asset, (_, _, releasedAsset) => release(key, releasedAsset)) + { + } + + public ResourceEntry(string key, Type assetType, object asset, Action release) + { + Key = string.IsNullOrWhiteSpace(key) + ? throw new ArgumentException("Resource key is required.", nameof(key)) + : key; + AssetType = assetType ?? throw new ArgumentNullException(nameof(assetType)); + Asset = asset ?? throw new ArgumentNullException(nameof(asset)); + _release = release ?? throw new ArgumentNullException(nameof(release)); + } + + public string Key { get; } + public Type AssetType { get; } + public object Asset { get; private set; } + public int ReferenceCount => _referenceCount; + + public ResourceHandle CreateHandle() where T : class + { + if (_released) + { + throw new ObjectDisposedException(nameof(ResourceEntry)); + } + + _referenceCount++; + return new ResourceHandle(this); + } + + public void ReleaseHandle() + { + if (_released) + { + return; + } + + _referenceCount--; + if (_referenceCount > 0) + { + return; + } + + _released = true; + var asset = Asset; + Asset = null; + _release(Key, AssetType, asset); + } + } +} diff --git a/My project/Assets/FlowScope/Runtime/Resources/ResourceEntry.cs.meta b/My project/Assets/FlowScope/Runtime/Resources/ResourceEntry.cs.meta new file mode 100644 index 0000000..f535b2d --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Resources/ResourceEntry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 02477283d77540ed9be0b002bb05f37a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Runtime/Resources/ResourceGroup.cs b/My project/Assets/FlowScope/Runtime/Resources/ResourceGroup.cs new file mode 100644 index 0000000..bdfcac4 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Resources/ResourceGroup.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + +namespace FlowScope.Resources +{ + public sealed class ResourceGroup : IResourceGroup + { + private readonly HashSet _handles = new(); + private bool _disposed; + + public int Count => _handles.Count; + + public void Add(IResourceHandle handle) where T : class + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(ResourceGroup)); + } + + if (handle == null) + { + throw new ArgumentNullException(nameof(handle)); + } + + _handles.Add(handle); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + foreach (var handle in _handles) + { + handle.Dispose(); + } + + _handles.Clear(); + } + } +} diff --git a/My project/Assets/FlowScope/Runtime/Resources/ResourceGroup.cs.meta b/My project/Assets/FlowScope/Runtime/Resources/ResourceGroup.cs.meta new file mode 100644 index 0000000..88d4a88 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Resources/ResourceGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 896ce428279a4740b44cebeb2c0b635e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Runtime/Resources/ResourceHandle.cs b/My project/Assets/FlowScope/Runtime/Resources/ResourceHandle.cs new file mode 100644 index 0000000..b571d57 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Resources/ResourceHandle.cs @@ -0,0 +1,51 @@ +using System; + +namespace FlowScope.Resources +{ + public sealed class ResourceHandle : IResourceHandle where T : class + { + private readonly ResourceEntry _entry; + private readonly Action _release; + private T _asset; + + internal ResourceHandle(ResourceEntry entry) + { + _entry = entry ?? throw new ArgumentNullException(nameof(entry)); + Key = entry.Key; + _asset = (T)entry.Asset; + } + + public ResourceHandle(string key, T asset, Action release) + { + Key = string.IsNullOrWhiteSpace(key) + ? throw new ArgumentException("Resource key is required.", nameof(key)) + : key; + _asset = asset ?? throw new ArgumentNullException(nameof(asset)); + _release = release ?? throw new ArgumentNullException(nameof(release)); + } + + public string Key { get; } + public T Asset => IsDisposed ? null : _asset; + public bool IsDisposed { get; private set; } + + public void Dispose() + { + if (IsDisposed) + { + return; + } + + IsDisposed = true; + var asset = _asset; + _asset = null; + + if (_entry != null) + { + _entry.ReleaseHandle(); + return; + } + + _release(Key, asset); + } + } +} diff --git a/My project/Assets/FlowScope/Runtime/Resources/ResourceHandle.cs.meta b/My project/Assets/FlowScope/Runtime/Resources/ResourceHandle.cs.meta new file mode 100644 index 0000000..fd1cc36 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Resources/ResourceHandle.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ab4e98e842c34a09a324062b57b09009 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests.meta b/My project/Assets/FlowScope/Tests.meta new file mode 100644 index 0000000..659eb85 --- /dev/null +++ b/My project/Assets/FlowScope/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3fc2c875b65a431fb05975eee30c358a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests/EditMode.meta b/My project/Assets/FlowScope/Tests/EditMode.meta new file mode 100644 index 0000000..7a833de --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a01312d2a2ae4e38bb24ac988df7ed8a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests/EditMode/Resources.meta b/My project/Assets/FlowScope/Tests/EditMode/Resources.meta new file mode 100644 index 0000000..4f6e2d2 --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fb94c8e882a64036908cf7520c43fbb5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests/EditMode/Resources/ResourceGroupTests.cs b/My project/Assets/FlowScope/Tests/EditMode/Resources/ResourceGroupTests.cs new file mode 100644 index 0000000..812a7a0 --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode/Resources/ResourceGroupTests.cs @@ -0,0 +1,56 @@ +using FlowScope.Resources; +using NUnit.Framework; + +namespace FlowScope.Tests.EditMode.Resources +{ + public sealed class ResourceGroupTests + { + [Test] + public void DisposeHandle_ReturnsNullAssetAndReleasesOnce() + { + var releaseCount = 0; + var asset = new TestAsset(); + var handle = new ResourceHandle( + "ui/main", + asset, + (_, _) => releaseCount++); + + Assert.AreSame(asset, handle.Asset); + + handle.Dispose(); + handle.Dispose(); + + Assert.IsTrue(handle.IsDisposed); + Assert.IsNull(handle.Asset); + Assert.AreEqual(1, releaseCount); + } + + [Test] + public void DisposeGroup_DisposesHandlesAndIgnoresDuplicateAdd() + { + var releaseCount = 0; + var handle = new ResourceHandle( + "ui/main", + new TestAsset(), + (_, _) => releaseCount++); + var group = new ResourceGroup(); + + group.Add(handle); + group.Add(handle); + + Assert.AreEqual(1, group.Count); + + group.Dispose(); + group.Dispose(); + + Assert.AreEqual(0, group.Count); + Assert.IsTrue(handle.IsDisposed); + Assert.IsNull(handle.Asset); + Assert.AreEqual(1, releaseCount); + } + + private sealed class TestAsset + { + } + } +} diff --git a/My project/Assets/FlowScope/Tests/EditMode/Resources/ResourceGroupTests.cs.meta b/My project/Assets/FlowScope/Tests/EditMode/Resources/ResourceGroupTests.cs.meta new file mode 100644 index 0000000..0e2fe23 --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode/Resources/ResourceGroupTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ec0e64759485405a99ee94f3fc5cae52 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests/PlayMode.meta b/My project/Assets/FlowScope/Tests/PlayMode.meta new file mode 100644 index 0000000..f9ee4ac --- /dev/null +++ b/My project/Assets/FlowScope/Tests/PlayMode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 83547470a4584f6dbc0590f5ebfe9392 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests/PlayMode/Resources.meta b/My project/Assets/FlowScope/Tests/PlayMode/Resources.meta new file mode 100644 index 0000000..a462d2b --- /dev/null +++ b/My project/Assets/FlowScope/Tests/PlayMode/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cd8c9e7d9aa8490bacb57789aab34daa +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests/PlayMode/Resources/AddressablesResourceServiceTests.cs b/My project/Assets/FlowScope/Tests/PlayMode/Resources/AddressablesResourceServiceTests.cs new file mode 100644 index 0000000..e739bba --- /dev/null +++ b/My project/Assets/FlowScope/Tests/PlayMode/Resources/AddressablesResourceServiceTests.cs @@ -0,0 +1,59 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FlowScope.Resources; +using NUnit.Framework; + +namespace FlowScope.Tests.PlayMode.Resources +{ + public sealed class AddressablesResourceServiceTests + { + [Test] + public async Task LoadAsync_WhenSameKeyLoadsConcurrently_SharesOneBackendLoad() + { + var backendLoads = 0; + var releaseCount = 0; + var gate = new TaskCompletionSource(); + var service = new AddressablesResourceService( + async (_, _, _) => + { + backendLoads++; + return await gate.Task; + }, + (_, _) => releaseCount++); + + var first = service.LoadAsync("shared", CancellationToken.None); + var second = service.LoadAsync("shared", CancellationToken.None); + + gate.SetResult(new TestAsset()); + var firstHandle = await first; + var secondHandle = await second; + + Assert.AreEqual(1, backendLoads); + Assert.AreSame(firstHandle.Asset, secondHandle.Asset); + + firstHandle.Dispose(); + Assert.AreEqual(0, releaseCount); + secondHandle.Dispose(); + Assert.AreEqual(1, releaseCount); + } + + [Test] + public void LoadAsync_WhenBackendFails_IncludesKeyAndBackend() + { + var service = new AddressablesResourceService( + (_, _, _) => throw new InvalidOperationException("missing"), + (_, _) => { }); + + var exception = Assert.ThrowsAsync( + () => service.LoadAsync("missing-key", CancellationToken.None)); + + StringAssert.Contains("missing-key", exception.Message); + StringAssert.Contains("Addressables", exception.Message); + } + + private sealed class TestAsset + { + } + } +} diff --git a/My project/Assets/FlowScope/Tests/PlayMode/Resources/AddressablesResourceServiceTests.cs.meta b/My project/Assets/FlowScope/Tests/PlayMode/Resources/AddressablesResourceServiceTests.cs.meta new file mode 100644 index 0000000..e13c995 --- /dev/null +++ b/My project/Assets/FlowScope/Tests/PlayMode/Resources/AddressablesResourceServiceTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6a8dd01aed1f4fac97325a858dc56e78 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: