feat(cordis): 接入上下文组合与模组事务热替换
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
本文件记录 `ShrinkNetwork.Integration.EventBus` 在当前工作区中的包内变更。
|
||||
|
||||
## [0.1.1] - 2026-04-06
|
||||
|
||||
### Added
|
||||
|
||||
- 提供更顺手的桥接扩展:`session.PublishEventAsync(...)`、`session.RequestEventAsync(...)`、`service.BroadcastEventAsync(...)`、`service.UseEventBusBridge(...)`。
|
||||
- 把 EventBus 事件桥接分为广播事件、请求事件、增量事件三类语义。
|
||||
|
||||
### Changed
|
||||
|
||||
- 桥接层转发过程中配合 `ShrinkEventBus` 语义修正,异步路径改为以克隆/快照方式分发,降低复用事件对象带来的污染风险。
|
||||
- README 重写,补齐桥接范式、事件语义和接入说明。
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d177dfebca8947e438bde6305bb342a7
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,252 @@
|
||||
# ShrinkNetwork.Integration.EventBus
|
||||
|
||||
`ShrinkNetwork` 与 `ShrinkEventBus` 的桥接层。目标不是“把网络生命周期抛几个普通事件出来”,而是让某些 `EventBase` 类型本身就能作为网络消息声明,并在本地与远端之间保持一致的事件语义。
|
||||
|
||||
## ✨ 特性概览
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 🌉 **事件即网络消息** | `EventBase` 可直接实现 `IShrinkNetworkMessage / IShrinkNetworkRequest` 参与网络同步 |
|
||||
| 🔁 **自动双端分发** | 本地 `EventBus.TriggerEvent(...)` 后,可自动转发到远端并重新进入远端 `EventBus` |
|
||||
| ✅ **HasResult 请求语义** | 请求型事件支持 `EventResult` 与取消状态回传 |
|
||||
| 📉 **增量事件去重** | `IShrinkNetworkDeltaEvent` 按 `SessionId + EventType + DeltaKey` 做版本过滤 |
|
||||
| 🧩 **按键接入** | `ShrinkNetworkEventBusComponent` 注入网络服务,随依赖激活和撤回 |
|
||||
| 🧰 **更顺手的扩展 API** | `session.PublishEventAsync(...)`、`session.RequestEventAsync(...)`、`service.UseEventBusBridge(...)` |
|
||||
|
||||
## 📦 依赖
|
||||
|
||||
- `ShrinkNetwork`
|
||||
- `ShrinkEventBus`
|
||||
- Unity 2022.3+
|
||||
|
||||
## ⚙️ 安装前提
|
||||
|
||||
桥接层默认假设你已经有:
|
||||
|
||||
- 一套正常工作的 `ShrinkNetworkService`
|
||||
- 一套正常工作的 `ShrinkEventBus`
|
||||
- 事件类型明确声明 `[ShrinkNetworkEvent] + [ShrinkNetworkMessage]`
|
||||
|
||||
## 🚀 快速上手
|
||||
|
||||
### 第一步:声明广播型网络事件
|
||||
|
||||
```csharp
|
||||
using ShrinkEventBus;
|
||||
using ShrinkNetwork;
|
||||
using ShrinkNetwork.Integration;
|
||||
|
||||
[ShrinkNetworkEvent]
|
||||
[ShrinkNetworkMessage(3001, "room/player_ready")]
|
||||
public sealed class PlayerReadyEvent : EventBase, IShrinkNetworkMessage
|
||||
{
|
||||
public string PlayerId { get; set; } = string.Empty;
|
||||
public string RoomId { get; set; } = string.Empty;
|
||||
}
|
||||
```
|
||||
|
||||
### 第二步:正常启动网络服务
|
||||
|
||||
```csharp
|
||||
var service = new ShrinkNetworkService(
|
||||
new ShrinkMessagePackNetworkSerializer(),
|
||||
new ShrinkNetworkMessageRegistry(),
|
||||
new ShrinkNetworkRouter());
|
||||
|
||||
service.AutoRegisterAll();
|
||||
service.BindTransport(new ShrinkTcpClientTransport("127.0.0.1", 17001));
|
||||
```
|
||||
|
||||
ContextLoader 项目由 Starter 组合根装配 `ShrinkNetworkEventBusComponent`。Standalone 项目需要显式调用 `service.UseEventBusBridge(...)`;仅绑定传输不会再通过反射接桥。
|
||||
|
||||
### 第三步:像普通事件一样触发
|
||||
|
||||
```csharp
|
||||
EventBus.TriggerEvent(new PlayerReadyEvent
|
||||
{
|
||||
PlayerId = "10001",
|
||||
RoomId = "alpha"
|
||||
});
|
||||
```
|
||||
|
||||
注册之后:
|
||||
|
||||
- 本地事件先按正常 `EventBus` 流程执行
|
||||
- 桥接层自动把该事件转发到当前 `ShrinkNetworkService` 的在线 session
|
||||
- 远端收到后重新进入远端 `EventBus.TriggerEventAsync(...)`
|
||||
|
||||
## 📖 核心概念
|
||||
|
||||
### 事件声明规则
|
||||
|
||||
可桥接事件需要同时满足:
|
||||
|
||||
1. 继承 `EventBase`
|
||||
2. 实现 `IShrinkNetworkMessage` 或 `IShrinkNetworkRequest`
|
||||
3. 标记 `[ShrinkNetworkEvent]`
|
||||
4. 标记 `[ShrinkNetworkMessage(opcode, route)]`
|
||||
|
||||
如果缺少这些条件,桥接层会忽略该事件类型。
|
||||
|
||||
### 广播型事件
|
||||
|
||||
广播型事件只需要:
|
||||
|
||||
- `EventBase`
|
||||
- `IShrinkNetworkMessage`
|
||||
- `[ShrinkNetworkEvent]`
|
||||
- `[ShrinkNetworkMessage(...)]`
|
||||
|
||||
适合:
|
||||
|
||||
- 玩家就绪
|
||||
- 房间状态变更
|
||||
- UI 同步通知
|
||||
|
||||
手动定向发送时,推荐直接用扩展:
|
||||
|
||||
```csharp
|
||||
await session.PublishEventAsync(new PlayerReadyEvent
|
||||
{
|
||||
PlayerId = "10001",
|
||||
RoomId = "alpha"
|
||||
});
|
||||
```
|
||||
|
||||
### 增量事件
|
||||
|
||||
如果事件本身就是 delta,而不是完整状态,可以实现 `IShrinkNetworkDeltaEvent`:
|
||||
|
||||
```csharp
|
||||
[ShrinkNetworkEvent]
|
||||
[ShrinkNetworkMessage(3002, "room/player_state_delta")]
|
||||
public sealed class PlayerStateDeltaEvent : EventBase, IShrinkNetworkMessage, IShrinkNetworkDeltaEvent
|
||||
{
|
||||
public string PlayerId { get; set; } = string.Empty;
|
||||
public int Hp { get; set; }
|
||||
|
||||
public string DeltaKey => PlayerId;
|
||||
public long DeltaVersion { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
桥接层会按:
|
||||
|
||||
`SessionId + EventType + DeltaKey`
|
||||
|
||||
记录已应用版本:
|
||||
|
||||
- 新版本进入远端 `EventBus`
|
||||
- 旧版本或重复版本直接丢弃
|
||||
|
||||
这套语义是“事件自己声明 delta 载荷”,不是自动做字段 diff。
|
||||
|
||||
### HasResult 请求型事件
|
||||
|
||||
如果事件同时满足:
|
||||
|
||||
1. `[ShrinkNetworkEvent]`
|
||||
2. `[ShrinkNetworkMessage(...)]`
|
||||
3. `[HasResult]`
|
||||
4. `IShrinkNetworkRequest`
|
||||
|
||||
就可以走“远端裁决”语义:
|
||||
|
||||
```csharp
|
||||
[Cancelable]
|
||||
[HasResult]
|
||||
[ShrinkNetworkEvent]
|
||||
[ShrinkNetworkMessage(3010, "room/can_use_skill")]
|
||||
public sealed class CanUseSkillEvent : EventBase, IShrinkNetworkRequest
|
||||
{
|
||||
public string PlayerId { get; set; } = string.Empty;
|
||||
public string SkillId { get; set; } = string.Empty;
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
var outcome = await session.RequestEventAsync(new CanUseSkillEvent
|
||||
{
|
||||
PlayerId = "10001",
|
||||
SkillId = "fireball"
|
||||
});
|
||||
|
||||
if (outcome.IsSuccess && outcome.Result == EventResult.ALLOW)
|
||||
{
|
||||
// 允许释放技能
|
||||
}
|
||||
```
|
||||
|
||||
当前回传内容只有:
|
||||
|
||||
- `EventResult`
|
||||
- `IsCanceled`
|
||||
- `ErrorCode / ErrorMessage`
|
||||
|
||||
不会自动回传整个事件对象上其他字段的最终改动。
|
||||
|
||||
## 🔧 API 参考
|
||||
|
||||
### ContextLoader 接入
|
||||
|
||||
```csharp
|
||||
// ShrinkApp.Starter.Basic 组合根自动加入 ShrinkNetworkEventBusComponent。
|
||||
// 组件注入 shrink.service.network 后注册,网络提供者撤回时注销。
|
||||
```
|
||||
|
||||
### 显式接入
|
||||
|
||||
Standalone 或需要自定义 session 过滤时:
|
||||
|
||||
```csharp
|
||||
service.UseEventBusBridge(new ShrinkNetworkEventBusBridgeOptions
|
||||
{
|
||||
SessionFilter = (session, evt) => session.SessionId > 0,
|
||||
DispatchScheduler = new ShrinkNetworkUnityMainThreadDispatchScheduler()
|
||||
});
|
||||
```
|
||||
|
||||
如果网络事件数量较大并且需要明确的每帧预算,使用 `ShrinkNetworkDispatchQueue`,在 Unity 主线程的 `Update` 中调用 `PumpAsync(maxItems)`;队列满时默认拒绝,避免无界堆积。
|
||||
|
||||
### 发送扩展
|
||||
|
||||
```csharp
|
||||
session.PublishEventAsync<TEvent>(eventArgs, route = null)
|
||||
session.RequestEventAsync<TEvent>(eventArgs, options = null)
|
||||
service.BroadcastEventAsync<TEvent>(eventArgs, sessionFilter = null, route = null)
|
||||
```
|
||||
|
||||
## 🏗️ 工作流
|
||||
|
||||
```
|
||||
本地 EventBus.TriggerEvent(evt)
|
||||
├─ 本地订阅链正常执行
|
||||
├─ Bridge 监听 OnEventTriggered
|
||||
├─ 识别为 [ShrinkNetworkEvent]
|
||||
├─ 自动转发到在线 session
|
||||
└─ 远端收到后重新进入 EventBus.TriggerEventAsync(evt)
|
||||
```
|
||||
|
||||
请求型事件则额外带回:
|
||||
|
||||
```
|
||||
远端 EventResult / IsCanceled / ErrorCode
|
||||
```
|
||||
|
||||
## ✅ 最佳实践
|
||||
|
||||
- 广播型事件和请求型事件分开设计,不要一个类型同时混两种用途
|
||||
- 高实时状态优先实现 `IShrinkNetworkDeltaEvent`
|
||||
- 需要“远端裁决”的事件显式标记 `[HasResult]`
|
||||
- 优先走 `session.PublishEventAsync(...)` / `session.RequestEventAsync(...)`,不要在业务层到处直接调用底层 bridge
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
- 核心网络程序集不反射发现桥接包;桥接生命周期只来自组合组件或显式 API
|
||||
- 当前只回传 `EventResult` 与取消状态,不自动同步事件对象其它字段改动
|
||||
- 桥接层会抑制“远端收到后再次回传”的回环转发
|
||||
- 如果你需要按目标 session 精细控制广播范围,使用 `UseEventBusBridge(...)` 或 `BroadcastEventAsync(...)`
|
||||
|
||||
## 📄 License
|
||||
|
||||
[MIT](LICENSE)
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7592055b3962e549a3a54179b99be3d
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "ShrinkNetwork.Integration.EventBus",
|
||||
"rootNamespace": "ShrinkNetwork.Integration",
|
||||
"references": [
|
||||
"ShrinkNetwork.Runtime",
|
||||
"ShrinkContext.Core.Runtime",
|
||||
"ShrinkEventBus.Runtime",
|
||||
"UniTask"
|
||||
],
|
||||
"optionalUnityReferences": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5642850b24bc7d248b4d2ff56c5275e5
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
using ShrinkEventBus;
|
||||
|
||||
namespace ShrinkNetwork.Integration
|
||||
{
|
||||
public interface IShrinkNetworkDeltaEvent
|
||||
{
|
||||
string DeltaKey { get; }
|
||||
long DeltaVersion { get; }
|
||||
}
|
||||
|
||||
[ShrinkNetworkMessage(-300001, "__integration/event_result_response")]
|
||||
public sealed class ShrinkNetworkEventResultResponse : ShrinkRpcResponseBase
|
||||
{
|
||||
public EventResult Result { get; set; } = EventResult.DEFAULT;
|
||||
public bool IsCanceled { get; set; }
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd46e5dac04669543aee6fd72fd9adea
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkNetwork.Integration
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class ShrinkNetworkEventAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3006890f24213734a9a8d7ccfb440c5f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,551 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkEventBus;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkNetwork.Integration
|
||||
{
|
||||
public static class ShrinkNetworkEventBusBridge
|
||||
{
|
||||
private sealed class ServiceRegistration
|
||||
{
|
||||
public ShrinkNetworkService Service = null!;
|
||||
public Func<ShrinkNetworkSession, EventBase, bool>? SessionFilter;
|
||||
public Dictionary<string, long> AppliedDeltaVersions { get; } = new();
|
||||
}
|
||||
|
||||
private static readonly object SyncRoot = new();
|
||||
private static readonly Dictionary<ShrinkNetworkService, ServiceRegistration> RegisteredServices = new();
|
||||
private static readonly AsyncLocal<int> SuppressForwardDepth = new();
|
||||
private static readonly ConcurrentDictionary<Type, Func<EventBase, UniTask>> IncomingDispatchers = new();
|
||||
private static readonly ConcurrentDictionary<Type, Func<ShrinkNetworkContext, EventBase, UniTask<object?>>> IncomingRequestDispatchers = new();
|
||||
private static readonly MethodInfo DispatchIncomingGenericMethod =
|
||||
typeof(ShrinkNetworkEventBusBridge).GetMethod(nameof(DispatchIncomingGenericAsync),
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
private static readonly MethodInfo DispatchIncomingRequestGenericMethod =
|
||||
typeof(ShrinkNetworkEventBusBridge).GetMethod(nameof(DispatchIncomingRequestGenericAsync),
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
|
||||
private static Dictionary<Type, ShrinkNetworkMessageAttribute> _networkEventTypes = new();
|
||||
private static bool _initialized;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetStaticState()
|
||||
{
|
||||
EventBus.OnEventTriggered -= HandleLocalEventTriggered;
|
||||
lock (SyncRoot)
|
||||
{
|
||||
RegisteredServices.Clear();
|
||||
FallbackDeltaVersions.Clear();
|
||||
}
|
||||
|
||||
IncomingDispatchers.Clear();
|
||||
IncomingRequestDispatchers.Clear();
|
||||
_networkEventTypes = new Dictionary<Type, ShrinkNetworkMessageAttribute>();
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
private static void EnsureBridgeInitialized()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
_initialized = true;
|
||||
RefreshNetworkEventTypes();
|
||||
EventBus.OnEventTriggered += HandleLocalEventTriggered;
|
||||
}
|
||||
|
||||
public static void RegisterService(ShrinkNetworkService service,
|
||||
Func<ShrinkNetworkSession, EventBase, bool>? sessionFilter = null)
|
||||
{
|
||||
if (service == null)
|
||||
throw new ArgumentNullException(nameof(service));
|
||||
|
||||
EnsureBridgeInitialized();
|
||||
RefreshNetworkEventTypes();
|
||||
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (RegisteredServices.TryGetValue(service, out var existing))
|
||||
{
|
||||
existing.SessionFilter = sessionFilter;
|
||||
return;
|
||||
}
|
||||
|
||||
RegisteredServices.Add(service, new ServiceRegistration
|
||||
{
|
||||
Service = service,
|
||||
SessionFilter = sessionFilter
|
||||
});
|
||||
}
|
||||
|
||||
RegisterInboundBridgeHandlers(service);
|
||||
}
|
||||
|
||||
public static void UnregisterService(ShrinkNetworkService service)
|
||||
{
|
||||
if (service == null)
|
||||
return;
|
||||
|
||||
lock (SyncRoot)
|
||||
{
|
||||
RegisteredServices.Remove(service);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RefreshBindings()
|
||||
{
|
||||
RefreshNetworkEventTypes();
|
||||
|
||||
List<ShrinkNetworkService> services;
|
||||
lock (SyncRoot)
|
||||
{
|
||||
services = RegisteredServices.Keys.ToList();
|
||||
}
|
||||
|
||||
foreach (var service in services)
|
||||
{
|
||||
RegisterInboundBridgeHandlers(service);
|
||||
}
|
||||
}
|
||||
|
||||
public static UniTask PublishAsync<TEvent>(ShrinkNetworkSession session, TEvent eventArgs, string? route = null)
|
||||
where TEvent : EventBase, IShrinkNetworkMessage
|
||||
{
|
||||
if (session == null)
|
||||
throw new ArgumentNullException(nameof(session));
|
||||
if (eventArgs == null)
|
||||
throw new ArgumentNullException(nameof(eventArgs));
|
||||
|
||||
EnsureNetworkEventType(typeof(TEvent));
|
||||
EnsurePublishableEventType(typeof(TEvent));
|
||||
return session.SendAsync(eventArgs, route);
|
||||
}
|
||||
|
||||
public static async UniTask PublishAsync<TEvent>(IEnumerable<ShrinkNetworkSession> sessions, TEvent eventArgs,
|
||||
string? route = null)
|
||||
where TEvent : EventBase, IShrinkNetworkMessage
|
||||
{
|
||||
if (sessions == null)
|
||||
throw new ArgumentNullException(nameof(sessions));
|
||||
if (eventArgs == null)
|
||||
throw new ArgumentNullException(nameof(eventArgs));
|
||||
|
||||
EnsureNetworkEventType(typeof(TEvent));
|
||||
EnsurePublishableEventType(typeof(TEvent));
|
||||
|
||||
var groupedSessions = sessions
|
||||
.Where(session => session != null)
|
||||
.GroupBy(session => session.Service);
|
||||
|
||||
foreach (var serviceGroup in groupedSessions)
|
||||
{
|
||||
var sessionList = serviceGroup.ToList();
|
||||
if (sessionList.Count == 0)
|
||||
continue;
|
||||
|
||||
var payload = serviceGroup.Key.Serializer.Serialize(eventArgs);
|
||||
|
||||
foreach (var session in sessionList)
|
||||
await serviceGroup.Key.SendSerializedAsync(session, typeof(TEvent), payload, route);
|
||||
}
|
||||
}
|
||||
|
||||
public static async UniTask<ShrinkNetworkEventResultResponse> RequestResultAsync<TEvent>(ShrinkNetworkSession session,
|
||||
TEvent eventArgs, ShrinkRpcCallOptions? options = null)
|
||||
where TEvent : EventBase, IShrinkNetworkRequest
|
||||
{
|
||||
if (session == null)
|
||||
throw new ArgumentNullException(nameof(session));
|
||||
if (eventArgs == null)
|
||||
throw new ArgumentNullException(nameof(eventArgs));
|
||||
|
||||
EnsureResultRequestEventType(typeof(TEvent));
|
||||
|
||||
options ??= new ShrinkRpcCallOptions();
|
||||
if (string.IsNullOrWhiteSpace(options.DebugLabel))
|
||||
options.DebugLabel = typeof(TEvent).Name;
|
||||
|
||||
var response = await session.RpcAsync<TEvent, ShrinkNetworkEventResultResponse>(eventArgs,
|
||||
options);
|
||||
|
||||
if (response.IsSuccess)
|
||||
{
|
||||
eventArgs.SetResult(response.Result);
|
||||
if (eventArgs.IsCancelable)
|
||||
eventArgs.SetCanceled(response.IsCanceled);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static void HandleLocalEventTriggered(EventBase eventArgs, Type eventType)
|
||||
{
|
||||
if (eventArgs == null || eventType == null)
|
||||
return;
|
||||
if (SuppressForwardDepth.Value > 0)
|
||||
return;
|
||||
if (!TryGetNetworkEventMeta(eventType, out _))
|
||||
return;
|
||||
if (eventArgs is IShrinkNetworkRequest)
|
||||
return;
|
||||
if (eventArgs is not IShrinkNetworkMessage networkMessage)
|
||||
return;
|
||||
|
||||
ForwardEventAsync(eventArgs, eventType, networkMessage).Forget();
|
||||
}
|
||||
|
||||
private static async UniTaskVoid ForwardEventAsync(EventBase eventArgs, Type eventType,
|
||||
IShrinkNetworkMessage networkMessage)
|
||||
{
|
||||
List<ServiceRegistration> registrations;
|
||||
lock (SyncRoot)
|
||||
{
|
||||
registrations = RegisteredServices.Values.ToList();
|
||||
}
|
||||
|
||||
var batches = new List<(ServiceRegistration Registration, List<ShrinkNetworkSession> Sessions, byte[] Payload)>();
|
||||
foreach (var registration in registrations)
|
||||
{
|
||||
var sessions = registration.Service.Sessions.Values
|
||||
.Where(session => registration.SessionFilter == null || registration.SessionFilter(session, eventArgs))
|
||||
.ToList();
|
||||
if (sessions.Count == 0)
|
||||
continue;
|
||||
|
||||
byte[] payload;
|
||||
try
|
||||
{
|
||||
// Event payload is immutable after serialization and can be
|
||||
// reused for every session owned by this service.
|
||||
payload = registration.Service.Serializer.Serialize(networkMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
Debug.LogError(
|
||||
$"[ShrinkNetwork.Integration] 序列化网络事件失败: {eventArgs.GetType().FullName}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Complete every serializer pass before the first await. EventBase
|
||||
// instances may be returned to EventPool immediately after the
|
||||
// synchronous trigger returns.
|
||||
batches.Add((registration, sessions, payload));
|
||||
}
|
||||
|
||||
foreach (var batch in batches)
|
||||
{
|
||||
foreach (var session in batch.Sessions)
|
||||
{
|
||||
try
|
||||
{
|
||||
await batch.Registration.Service.SendSerializedAsync(session, eventType, batch.Payload);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
Debug.LogError(
|
||||
$"[ShrinkNetwork.Integration] 转发网络事件失败: {eventArgs.GetType().FullName}, Session={session.SessionId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterInboundBridgeHandlers(ShrinkNetworkService service)
|
||||
{
|
||||
foreach (var pair in _networkEventTypes)
|
||||
{
|
||||
if (!service.MessageRegistry.TryGetMeta(pair.Key, out _))
|
||||
{
|
||||
service.RegisterMessage(pair.Key, pair.Value.Opcode, pair.Value.Route);
|
||||
}
|
||||
|
||||
if (!service.MessageRegistry.TryGetMeta(typeof(ShrinkNetworkEventResultResponse), out _))
|
||||
{
|
||||
var responseAttribute =
|
||||
typeof(ShrinkNetworkEventResultResponse).GetCustomAttribute<ShrinkNetworkMessageAttribute>(false);
|
||||
if (responseAttribute != null)
|
||||
{
|
||||
service.RegisterMessage(typeof(ShrinkNetworkEventResultResponse), responseAttribute.Opcode,
|
||||
responseAttribute.Route);
|
||||
}
|
||||
}
|
||||
|
||||
var isRequestEvent = typeof(IShrinkNetworkRequest).IsAssignableFrom(pair.Key);
|
||||
|
||||
try
|
||||
{
|
||||
if (isRequestEvent)
|
||||
{
|
||||
service.RegisterRequestHandler(pair.Key, typeof(ShrinkNetworkEventResultResponse),
|
||||
DispatchIncomingRequestAsync);
|
||||
}
|
||||
else
|
||||
{
|
||||
service.RegisterHandler(pair.Key, DispatchIncomingAsync);
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// 同一个 service 上该事件类型只能绑定一个处理器。已存在时认为调用方自行接管。
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static UniTask DispatchIncomingAsync(ShrinkNetworkContext context, object message)
|
||||
{
|
||||
if (message is not EventBase eventArgs)
|
||||
return UniTask.CompletedTask;
|
||||
|
||||
var dispatcher = IncomingDispatchers.GetOrAdd(eventArgs.GetType(), CreateIncomingDispatcher);
|
||||
return dispatcher(eventArgs);
|
||||
}
|
||||
|
||||
private static Func<EventBase, UniTask> CreateIncomingDispatcher(Type eventType)
|
||||
{
|
||||
var closedMethod = DispatchIncomingGenericMethod.MakeGenericMethod(eventType);
|
||||
return (Func<EventBase, UniTask>)closedMethod.CreateDelegate(typeof(Func<EventBase, UniTask>));
|
||||
}
|
||||
|
||||
private static UniTask<object?> DispatchIncomingRequestAsync(ShrinkNetworkContext context, object message)
|
||||
{
|
||||
if (message is not EventBase eventArgs)
|
||||
{
|
||||
return UniTask.FromResult<object?>(new ShrinkNetworkEventResultResponse
|
||||
{
|
||||
ErrorCode = ShrinkRpcErrorCode.InvalidResponse,
|
||||
ErrorMessage = "Incoming network event request is not an EventBase."
|
||||
});
|
||||
}
|
||||
|
||||
var dispatcher = IncomingRequestDispatchers.GetOrAdd(eventArgs.GetType(), CreateIncomingRequestDispatcher);
|
||||
return dispatcher(context, eventArgs);
|
||||
}
|
||||
|
||||
private static Func<ShrinkNetworkContext, EventBase, UniTask<object?>> CreateIncomingRequestDispatcher(Type eventType)
|
||||
{
|
||||
var closedMethod = DispatchIncomingRequestGenericMethod.MakeGenericMethod(eventType);
|
||||
return (Func<ShrinkNetworkContext, EventBase, UniTask<object?>>)closedMethod.CreateDelegate(
|
||||
typeof(Func<ShrinkNetworkContext, EventBase, UniTask<object?>>));
|
||||
}
|
||||
|
||||
private static async UniTask DispatchIncomingGenericAsync<TEvent>(EventBase eventArgs)
|
||||
where TEvent : EventBase
|
||||
{
|
||||
SuppressForwardDepth.Value++;
|
||||
try
|
||||
{
|
||||
if (TryGetDeltaEvent(eventArgs, typeof(TEvent), out var deltaEvent) &&
|
||||
!TryMarkIncomingDelta(typeof(TEvent), deltaEvent, null))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await EventBus.TriggerEventAsync((TEvent)eventArgs);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SuppressForwardDepth.Value--;
|
||||
}
|
||||
}
|
||||
|
||||
private static async UniTask<object?> DispatchIncomingRequestGenericAsync<TEvent>(ShrinkNetworkContext context,
|
||||
EventBase eventArgs)
|
||||
where TEvent : EventBase
|
||||
{
|
||||
var typedEvent = (TEvent)eventArgs;
|
||||
|
||||
SuppressForwardDepth.Value++;
|
||||
try
|
||||
{
|
||||
if (TryGetDeltaEvent(typedEvent, typeof(TEvent), out var deltaEvent) &&
|
||||
!TryMarkIncomingDelta(typeof(TEvent), deltaEvent, context))
|
||||
{
|
||||
return BuildResultResponse(typedEvent);
|
||||
}
|
||||
|
||||
await EventBus.TriggerEventAsync(typedEvent);
|
||||
return BuildResultResponse(typedEvent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
return new ShrinkNetworkEventResultResponse
|
||||
{
|
||||
ErrorCode = ShrinkRpcErrorCode.HandlerException,
|
||||
ErrorMessage = ex.Message,
|
||||
Result = typedEvent.HasResult ? typedEvent.Result : EventResult.DEFAULT,
|
||||
IsCanceled = typedEvent.IsCancelable && typedEvent.IsCanceled
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
SuppressForwardDepth.Value--;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RefreshNetworkEventTypes()
|
||||
{
|
||||
var eventTypes = new Dictionary<Type, ShrinkNetworkMessageAttribute>();
|
||||
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
Type[] types;
|
||||
try
|
||||
{
|
||||
types = assembly.GetTypes();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (type == null || type.IsAbstract)
|
||||
continue;
|
||||
if (!typeof(EventBase).IsAssignableFrom(type))
|
||||
continue;
|
||||
if (!typeof(IShrinkNetworkMessage).IsAssignableFrom(type))
|
||||
continue;
|
||||
if (type.GetCustomAttribute<ShrinkNetworkEventAttribute>(false) == null)
|
||||
continue;
|
||||
|
||||
var messageAttr = type.GetCustomAttribute<ShrinkNetworkMessageAttribute>(false);
|
||||
if (messageAttr == null)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[ShrinkNetwork.Integration] {type.FullName} 标记了 [ShrinkNetworkEvent],但缺少 [ShrinkNetworkMessage],已忽略。");
|
||||
continue;
|
||||
}
|
||||
|
||||
eventTypes[type] = messageAttr;
|
||||
}
|
||||
}
|
||||
|
||||
_networkEventTypes = eventTypes;
|
||||
}
|
||||
|
||||
private static bool TryGetNetworkEventMeta(Type eventType, out ShrinkNetworkMessageAttribute? messageAttribute)
|
||||
{
|
||||
if (eventType == null)
|
||||
{
|
||||
messageAttribute = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return _networkEventTypes.TryGetValue(eventType, out messageAttribute);
|
||||
}
|
||||
|
||||
private static void EnsureNetworkEventType(Type eventType)
|
||||
{
|
||||
if (!TryGetNetworkEventMeta(eventType, out _))
|
||||
throw new InvalidOperationException(
|
||||
$"Event type {eventType.FullName} must implement EventBase + IShrinkNetworkMessage and declare [ShrinkNetworkEvent] + [ShrinkNetworkMessage].");
|
||||
}
|
||||
|
||||
private static void EnsurePublishableEventType(Type eventType)
|
||||
{
|
||||
if (typeof(IShrinkNetworkRequest).IsAssignableFrom(eventType))
|
||||
throw new InvalidOperationException(
|
||||
$"Event type {eventType.FullName} is a request event. Use RequestResultAsync instead of PublishAsync.");
|
||||
}
|
||||
|
||||
private static void EnsureResultRequestEventType(Type eventType)
|
||||
{
|
||||
EnsureNetworkEventType(eventType);
|
||||
|
||||
if (!typeof(IShrinkNetworkRequest).IsAssignableFrom(eventType))
|
||||
throw new InvalidOperationException(
|
||||
$"Event type {eventType.FullName} must implement IShrinkNetworkRequest to use RequestResultAsync.");
|
||||
if (eventType.GetCustomAttribute<HasResultAttribute>(false) == null)
|
||||
throw new InvalidOperationException(
|
||||
$"Event type {eventType.FullName} must declare [HasResult] to use RequestResultAsync.");
|
||||
}
|
||||
|
||||
private static ShrinkNetworkEventResultResponse BuildResultResponse(EventBase eventArgs)
|
||||
{
|
||||
return new ShrinkNetworkEventResultResponse
|
||||
{
|
||||
Result = eventArgs.HasResult ? eventArgs.Result : EventResult.DEFAULT,
|
||||
IsCanceled = eventArgs.IsCancelable && eventArgs.IsCanceled
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryGetDeltaEvent(EventBase eventArgs, Type eventType, out IShrinkNetworkDeltaEvent deltaEvent)
|
||||
{
|
||||
if (eventArgs is IShrinkNetworkDeltaEvent eventDelta)
|
||||
{
|
||||
deltaEvent = eventDelta;
|
||||
return true;
|
||||
}
|
||||
|
||||
deltaEvent = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryMarkIncomingDelta(Type eventType, IShrinkNetworkDeltaEvent deltaEvent,
|
||||
ShrinkNetworkContext? context)
|
||||
{
|
||||
if (deltaEvent == null)
|
||||
return true;
|
||||
|
||||
var scopeKey = BuildDeltaScopeKey(eventType, deltaEvent, context);
|
||||
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (context != null && RegisteredServices.TryGetValue(context.Service, out var serviceRegistration))
|
||||
{
|
||||
return TryMarkIncomingDeltaCore(serviceRegistration.AppliedDeltaVersions, scopeKey,
|
||||
deltaEvent.DeltaVersion);
|
||||
}
|
||||
|
||||
return TryMarkIncomingDeltaCore(FallbackDeltaVersions, scopeKey, deltaEvent.DeltaVersion);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryMarkIncomingDeltaCore(Dictionary<string, long> versionMap, string scopeKey, long deltaVersion)
|
||||
{
|
||||
if (versionMap.TryGetValue(scopeKey, out var existingVersion) && deltaVersion <= existingVersion)
|
||||
return false;
|
||||
|
||||
versionMap[scopeKey] = deltaVersion;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string BuildDeltaScopeKey(Type eventType, IShrinkNetworkDeltaEvent deltaEvent,
|
||||
ShrinkNetworkContext? context)
|
||||
{
|
||||
var sessionId = context?.Session?.SessionId ?? 0;
|
||||
var key = string.IsNullOrWhiteSpace(deltaEvent.DeltaKey) ? "__default" : deltaEvent.DeltaKey.Trim();
|
||||
return $"{sessionId}:{eventType.FullName}:{key}";
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, long> FallbackDeltaVersions = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches network callbacks to Unity's main thread before entering EventBus
|
||||
/// handlers. Use ShrinkNetworkDispatchQueue when a hard per-frame budget is
|
||||
/// required instead of an unbounded PlayerLoop backlog.
|
||||
/// </summary>
|
||||
public sealed class ShrinkNetworkUnityMainThreadDispatchScheduler : IShrinkNetworkDispatchScheduler
|
||||
{
|
||||
public async UniTask<bool> ScheduleAsync(Func<UniTask> callback)
|
||||
{
|
||||
if (callback == null)
|
||||
throw new ArgumentNullException(nameof(callback));
|
||||
|
||||
await UniTask.SwitchToMainThread();
|
||||
await callback();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aaece45108fa6d64db2ef4a496c4a373
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkEventBus;
|
||||
|
||||
namespace ShrinkNetwork.Integration
|
||||
{
|
||||
public static class ShrinkNetworkEventBusBridgeExtensions
|
||||
{
|
||||
public static ShrinkNetworkService UseEventBusBridge(this ShrinkNetworkService service,
|
||||
ShrinkNetworkEventBusBridgeOptions? options = null)
|
||||
{
|
||||
if (service == null)
|
||||
throw new ArgumentNullException(nameof(service));
|
||||
|
||||
options ??= new ShrinkNetworkEventBusBridgeOptions();
|
||||
if (options.AutoRegisterAttributedMessages)
|
||||
service.AutoRegisterAttributedMessages();
|
||||
if (options.AutoRegisterStaticHandlers)
|
||||
service.AutoRegisterStaticHandlers();
|
||||
if (options.DispatchScheduler != null)
|
||||
service.DispatchScheduler = options.DispatchScheduler;
|
||||
|
||||
ShrinkNetworkEventBusBridge.RegisterService(service, options.SessionFilter);
|
||||
return service;
|
||||
}
|
||||
|
||||
public static UniTask PublishEventAsync<TEvent>(this ShrinkNetworkSession session, TEvent eventArgs, string? route = null)
|
||||
where TEvent : EventBase, IShrinkNetworkMessage
|
||||
{
|
||||
return ShrinkNetworkEventBusBridge.PublishAsync(session, eventArgs, route);
|
||||
}
|
||||
|
||||
public static UniTask BroadcastEventAsync<TEvent>(this ShrinkNetworkService service, TEvent eventArgs,
|
||||
Func<ShrinkNetworkSession, bool>? sessionFilter = null, string? route = null)
|
||||
where TEvent : EventBase, IShrinkNetworkMessage
|
||||
{
|
||||
if (service == null)
|
||||
throw new ArgumentNullException(nameof(service));
|
||||
|
||||
IEnumerable<ShrinkNetworkSession> sessions = service.Sessions.Values;
|
||||
if (sessionFilter != null)
|
||||
{
|
||||
var filtered = new List<ShrinkNetworkSession>();
|
||||
foreach (var session in service.Sessions.Values)
|
||||
{
|
||||
if (sessionFilter(session))
|
||||
filtered.Add(session);
|
||||
}
|
||||
|
||||
sessions = filtered;
|
||||
}
|
||||
|
||||
return ShrinkNetworkEventBusBridge.PublishAsync(sessions, eventArgs, route);
|
||||
}
|
||||
|
||||
public static async UniTask<ShrinkNetworkEventRequestOutcome> RequestEventAsync<TEvent>(
|
||||
this ShrinkNetworkSession session,
|
||||
TEvent eventArgs,
|
||||
ShrinkRpcCallOptions? options = null)
|
||||
where TEvent : EventBase, IShrinkNetworkRequest
|
||||
{
|
||||
var response = await ShrinkNetworkEventBusBridge.RequestResultAsync(session, eventArgs, options);
|
||||
return ShrinkNetworkEventRequestOutcome.FromResponse(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 942526ca2f2df5441b6c742f2476651a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using ShrinkEventBus;
|
||||
|
||||
namespace ShrinkNetwork.Integration
|
||||
{
|
||||
public sealed class ShrinkNetworkEventBusBridgeOptions
|
||||
{
|
||||
public bool AutoRegisterAttributedMessages { get; set; } = true;
|
||||
public bool AutoRegisterStaticHandlers { get; set; }
|
||||
public Func<ShrinkNetworkSession, EventBase, bool>? SessionFilter { get; set; }
|
||||
public IShrinkNetworkDispatchScheduler? DispatchScheduler { get; set; }
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b24445c5c1c712a40bc2958fbf7bf313
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,35 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkContext;
|
||||
|
||||
namespace ShrinkNetwork.Integration
|
||||
{
|
||||
/// <summary>通过网络服务键管理 Network ↔ EventBus 桥的可逆生命周期。</summary>
|
||||
public sealed class ShrinkNetworkEventBusComponent : IShrinkComponent
|
||||
{
|
||||
public const string NetworkServiceKey = "shrink.service.network";
|
||||
public const string ProvideKey = "shrink.integration.network-eventbus";
|
||||
|
||||
private static readonly string[] InjectKeys = { NetworkServiceKey };
|
||||
private static readonly string[] ProvideKeys = { ProvideKey };
|
||||
|
||||
public string Name => "shrink.integration.network-eventbus";
|
||||
public IReadOnlyList<string> Inject => InjectKeys;
|
||||
public IReadOnlyList<string> Provide => ProvideKeys;
|
||||
|
||||
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
|
||||
{
|
||||
var service = ctx.Get<ShrinkNetworkService>(NetworkServiceKey);
|
||||
service.UseEventBusBridge(config as ShrinkNetworkEventBusBridgeOptions);
|
||||
ctx.EffectInverse(() =>
|
||||
{
|
||||
ShrinkNetworkEventBusBridge.UnregisterService(service);
|
||||
return UniTask.CompletedTask;
|
||||
});
|
||||
ctx.Set(ProvideKey, Name);
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: baeaef51e2232ff449b18fb337209ab1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,31 @@
|
||||
#nullable enable
|
||||
using ShrinkEventBus;
|
||||
|
||||
namespace ShrinkNetwork.Integration
|
||||
{
|
||||
public readonly struct ShrinkNetworkEventRequestOutcome
|
||||
{
|
||||
public ShrinkNetworkEventRequestOutcome(int errorCode, string errorMessage, EventResult result, bool isCanceled)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
ErrorMessage = errorMessage ?? string.Empty;
|
||||
Result = result;
|
||||
IsCanceled = isCanceled;
|
||||
}
|
||||
|
||||
public int ErrorCode { get; }
|
||||
public string ErrorMessage { get; }
|
||||
public EventResult Result { get; }
|
||||
public bool IsCanceled { get; }
|
||||
public bool IsSuccess => ErrorCode == 0;
|
||||
|
||||
internal static ShrinkNetworkEventRequestOutcome FromResponse(ShrinkNetworkEventResultResponse response)
|
||||
{
|
||||
return new ShrinkNetworkEventRequestOutcome(
|
||||
response?.ErrorCode ?? ShrinkRpcErrorCode.InvalidResponse,
|
||||
response?.ErrorMessage ?? "Network event request returned an invalid response.",
|
||||
response?.Result ?? EventResult.DEFAULT,
|
||||
response?.IsCanceled ?? false);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea5612b9af38aa049b71da7196dda594
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "com.cneicy.shrink-network-integration-eventbus",
|
||||
"version": "0.1.1",
|
||||
"displayName": "ShrinkNetwork - EventBus Integration",
|
||||
"description": "ShrinkNetwork 与 ShrinkEventBus 的桥接层,让特定 EventBase 事件类型可直接走网络同步并在远端重新分发到 EventBus。",
|
||||
"unity": "2022.3",
|
||||
"dependencies": {
|
||||
"com.cneicy.shrink-network": "0.1.0",
|
||||
"com.cneicy.shrink-context-core": "0.1.0",
|
||||
"com.cneicy.shrink-eventbus": "1.1.5"
|
||||
},
|
||||
"keywords": ["network", "eventbus", "integration", "bridge", "sync"],
|
||||
"author": {
|
||||
"name": "cneicy",
|
||||
"url": "https://github.com/cneicy"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 157418d926bfc4d44946fc8f26531f74
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user