feat(sdk): migrate to EventBus 2.0
Replace the legacy EventBase runtime with generated multi-bus bindings and explicit scheduling. Migrate app, data, network, demo, and mod consumers; add generated network-event registration and owner-scoped mod content overrides.
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkContext;
|
||||
using ShrinkEventBus;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
@@ -66,6 +67,16 @@ namespace ShrinkModFramework
|
||||
instance.OnRegisterContent(modContext);
|
||||
handle.State = ShrinkModState.ContentRegistered;
|
||||
|
||||
IDisposable eventBinding = null;
|
||||
ctx.Effect(
|
||||
() => eventBinding = ShrinkModOptionalRuntimeIntegration.TryAttachEventBusInstance(
|
||||
instance.GetType(), instance, handle.Info.ModId, _host.VerboseLogging),
|
||||
() =>
|
||||
{
|
||||
eventBinding?.Dispose();
|
||||
EventBus.RemoveBus(ShrinkBusKey.Mod(handle.Info.ModId));
|
||||
});
|
||||
|
||||
// 模组网络 handler 以 ModId 为归属键,可在失败或卸载时整体撤回。
|
||||
ctx.Effect(() => { }, () => ShrinkModNetworkManager.UnregisterHandlers(handle.Info.ModId));
|
||||
instance.OnInitialize(modContext);
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace ShrinkModFramework
|
||||
public IReadOnlyList<ShrinkModComponentSource> CurrentSources => _currentSources.ToArray();
|
||||
internal ShrinkModRegistryManager RegistryManager { get; }
|
||||
|
||||
public ShrinkModRegistry<T> GetOrCreateRegistry<T>(string name) =>
|
||||
public IReadOnlyShrinkModRegistry<T> GetOrCreateRegistry<T>(string name) =>
|
||||
RegistryManager.GetOrCreateRegistry<T>(name);
|
||||
|
||||
public static string GetModKey(string modId)
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace ShrinkModFramework
|
||||
return _host.Mods;
|
||||
}
|
||||
|
||||
public static ShrinkModRegistry<T> GetOrCreateRegistry<T>(string name) =>
|
||||
public static IReadOnlyShrinkModRegistry<T> GetOrCreateRegistry<T>(string name) =>
|
||||
Host.GetOrCreateRegistry<T>(name);
|
||||
|
||||
internal static void ResetForDomainReload()
|
||||
|
||||
@@ -27,11 +27,22 @@ namespace ShrinkModFramework
|
||||
public IReadOnlyDictionary<string, ShrinkModHandle> LoadedMods => _loadedMods;
|
||||
public bool IsContextManaged => _effectContext != null;
|
||||
|
||||
public ShrinkModRegistry<T> GetRegistry<T>(string name) => _registryManager.GetOrCreateRegistry<T>(name);
|
||||
public IReadOnlyShrinkModRegistry<T> GetRegistry<T>(string name) =>
|
||||
_registryManager.GetOrCreateRegistry<T>(name);
|
||||
|
||||
public bool TryGetRegistry<T>(string name, out ShrinkModRegistry<T> registry)
|
||||
public bool TryGetRegistry<T>(string name, out IReadOnlyShrinkModRegistry<T> registry)
|
||||
=> _registryManager.TryGetRegistry(name, out registry);
|
||||
|
||||
/// <summary>用当前 ModId 作为 namespace 注册内容,并返回最终完整键。</summary>
|
||||
public string RegisterContent<T>(string registryName, string localKey, T value) =>
|
||||
_registryManager.GetOrCreateMutableRegistry<T>(registryName)
|
||||
.RegisterNamespaced(ModInfo.ModId, localKey, value);
|
||||
|
||||
/// <summary>以当前模组为 owner 覆盖已有内容;同目标、同优先级冲突会直接失败。</summary>
|
||||
public void OverrideContent<T>(string registryName, string targetKey, int priority, T value) =>
|
||||
_registryManager.GetOrCreateMutableRegistry<T>(registryName)
|
||||
.RegisterOverride(ModInfo.ModId, targetKey, priority, value);
|
||||
|
||||
public bool IsModLoaded(string modId) => !string.IsNullOrWhiteSpace(modId) && _loadedMods.ContainsKey(modId);
|
||||
|
||||
public bool TryGetLoadedMod(string modId, out ShrinkModHandle handle)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public sealed class ShrinkModHandle
|
||||
@@ -6,6 +8,7 @@ namespace ShrinkModFramework
|
||||
public IShrinkMod Instance { get; }
|
||||
public ShrinkModState State { get; internal set; }
|
||||
public long Generation { get; internal set; }
|
||||
internal IDisposable EventBinding { get; set; }
|
||||
|
||||
internal ShrinkModHandle(ShrinkModInfo info, IShrinkMod instance)
|
||||
{
|
||||
|
||||
+15
-16
@@ -1,21 +1,24 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using ShrinkEventBus;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
internal static class ShrinkModOptionalRuntimeIntegration
|
||||
{
|
||||
public static void RegisterModInstance(IShrinkMod modInstance, ShrinkModInfo modInfo, bool verboseLogging)
|
||||
public static IDisposable RegisterModInstance(IShrinkMod modInstance, ShrinkModInfo modInfo,
|
||||
bool verboseLogging)
|
||||
{
|
||||
if (modInstance == null || modInfo == null)
|
||||
return;
|
||||
return null;
|
||||
|
||||
var modType = modInstance.GetType();
|
||||
TryRegisterEventBusInstance(modType, modInstance, modInfo.ModId, verboseLogging);
|
||||
var eventBinding = TryAttachEventBusInstance(modType, modInstance, modInfo.ModId, verboseLogging);
|
||||
TryRegisterCommandInstance(modType, modInstance, modInfo.ModId, verboseLogging);
|
||||
TryRegisterNetworkInstance(modType, modInstance, modInfo.ModId, verboseLogging);
|
||||
return eventBinding;
|
||||
}
|
||||
|
||||
public static void RefreshGlobalBindings(bool verboseLogging)
|
||||
@@ -23,29 +26,25 @@ namespace ShrinkModFramework
|
||||
TryRefreshNetworkEventBusBridge(verboseLogging);
|
||||
}
|
||||
|
||||
private static void TryRegisterEventBusInstance(Type modType, object modInstance, string modId, bool verboseLogging)
|
||||
public static IDisposable TryAttachEventBusInstance(Type modType, object modInstance,
|
||||
string modId, bool verboseLogging)
|
||||
{
|
||||
if (!HasAttribute(modType, "ShrinkEventBus.EventBusSubscriberAttribute", "ShrinkEventBus.Runtime"))
|
||||
return;
|
||||
if (modInstance is not IShrinkGeneratedSubscriber)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var eventBusType = FindType("ShrinkEventBus.EventBus", "ShrinkEventBus.Runtime");
|
||||
var autoRegisterMethod = eventBusType?.GetMethod("AutoRegister",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
new[] { typeof(object) },
|
||||
null);
|
||||
if (autoRegisterMethod == null)
|
||||
return;
|
||||
|
||||
autoRegisterMethod.Invoke(null, new[] { modInstance });
|
||||
var busKey = ShrinkBusKey.Mod(modId);
|
||||
EventBus.GetOrCreateBus(busKey, ShrinkBusOptions.DedicatedThread());
|
||||
var binding = EventBus.Attach(modInstance, busKey);
|
||||
if (verboseLogging)
|
||||
Debug.Log($"[ShrinkModFramework] 模组 {modId} 已接入 ShrinkEventBus 实例订阅。");
|
||||
return binding;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[ShrinkModFramework] 模组 {modId} 接入 ShrinkEventBus 失败:{ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using ShrinkEventBus;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
@@ -106,6 +107,9 @@ namespace ShrinkModFramework
|
||||
{
|
||||
foreach (var handle in newHandles)
|
||||
{
|
||||
handle.EventBinding?.Dispose();
|
||||
handle.EventBinding = null;
|
||||
EventBus.RemoveBus(ShrinkBusKey.Mod(handle.Info.ModId));
|
||||
LoadedMods.Remove(handle.Info.ModId);
|
||||
LoadSequence.Remove(handle);
|
||||
}
|
||||
@@ -123,7 +127,7 @@ namespace ShrinkModFramework
|
||||
return false;
|
||||
}
|
||||
|
||||
public static ShrinkModRegistry<T> GetOrCreateRegistry<T>(string name)
|
||||
public static IReadOnlyShrinkModRegistry<T> GetOrCreateRegistry<T>(string name)
|
||||
=> ShrinkModCordisRuntime.IsInitialized
|
||||
? ShrinkModCordisRuntime.GetOrCreateRegistry<T>(name)
|
||||
: RegistryManager.GetOrCreateRegistry<T>(name);
|
||||
@@ -131,6 +135,7 @@ namespace ShrinkModFramework
|
||||
internal static void ResetForTesting()
|
||||
{
|
||||
ShrinkModCordisRuntime.ResetForTesting();
|
||||
DisposeLegacyEventBindings();
|
||||
LoadedMods.Clear();
|
||||
LoadSequence.Clear();
|
||||
RegistryManager.Clear();
|
||||
@@ -145,6 +150,7 @@ namespace ShrinkModFramework
|
||||
internal static void ResetForDomainReload()
|
||||
{
|
||||
ShrinkModCordisRuntime.ResetForDomainReload();
|
||||
DisposeLegacyEventBindings();
|
||||
LoadedMods.Clear();
|
||||
LoadSequence.Clear();
|
||||
RegistryManager.Clear();
|
||||
@@ -383,11 +389,22 @@ namespace ShrinkModFramework
|
||||
private static void RegisterOptionalRuntimeIntegrations(IEnumerable<ShrinkModHandle> handles, bool verboseLogging)
|
||||
{
|
||||
foreach (var handle in handles)
|
||||
ShrinkModOptionalRuntimeIntegration.RegisterModInstance(handle.Instance, handle.Info, verboseLogging);
|
||||
handle.EventBinding = ShrinkModOptionalRuntimeIntegration.RegisterModInstance(
|
||||
handle.Instance, handle.Info, verboseLogging);
|
||||
|
||||
ShrinkModOptionalRuntimeIntegration.RefreshGlobalBindings(verboseLogging);
|
||||
}
|
||||
|
||||
private static void DisposeLegacyEventBindings()
|
||||
{
|
||||
foreach (var handle in LoadedMods.Values)
|
||||
{
|
||||
handle.EventBinding?.Dispose();
|
||||
handle.EventBinding = null;
|
||||
EventBus.RemoveBus(ShrinkBusKey.Mod(handle.Info.ModId));
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunPhase(IEnumerable<ShrinkModHandle> handles, ShrinkModState targetState, bool verboseLogging,
|
||||
Action<IShrinkMod, ShrinkModContext> callback, string phaseName)
|
||||
{
|
||||
|
||||
@@ -4,14 +4,28 @@ using System.Linq;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public interface IReadOnlyShrinkModRegistry<T>
|
||||
{
|
||||
string Name { get; }
|
||||
IReadOnlyCollection<ShrinkRegistryEntry<T>> Entries { get; }
|
||||
IReadOnlyCollection<ShrinkRegistryEntry<T>> BaseEntries { get; }
|
||||
bool Contains(string key);
|
||||
bool TryGet(string key, out T value);
|
||||
bool TryGetEntry(string key, out ShrinkRegistryEntry<T> entry);
|
||||
IReadOnlyList<ShrinkRegistryEntry<T>> GetOverrideCandidates(string key);
|
||||
}
|
||||
|
||||
internal interface IShrinkOwnedRegistry
|
||||
{
|
||||
void RemoveOwnedEntries(string ownerModId);
|
||||
}
|
||||
|
||||
public sealed class ShrinkModRegistry<T> : IShrinkOwnedRegistry
|
||||
internal sealed class ShrinkModRegistry<T> : IReadOnlyShrinkModRegistry<T>, IShrinkOwnedRegistry
|
||||
{
|
||||
private readonly Dictionary<string, ShrinkRegistryEntry<T>> _entries = new();
|
||||
private readonly Dictionary<string, ShrinkRegistryEntry<T>> _baseEntries =
|
||||
new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, List<ShrinkRegistryEntry<T>>> _overrides =
|
||||
new(StringComparer.Ordinal);
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
@@ -23,27 +37,88 @@ namespace ShrinkModFramework
|
||||
Name = name.Trim();
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<ShrinkRegistryEntry<T>> Entries => _entries.Values.ToArray();
|
||||
/// <summary>当前生效项;存在覆盖时返回最高优先级的覆盖项。</summary>
|
||||
public IReadOnlyCollection<ShrinkRegistryEntry<T>> Entries => _baseEntries.Keys
|
||||
.OrderBy(key => key, StringComparer.Ordinal)
|
||||
.Select(ResolveEntry)
|
||||
.ToArray();
|
||||
|
||||
public void Register(string ownerModId, string key, T value)
|
||||
/// <summary>不应用覆盖的基础注册项快照。</summary>
|
||||
public IReadOnlyCollection<ShrinkRegistryEntry<T>> BaseEntries => _baseEntries.Values
|
||||
.OrderBy(entry => entry.Key, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
private void RegisterBase(string ownerModId, string key, T value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ownerModId))
|
||||
throw new ArgumentException("归属模组 ID 不能为空。", nameof(ownerModId));
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentException("注册键不能为空。", nameof(key));
|
||||
|
||||
key = key.Trim();
|
||||
if (_entries.ContainsKey(key))
|
||||
ownerModId = NormalizeOwner(ownerModId);
|
||||
key = NormalizeKey(key);
|
||||
if (_baseEntries.ContainsKey(key))
|
||||
throw new InvalidOperationException($"注册表 '{Name}' 中已存在键 '{key}'。");
|
||||
|
||||
_entries.Add(key, new ShrinkRegistryEntry<T>(ownerModId.Trim(), key, value));
|
||||
_baseEntries.Add(key, new ShrinkRegistryEntry<T>(ownerModId, key, value));
|
||||
}
|
||||
|
||||
public bool Contains(string key) => !string.IsNullOrWhiteSpace(key) && _entries.ContainsKey(key.Trim());
|
||||
/// <summary>
|
||||
/// 使用 owner 作为 namespace 注册内容,并返回最终键。localKey 只能是局部名称,不能包含冒号。
|
||||
/// </summary>
|
||||
public string RegisterNamespaced(string ownerModId, string localKey, T value)
|
||||
{
|
||||
ownerModId = NormalizeOwner(ownerModId);
|
||||
if (ownerModId.IndexOf(':') >= 0)
|
||||
throw new ArgumentException("归属模组 ID 不能包含 ':'。", nameof(ownerModId));
|
||||
if (string.IsNullOrWhiteSpace(localKey))
|
||||
throw new ArgumentException("局部注册键不能为空。", nameof(localKey));
|
||||
|
||||
localKey = localKey.Trim();
|
||||
if (localKey.IndexOf(':') >= 0)
|
||||
throw new ArgumentException("局部注册键不能包含 ':'。", nameof(localKey));
|
||||
|
||||
var key = ownerModId + ":" + localKey;
|
||||
RegisterBase(ownerModId, key, value);
|
||||
return key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为已有基础项注册显式覆盖。优先级越高越先命中;同一目标的相同优先级视为硬冲突。
|
||||
/// </summary>
|
||||
public void RegisterOverride(string ownerModId, string targetKey, int priority, T value)
|
||||
{
|
||||
ownerModId = NormalizeOwner(ownerModId);
|
||||
targetKey = NormalizeKey(targetKey);
|
||||
if (!_baseEntries.ContainsKey(targetKey))
|
||||
throw new InvalidOperationException(
|
||||
$"注册表 '{Name}' 的覆盖目标 '{targetKey}' 不存在。");
|
||||
|
||||
if (!_overrides.TryGetValue(targetKey, out var candidates))
|
||||
{
|
||||
candidates = new List<ShrinkRegistryEntry<T>>();
|
||||
_overrides.Add(targetKey, candidates);
|
||||
}
|
||||
|
||||
if (candidates.Any(candidate => candidate.Priority == priority))
|
||||
throw new InvalidOperationException(
|
||||
$"注册表 '{Name}' 的键 '{targetKey}' 在优先级 {priority} 存在覆盖冲突。");
|
||||
|
||||
candidates.Add(ShrinkRegistryEntry<T>.CreateOverride(ownerModId, targetKey, priority, value));
|
||||
}
|
||||
|
||||
public IReadOnlyList<ShrinkRegistryEntry<T>> GetOverrideCandidates(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || !_overrides.TryGetValue(key.Trim(), out var candidates))
|
||||
return Array.Empty<ShrinkRegistryEntry<T>>();
|
||||
|
||||
return candidates
|
||||
.OrderByDescending(candidate => candidate.Priority)
|
||||
.ThenBy(candidate => candidate.OwnerModId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public bool Contains(string key) =>
|
||||
!string.IsNullOrWhiteSpace(key) && _baseEntries.ContainsKey(key.Trim());
|
||||
|
||||
public bool TryGet(string key, out T value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(key) && _entries.TryGetValue(key.Trim(), out var entry))
|
||||
if (TryGetEntry(key, out var entry))
|
||||
{
|
||||
value = entry.Value;
|
||||
return true;
|
||||
@@ -55,8 +130,11 @@ namespace ShrinkModFramework
|
||||
|
||||
public bool TryGetEntry(string key, out ShrinkRegistryEntry<T> entry)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(key))
|
||||
return _entries.TryGetValue(key.Trim(), out entry);
|
||||
if (!string.IsNullOrWhiteSpace(key) && _baseEntries.ContainsKey(key.Trim()))
|
||||
{
|
||||
entry = ResolveEntry(key.Trim());
|
||||
return true;
|
||||
}
|
||||
|
||||
entry = default;
|
||||
return false;
|
||||
@@ -67,12 +145,52 @@ namespace ShrinkModFramework
|
||||
if (string.IsNullOrWhiteSpace(ownerModId))
|
||||
return;
|
||||
|
||||
var keys = _entries
|
||||
ownerModId = ownerModId.Trim();
|
||||
var keys = _baseEntries
|
||||
.Where(pair => string.Equals(pair.Value.OwnerModId, ownerModId, StringComparison.Ordinal))
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray();
|
||||
foreach (var key in keys)
|
||||
_entries.Remove(key);
|
||||
_baseEntries.Remove(key);
|
||||
|
||||
var emptyOverrideTargets = new List<string>();
|
||||
foreach (var pair in _overrides)
|
||||
{
|
||||
pair.Value.RemoveAll(candidate =>
|
||||
string.Equals(candidate.OwnerModId, ownerModId, StringComparison.Ordinal));
|
||||
if (pair.Value.Count == 0)
|
||||
emptyOverrideTargets.Add(pair.Key);
|
||||
}
|
||||
|
||||
foreach (var targetKey in emptyOverrideTargets)
|
||||
_overrides.Remove(targetKey);
|
||||
}
|
||||
|
||||
private ShrinkRegistryEntry<T> ResolveEntry(string key)
|
||||
{
|
||||
if (_overrides.TryGetValue(key, out var candidates) && candidates.Count > 0)
|
||||
{
|
||||
return candidates
|
||||
.OrderByDescending(candidate => candidate.Priority)
|
||||
.ThenBy(candidate => candidate.OwnerModId, StringComparer.Ordinal)
|
||||
.First();
|
||||
}
|
||||
|
||||
return _baseEntries[key];
|
||||
}
|
||||
|
||||
private static string NormalizeOwner(string ownerModId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ownerModId))
|
||||
throw new ArgumentException("归属模组 ID 不能为空。", nameof(ownerModId));
|
||||
return ownerModId.Trim();
|
||||
}
|
||||
|
||||
private static string NormalizeKey(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentException("注册键不能为空。", nameof(key));
|
||||
return key.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,12 +199,25 @@ namespace ShrinkModFramework
|
||||
public string OwnerModId { get; }
|
||||
public string Key { get; }
|
||||
public T Value { get; }
|
||||
public bool IsOverride { get; }
|
||||
public int Priority { get; }
|
||||
|
||||
public ShrinkRegistryEntry(string ownerModId, string key, T value)
|
||||
: this(ownerModId, key, value, false, 0)
|
||||
{
|
||||
}
|
||||
|
||||
private ShrinkRegistryEntry(string ownerModId, string key, T value, bool isOverride, int priority)
|
||||
{
|
||||
OwnerModId = ownerModId;
|
||||
Key = key;
|
||||
Value = value;
|
||||
IsOverride = isOverride;
|
||||
Priority = priority;
|
||||
}
|
||||
|
||||
internal static ShrinkRegistryEntry<T> CreateOverride(
|
||||
string ownerModId, string key, int priority, T value) =>
|
||||
new(ownerModId, key, value, true, priority);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ namespace ShrinkModFramework
|
||||
{
|
||||
private readonly Dictionary<string, object> _registries = new();
|
||||
|
||||
public ShrinkModRegistry<T> GetOrCreateRegistry<T>(string name)
|
||||
public IReadOnlyShrinkModRegistry<T> GetOrCreateRegistry<T>(string name) =>
|
||||
GetOrCreateMutableRegistry<T>(name);
|
||||
|
||||
internal ShrinkModRegistry<T> GetOrCreateMutableRegistry<T>(string name)
|
||||
{
|
||||
var compositeKey = BuildCompositeKey(typeof(T), name);
|
||||
if (_registries.TryGetValue(compositeKey, out var existing))
|
||||
@@ -18,7 +21,7 @@ namespace ShrinkModFramework
|
||||
return registry;
|
||||
}
|
||||
|
||||
public bool TryGetRegistry<T>(string name, out ShrinkModRegistry<T> registry)
|
||||
public bool TryGetRegistry<T>(string name, out IReadOnlyShrinkModRegistry<T> registry)
|
||||
{
|
||||
var compositeKey = BuildCompositeKey(typeof(T), name);
|
||||
if (_registries.TryGetValue(compositeKey, out var existing))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"references": [
|
||||
"Newtonsoft.Json",
|
||||
"ShrinkContext.Core.Runtime",
|
||||
"ShrinkEventBus.Runtime",
|
||||
"UniTask"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
|
||||
Reference in New Issue
Block a user