feat(cordis): 接入上下文组合与模组事务热替换

This commit is contained in:
2026-08-16 23:20:40 +08:00
commit ad256f109b
676 changed files with 52168 additions and 0 deletions
@@ -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:
@@ -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
}
@@ -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; }
}
}
@@ -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:
@@ -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);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 942526ca2f2df5441b6c742f2476651a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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; }
}
}
@@ -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;
}
}
}
@@ -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);
}
}
}
@@ -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: