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:
2026-08-26 01:15:48 +08:00
parent 67d32795c4
commit ad5a7b68a3
129 changed files with 5071 additions and 5584 deletions
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c674d22418294a93b1da9df8149c49e9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a63a45e5ab8a4de682ca810f8a09cbca
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,184 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Pdb;
using ShrinkEventBus.CodeGen;
using Unity.CompilationPipeline.Common.Diagnostics;
using Unity.CompilationPipeline.Common.ILPostProcessing;
namespace ShrinkNetwork.Integration.CodeGen
{
public sealed class NetworkEventILPostProcessor : ILPostProcessor
{
private const string IntegrationAssembly = "ShrinkNetwork.Integration.EventBus";
public override ILPostProcessor GetInstance() => this;
public override bool WillProcess(ICompiledAssembly compiledAssembly) =>
compiledAssembly.References.Any(reference =>
string.Equals(Path.GetFileNameWithoutExtension(reference), IntegrationAssembly,
StringComparison.Ordinal));
public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
{
var diagnostics = new List<DiagnosticMessage>();
if (!WillProcess(compiledAssembly))
return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, diagnostics);
var assembly = ReadAssembly(compiledAssembly);
try
{
InjectRegistrations(assembly.MainModule);
}
catch (Exception exception)
{
diagnostics.Add(new DiagnosticMessage
{
DiagnosticType = DiagnosticType.Error,
MessageData = $"[ShrinkNetwork.EventBus.CodeGen] {exception.Message}"
});
}
return WriteAssembly(assembly, diagnostics);
}
private static void InjectRegistrations(ModuleDefinition module)
{
var networkEventAttribute = ResolveType(module,
"ShrinkNetwork.Integration.ShrinkNetworkEventAttribute", IntegrationAssembly);
var messageAttribute = ResolveType(module,
"ShrinkNetwork.ShrinkNetworkMessageAttribute", "ShrinkNetwork.Runtime");
var registryType = ResolveType(module,
"ShrinkNetwork.Integration.ShrinkNetworkEventRegistry", IntegrationAssembly);
if (networkEventAttribute == null || messageAttribute == null || registryType == null)
return;
var registrations = GetAllTypes(module.Types)
.Where(type => !type.IsAbstract && HasAttribute(type, networkEventAttribute))
.Select(type => new
{
Type = type,
Message = type.CustomAttributes.FirstOrDefault(attribute =>
attribute.AttributeType.FullName == messageAttribute.FullName)
})
.Where(item => item.Message != null)
.ToArray();
if (registrations.Length == 0)
return;
var registerDefinition = registryType.Resolve()?.Methods.Single(method =>
method.Name == "Register" && method.HasGenericParameters)
?? throw new InvalidOperationException("ShrinkNetworkEventRegistry.Register<T> was not found.");
var registerOpen = module.ImportReference(registerDefinition);
var bootstrap = new TypeDefinition("ShrinkNetwork.Integration.Generated",
"ShrinkGeneratedNetworkEventBindings",
TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.NotPublic,
module.TypeSystem.Object);
module.Types.Add(bootstrap);
var initialize = new MethodDefinition("Register",
MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig,
module.TypeSystem.Void);
bootstrap.Methods.Add(initialize);
var il = initialize.Body.GetILProcessor();
foreach (var item in registrations)
{
var attribute = item.Message!;
if (attribute.ConstructorArguments.Count == 0)
throw new InvalidOperationException(
$"[ShrinkNetworkMessage] on {item.Type.FullName} has no opcode.");
var opcode = Convert.ToInt32(attribute.ConstructorArguments[0].Value);
var route = attribute.ConstructorArguments.Count > 1
? attribute.ConstructorArguments[1].Value as string
: null;
var registerClosed = new GenericInstanceMethod(registerOpen);
registerClosed.GenericArguments.Add(module.ImportReference(item.Type));
il.Emit(OpCodes.Ldc_I4, opcode);
if (route == null)
il.Emit(OpCodes.Ldnull);
else
il.Emit(OpCodes.Ldstr, route);
il.Emit(OpCodes.Call, registerClosed);
}
il.Emit(OpCodes.Ret);
InjectModuleInitializer(module, initialize);
}
private static void InjectModuleInitializer(ModuleDefinition module, MethodReference register)
{
var moduleType = module.Types.First(type => type.Name == "<Module>");
var initializer = moduleType.Methods.FirstOrDefault(method => method.Name == ".cctor");
if (initializer == null)
{
initializer = new MethodDefinition(".cctor",
MethodAttributes.Private | MethodAttributes.Static | MethodAttributes.HideBySig |
MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
module.TypeSystem.Void);
initializer.Body.GetILProcessor().Emit(OpCodes.Ret);
moduleType.Methods.Add(initializer);
}
var il = initializer.Body.GetILProcessor();
il.InsertBefore(initializer.Body.Instructions[0], il.Create(OpCodes.Call, register));
}
private static bool HasAttribute(ICustomAttributeProvider provider, TypeReference attributeType) =>
provider.CustomAttributes.Any(attribute =>
attribute.AttributeType.FullName == attributeType.FullName);
private static IEnumerable<TypeDefinition> GetAllTypes(IEnumerable<TypeDefinition> roots)
{
foreach (var type in roots)
{
yield return type;
foreach (var nested in GetAllTypes(type.NestedTypes))
yield return nested;
}
}
private static TypeReference? ResolveType(ModuleDefinition module, string fullName,
string assemblyName)
{
var reference = module.AssemblyReferences.FirstOrDefault(item => item.Name == assemblyName);
var definition = reference == null ? null : module.AssemblyResolver.Resolve(reference);
var type = definition?.MainModule.GetType(fullName);
return type == null ? null : module.ImportReference(type);
}
private static AssemblyDefinition ReadAssembly(ICompiledAssembly compiledAssembly)
{
var resolver = new PostProcessorAssemblyResolver(compiledAssembly);
var parameters = new ReaderParameters
{
SymbolStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData.ToArray()),
SymbolReaderProvider = new PdbReaderProvider(),
AssemblyResolver = resolver,
ReflectionImporterProvider = new PostProcessorReflectionImporterProvider(),
ReadingMode = ReadingMode.Immediate
};
var assembly = AssemblyDefinition.ReadAssembly(
new MemoryStream(compiledAssembly.InMemoryAssembly.PeData.ToArray()), parameters);
resolver.AddAssemblyDefinitionBeingOperatedOn(assembly);
return assembly;
}
private static ILPostProcessResult WriteAssembly(AssemblyDefinition assembly,
List<DiagnosticMessage> diagnostics)
{
var pe = new MemoryStream();
var pdb = new MemoryStream();
assembly.Write(pe, new WriterParameters
{
SymbolWriterProvider = new PdbWriterProvider(),
SymbolStream = pdb,
WriteSymbols = true
});
return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), diagnostics);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 64b8b451fc094da2a1547ade9f5ba4dd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,19 @@
{
"name": "Unity.ShrinkNetwork.EventBus.CodeGen",
"rootNamespace": "ShrinkNetwork.Integration.CodeGen",
"references": [
"ShrinkNetwork.Integration.EventBus",
"Unity.ShrinkEventBus.CodeGen"
],
"includePlatforms": ["Editor"],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": true,
"precompiledReferences": [
"Mono.Cecil.dll",
"Mono.Cecil.Mdb.dll",
"Mono.Cecil.Pdb.dll",
"Mono.Cecil.Rocks.dll"
],
"autoReferenced": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a0970354612f4fb6bdc88828308160c5
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,252 +1,70 @@
# ShrinkNetwork.Integration.EventBus
`ShrinkNetwork``ShrinkEventBus` 的桥接层。目标不是“把网络生命周期抛几个普通事件出来”,而是让某些 `EventBase` 类型本身就能作为网络消息声明,并在本地与远端之间保持一致的事件语义
把实现 `IShrinkEvent` 的网络事件与 ShrinkEventBus 2.0 连接。广播事件可从本地 Bus 转发到远端;请求事件可在远端 `PostAsync` 后回传 `EventResult` 与取消状态;delta 事件按版本去重
## ✨ 特性概览
| 特性 | 说明 |
|------|------|
| 🌉 **事件即网络消息** | `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 sealed class PlayerReadyEvent : IShrinkEvent, 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));
EventBus.Post(new PlayerReadyEvent { PlayerId = "10001" });
await session.PublishEventAsync(new PlayerReadyEvent { PlayerId = "10001" });
```
ContextLoader 项目由 Starter 组合根装配 `ShrinkNetworkEventBusComponent`。Standalone 项目需要显式调用 `service.UseEventBusBridge(...)`;仅绑定传输不会再通过反射接桥
事件必须同时实现 `IShrinkEvent``IShrinkNetworkMessage`,并声明 `[ShrinkNetworkEvent]``[ShrinkNetworkMessage]`。本包的 ILPostProcessor 为每个事件生成强类型入站 dispatcher 和模块注册,不在运行时扫描程序集、构造泛型方法或反射调用 handler
### 第三步:像普通事件一样触发
## 远端裁决
```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 sealed class CanUseSkillEvent :
IShrinkResultEvent<EventResult>, IShrinkCancelableEvent, IShrinkNetworkRequest
{
public string PlayerId { get; set; } = string.Empty;
public string SkillId { get; set; } = string.Empty;
private EventResult _result;
private bool _canceled;
public EventResult Result => _result;
public bool IsCanceled => _canceled;
public void SetResult(EventResult value) => _result = value;
public void SetCanceled(bool value) => _canceled = value;
}
```
```csharp
var outcome = await session.RequestEventAsync(new CanUseSkillEvent
{
PlayerId = "10001",
SkillId = "fireball"
});
if (outcome.IsSuccess && outcome.Result == EventResult.ALLOW)
{
// 允许释放技能
}
var outcome = await session.RequestEventAsync(new CanUseSkillEvent());
```
当前回传内容只有:
请求事件必须实现 `IShrinkResultEvent<EventResult>``IShrinkNetworkRequest`。如果需要取消回传,再实现 `IShrinkCancelableEvent`。当前响应只包含 `EventResult`、取消状态和 RPC 错误,不自动回传事件上的其它可变字段。
- `EventResult`
- `IsCanceled`
- `ErrorCode / ErrorMessage`
## Delta
不会自动回传整个事件对象上其他字段的最终改动
实现 `IShrinkNetworkDeltaEvent` 后,入站按 `SessionId + EventType + DeltaKey` 记录最高 `DeltaVersion`;旧版本和重复版本不会进入 Bus
## 🔧 API 参考
## 接入
### ContextLoader 接入
```csharp
// ShrinkApp.Starter.Basic 组合根自动加入 ShrinkNetworkEventBusComponent。
// 组件注入 shrink.service.network 后注册,网络提供者撤回时注销。
```
### 显式接入
Standalone 或需要自定义 session 过滤时:
ContextLoader 默认通过 `ShrinkNetworkEventBusComponent` 接桥。Standalone
```csharp
service.UseEventBusBridge(new ShrinkNetworkEventBusBridgeOptions
{
SessionFilter = (session, evt) => session.SessionId > 0,
DispatchScheduler = new ShrinkNetworkUnityMainThreadDispatchScheduler()
SessionFilter = (session, value) => session.SessionId > 0
});
```
如果网络事件数量较大并且需要明确的每帧预算,使用 `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)
session.PublishEventAsync(eventData);
session.RequestEventAsync(eventData);
service.BroadcastEventAsync(eventData);
```
## 🏗️ 工作流
```
本地 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)
远端事件重入 Bus 时会抑制回环转发。网络桥只观察 `EventBus` 全局宿主管理的 Bus;独立 `ShrinkEventBusHost` 不会被自动网络转发。
@@ -1,9 +1,7 @@
#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;
@@ -16,38 +14,27 @@ namespace ShrinkNetwork.Integration
private sealed class ServiceRegistration
{
public ShrinkNetworkService Service = null!;
public Func<ShrinkNetworkSession, EventBase, bool>? SessionFilter;
public Func<ShrinkNetworkSession, IShrinkEvent, 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 Dictionary<Type, ShrinkNetworkEventRegistration> _networkEventTypes = new();
private static bool _initialized;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetStaticState()
{
EventBus.OnEventTriggered -= HandleLocalEventTriggered;
EventBus.Posted -= HandleLocalEventPosted;
lock (SyncRoot)
{
RegisteredServices.Clear();
FallbackDeltaVersions.Clear();
}
IncomingDispatchers.Clear();
IncomingRequestDispatchers.Clear();
_networkEventTypes = new Dictionary<Type, ShrinkNetworkMessageAttribute>();
_networkEventTypes = new Dictionary<Type, ShrinkNetworkEventRegistration>();
_initialized = false;
}
@@ -58,11 +45,11 @@ namespace ShrinkNetwork.Integration
_initialized = true;
RefreshNetworkEventTypes();
EventBus.OnEventTriggered += HandleLocalEventTriggered;
EventBus.Posted += HandleLocalEventPosted;
}
public static void RegisterService(ShrinkNetworkService service,
Func<ShrinkNetworkSession, EventBase, bool>? sessionFilter = null)
Func<ShrinkNetworkSession, IShrinkEvent, bool>? sessionFilter = null)
{
if (service == null)
throw new ArgumentNullException(nameof(service));
@@ -116,7 +103,7 @@ namespace ShrinkNetwork.Integration
}
public static UniTask PublishAsync<TEvent>(ShrinkNetworkSession session, TEvent eventArgs, string? route = null)
where TEvent : EventBase, IShrinkNetworkMessage
where TEvent : IShrinkEvent, IShrinkNetworkMessage
{
if (session == null)
throw new ArgumentNullException(nameof(session));
@@ -130,7 +117,7 @@ namespace ShrinkNetwork.Integration
public static async UniTask PublishAsync<TEvent>(IEnumerable<ShrinkNetworkSession> sessions, TEvent eventArgs,
string? route = null)
where TEvent : EventBase, IShrinkNetworkMessage
where TEvent : IShrinkEvent, IShrinkNetworkMessage
{
if (sessions == null)
throw new ArgumentNullException(nameof(sessions));
@@ -159,7 +146,7 @@ namespace ShrinkNetwork.Integration
public static async UniTask<ShrinkNetworkEventResultResponse> RequestResultAsync<TEvent>(ShrinkNetworkSession session,
TEvent eventArgs, ShrinkRpcCallOptions? options = null)
where TEvent : EventBase, IShrinkNetworkRequest
where TEvent : IShrinkResultEvent<EventResult>, IShrinkNetworkRequest
{
if (session == null)
throw new ArgumentNullException(nameof(session));
@@ -178,14 +165,14 @@ namespace ShrinkNetwork.Integration
if (response.IsSuccess)
{
eventArgs.SetResult(response.Result);
if (eventArgs.IsCancelable)
eventArgs.SetCanceled(response.IsCanceled);
if (eventArgs is IShrinkCancelableEvent cancelable)
cancelable.SetCanceled(response.IsCanceled);
}
return response;
}
private static void HandleLocalEventTriggered(EventBase eventArgs, Type eventType)
private static void HandleLocalEventPosted(IShrinkEvent eventArgs, Type eventType, ShrinkBusKey busKey)
{
if (eventArgs == null || eventType == null)
return;
@@ -201,7 +188,7 @@ namespace ShrinkNetwork.Integration
ForwardEventAsync(eventArgs, eventType, networkMessage).Forget();
}
private static async UniTaskVoid ForwardEventAsync(EventBase eventArgs, Type eventType,
private static async UniTaskVoid ForwardEventAsync(IShrinkEvent eventArgs, Type eventType,
IShrinkNetworkMessage networkMessage)
{
List<ServiceRegistration> registrations;
@@ -234,9 +221,8 @@ namespace ShrinkNetwork.Integration
continue;
}
// Complete every serializer pass before the first await. EventBase
// instances may be returned to EventPool immediately after the
// synchronous trigger returns.
// Complete every serializer pass before the first await so mutable
// event payloads cannot change between target sessions.
batches.Add((registration, sessions, payload));
}
@@ -269,13 +255,8 @@ namespace ShrinkNetwork.Integration
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);
}
service.RegisterMessage(typeof(ShrinkNetworkEventResultResponse), -300001,
"__integration/event_result_response");
}
var isRequestEvent = typeof(IShrinkNetworkRequest).IsAssignableFrom(pair.Key);
@@ -301,43 +282,37 @@ namespace ShrinkNetwork.Integration
private static UniTask DispatchIncomingAsync(ShrinkNetworkContext context, object message)
{
if (message is not EventBase eventArgs)
if (message is not IShrinkEvent 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>));
return _networkEventTypes.TryGetValue(eventArgs.GetType(), out var registration)
? registration.Dispatch(eventArgs)
: UniTask.CompletedTask;
}
private static UniTask<object?> DispatchIncomingRequestAsync(ShrinkNetworkContext context, object message)
{
if (message is not EventBase eventArgs)
if (message is not IShrinkEvent eventArgs)
{
return UniTask.FromResult<object?>(new ShrinkNetworkEventResultResponse
{
ErrorCode = ShrinkRpcErrorCode.InvalidResponse,
ErrorMessage = "Incoming network event request is not an EventBase."
ErrorMessage = "Incoming network event request does not implement IShrinkEvent."
});
}
var dispatcher = IncomingRequestDispatchers.GetOrAdd(eventArgs.GetType(), CreateIncomingRequestDispatcher);
return dispatcher(context, eventArgs);
return _networkEventTypes.TryGetValue(eventArgs.GetType(), out var registration) &&
registration.RequestDispatch != null
? registration.RequestDispatch(context, eventArgs)
: UniTask.FromResult<object?>(new ShrinkNetworkEventResultResponse
{
ErrorCode = ShrinkRpcErrorCode.InvalidResponse,
ErrorMessage = $"No generated network event request binding for {eventArgs.GetType().FullName}."
});
}
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
internal static async UniTask DispatchGeneratedAsync<TEvent>(TEvent eventArgs)
where TEvent : IShrinkEvent
{
SuppressForwardDepth.Value++;
try
@@ -348,7 +323,7 @@ namespace ShrinkNetwork.Integration
return;
}
await EventBus.TriggerEventAsync((TEvent)eventArgs);
await EventBus.PostAsync(eventArgs);
}
finally
{
@@ -356,23 +331,21 @@ namespace ShrinkNetwork.Integration
}
}
private static async UniTask<object?> DispatchIncomingRequestGenericAsync<TEvent>(ShrinkNetworkContext context,
EventBase eventArgs)
where TEvent : EventBase
internal static async UniTask<object?> DispatchGeneratedRequestAsync<TEvent>(
ShrinkNetworkContext context, TEvent eventArgs)
where TEvent : IShrinkEvent
{
var typedEvent = (TEvent)eventArgs;
SuppressForwardDepth.Value++;
try
{
if (TryGetDeltaEvent(typedEvent, typeof(TEvent), out var deltaEvent) &&
if (TryGetDeltaEvent(eventArgs, typeof(TEvent), out var deltaEvent) &&
!TryMarkIncomingDelta(typeof(TEvent), deltaEvent, context))
{
return BuildResultResponse(typedEvent);
return BuildResultResponse(eventArgs);
}
await EventBus.TriggerEventAsync(typedEvent);
return BuildResultResponse(typedEvent);
await EventBus.PostAsync(eventArgs);
return BuildResultResponse(eventArgs);
}
catch (Exception ex)
{
@@ -381,8 +354,10 @@ namespace ShrinkNetwork.Integration
{
ErrorCode = ShrinkRpcErrorCode.HandlerException,
ErrorMessage = ex.Message,
Result = typedEvent.HasResult ? typedEvent.Result : EventResult.DEFAULT,
IsCanceled = typedEvent.IsCancelable && typedEvent.IsCanceled
Result = eventArgs is IShrinkResultEvent<EventResult> resultEvent
? resultEvent.Result
: EventResult.DEFAULT,
IsCanceled = eventArgs is IShrinkCancelableEvent cancelable && cancelable.IsCanceled
};
}
finally
@@ -393,62 +368,30 @@ namespace ShrinkNetwork.Integration
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;
}
}
var eventTypes = new Dictionary<Type, ShrinkNetworkEventRegistration>();
foreach (var registration in ShrinkNetworkEventRegistry.Snapshot())
eventTypes[registration.EventType] = registration;
_networkEventTypes = eventTypes;
}
private static bool TryGetNetworkEventMeta(Type eventType, out ShrinkNetworkMessageAttribute? messageAttribute)
private static bool TryGetNetworkEventMeta(Type eventType,
out ShrinkNetworkEventRegistration registration)
{
if (eventType == null)
{
messageAttribute = null;
registration = default;
return false;
}
return _networkEventTypes.TryGetValue(eventType, out messageAttribute);
return _networkEventTypes.TryGetValue(eventType, out registration);
}
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].");
$"Event type {eventType.FullName} must implement IShrinkEvent + IShrinkNetworkMessage and declare [ShrinkNetworkEvent] + [ShrinkNetworkMessage].");
}
private static void EnsurePublishableEventType(Type eventType)
@@ -465,21 +408,23 @@ namespace ShrinkNetwork.Integration
if (!typeof(IShrinkNetworkRequest).IsAssignableFrom(eventType))
throw new InvalidOperationException(
$"Event type {eventType.FullName} must implement IShrinkNetworkRequest to use RequestResultAsync.");
if (eventType.GetCustomAttribute<HasResultAttribute>(false) == null)
if (!typeof(IShrinkResultEvent<EventResult>).IsAssignableFrom(eventType))
throw new InvalidOperationException(
$"Event type {eventType.FullName} must declare [HasResult] to use RequestResultAsync.");
$"Event type {eventType.FullName} must implement IShrinkResultEvent<EventResult> to use RequestResultAsync.");
}
private static ShrinkNetworkEventResultResponse BuildResultResponse(EventBase eventArgs)
private static ShrinkNetworkEventResultResponse BuildResultResponse(IShrinkEvent eventArgs)
{
return new ShrinkNetworkEventResultResponse
{
Result = eventArgs.HasResult ? eventArgs.Result : EventResult.DEFAULT,
IsCanceled = eventArgs.IsCancelable && eventArgs.IsCanceled
Result = eventArgs is IShrinkResultEvent<EventResult> resultEvent
? resultEvent.Result
: EventResult.DEFAULT,
IsCanceled = eventArgs is IShrinkCancelableEvent cancelable && cancelable.IsCanceled
};
}
private static bool TryGetDeltaEvent(EventBase eventArgs, Type eventType, out IShrinkNetworkDeltaEvent deltaEvent)
private static bool TryGetDeltaEvent(IShrinkEvent eventArgs, Type eventType, out IShrinkNetworkDeltaEvent deltaEvent)
{
if (eventArgs is IShrinkNetworkDeltaEvent eventDelta)
{
@@ -27,14 +27,14 @@ namespace ShrinkNetwork.Integration
}
public static UniTask PublishEventAsync<TEvent>(this ShrinkNetworkSession session, TEvent eventArgs, string? route = null)
where TEvent : EventBase, IShrinkNetworkMessage
where TEvent : IShrinkEvent, 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
where TEvent : IShrinkEvent, IShrinkNetworkMessage
{
if (service == null)
throw new ArgumentNullException(nameof(service));
@@ -59,7 +59,7 @@ namespace ShrinkNetwork.Integration
this ShrinkNetworkSession session,
TEvent eventArgs,
ShrinkRpcCallOptions? options = null)
where TEvent : EventBase, IShrinkNetworkRequest
where TEvent : IShrinkResultEvent<EventResult>, IShrinkNetworkRequest
{
var response = await ShrinkNetworkEventBusBridge.RequestResultAsync(session, eventArgs, options);
return ShrinkNetworkEventRequestOutcome.FromResponse(response);
@@ -8,7 +8,7 @@ namespace ShrinkNetwork.Integration
{
public bool AutoRegisterAttributedMessages { get; set; } = true;
public bool AutoRegisterStaticHandlers { get; set; }
public Func<ShrinkNetworkSession, EventBase, bool>? SessionFilter { get; set; }
public Func<ShrinkNetworkSession, IShrinkEvent, bool>? SessionFilter { get; set; }
public IShrinkNetworkDispatchScheduler? DispatchScheduler { get; set; }
}
}
@@ -0,0 +1,56 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using ShrinkEventBus;
namespace ShrinkNetwork.Integration
{
public readonly struct ShrinkNetworkEventRegistration
{
public ShrinkNetworkEventRegistration(Type eventType, int opcode, string? route,
Func<IShrinkEvent, UniTask> dispatch,
Func<ShrinkNetworkContext, IShrinkEvent, UniTask<object?>>? requestDispatch)
{
EventType = eventType ?? throw new ArgumentNullException(nameof(eventType));
Opcode = opcode;
Route = route;
Dispatch = dispatch ?? throw new ArgumentNullException(nameof(dispatch));
RequestDispatch = requestDispatch;
}
public Type EventType { get; }
public int Opcode { get; }
public string? Route { get; }
public Func<IShrinkEvent, UniTask> Dispatch { get; }
public Func<ShrinkNetworkContext, IShrinkEvent, UniTask<object?>>? RequestDispatch { get; }
}
public static class ShrinkNetworkEventRegistry
{
private static readonly object Gate = new();
private static readonly Dictionary<Type, ShrinkNetworkEventRegistration> Registrations = new();
public static void Register<TEvent>(int opcode, string? route)
where TEvent : IShrinkEvent, IShrinkNetworkMessage
{
var registration = new ShrinkNetworkEventRegistration(
typeof(TEvent), opcode, route,
static eventData => ShrinkNetworkEventBusBridge.DispatchGeneratedAsync((TEvent)eventData),
typeof(IShrinkNetworkRequest).IsAssignableFrom(typeof(TEvent))
? static (context, eventData) =>
ShrinkNetworkEventBusBridge.DispatchGeneratedRequestAsync(context, (TEvent)eventData)
: null);
lock (Gate)
Registrations[typeof(TEvent)] = registration;
}
internal static IReadOnlyList<ShrinkNetworkEventRegistration> Snapshot()
{
lock (Gate)
return Registrations.Values.ToArray();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a8863e85b074d7486cd06cbe46b8dd3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ba18b28ec93a44c2b78d95b2378f82d1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,18 @@
{
"name": "ShrinkNetwork.Integration.EventBus.Tests",
"rootNamespace": "ShrinkNetwork.Integration.EventBus.Tests",
"references": [
"ShrinkNetwork.Integration.EventBus",
"ShrinkNetwork.Runtime",
"ShrinkEventBus.Runtime",
"UniTask",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner"
],
"includePlatforms": ["Editor"],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": false,
"defineConstraints": ["UNITY_INCLUDE_TESTS"]
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 132ab80308b64e7abf3a68bb161b7d2f
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,36 @@
#nullable enable
using NUnit.Framework;
using ShrinkEventBus;
namespace ShrinkNetwork.Integration.EventBus.Tests
{
[ShrinkNetworkEvent]
[ShrinkNetworkMessage(49001, "tests/generated_event")]
internal sealed class GeneratedNetworkEvent : IShrinkEvent, IShrinkNetworkMessage
{
}
public sealed class ShrinkNetworkEventCodeGenTests
{
[Test]
public void GeneratedRegistrationAddsNetworkEventToService()
{
var service = new ShrinkNetworkService(
new ShrinkJsonNetworkSerializer(),
new ShrinkNetworkMessageRegistry(),
new ShrinkNetworkRouter());
ShrinkNetworkEventBusBridge.RegisterService(service);
try
{
Assert.IsTrue(service.MessageRegistry.TryGetMeta(typeof(GeneratedNetworkEvent), out var meta));
Assert.AreEqual(49001, meta!.Opcode);
Assert.AreEqual("tests/generated_event", meta.Route);
}
finally
{
ShrinkNetworkEventBusBridge.UnregisterService(service);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7d790812eb9845b0820da061de4d7fe5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -2,12 +2,12 @@
"name": "com.cneicy.shrink-network-integration-eventbus",
"version": "0.1.1",
"displayName": "ShrinkNetwork - EventBus Integration",
"description": "ShrinkNetwork 与 ShrinkEventBus 的桥接层,让特定 EventBase 事件类型可直接走网络同步并在远端重新分发到 EventBus。",
"description": "ShrinkNetwork 与 ShrinkEventBus 2.0 的生成式桥接层,让 IShrinkEvent 网络事件可广播、裁决并在远端重新进入命名 Bus。",
"unity": "2022.3",
"dependencies": {
"com.cneicy.shrink-network": "0.2.0",
"com.cneicy.shrink-context-core": "0.1.0",
"com.cneicy.shrink-eventbus": "1.3.0"
"com.cneicy.shrink-eventbus": "2.0.0"
},
"keywords": ["network", "eventbus", "integration", "bridge", "sync"],
"author": {