From 05bfe9c430b61c0a33987696cd4663b261cb62d4 Mon Sep 17 00:00:00 2001 From: cneicy Date: Wed, 26 Aug 2026 02:50:38 +0800 Subject: [PATCH] chore: initialize standalone UPM package --- .gitea/workflows/publish.yml | 49 ++ .gitea/workflows/unity-verify.yml | 39 ++ .gitignore | 10 + .npmignore | 8 + CHANGELOG.md | 15 + CHANGELOG.md.meta | 7 + CodeGen.meta | 8 + CodeGen/Editor.meta | 8 + CodeGen/Editor/NetworkEventILPostProcessor.cs | 184 +++++++ .../NetworkEventILPostProcessor.cs.meta | 11 + ...nity.ShrinkNetwork.EventBus.CodeGen.asmdef | 19 + ...ShrinkNetwork.EventBus.CodeGen.asmdef.meta | 7 + Development~/UnityProject/.gitignore | 6 + Development~/UnityProject/Assets/.gitkeep | 0 .../UnityProject/Packages/manifest.json | 15 + .../ProjectSettings/ProjectVersion.txt | 2 + README.md | 70 +++ README.md.meta | 7 + ShrinkNetwork.Integration.EventBus.asmdef | 19 + ...nkNetwork.Integration.EventBus.asmdef.meta | 7 + ShrinkNetworkDeltaEventContracts.cs | 17 + ShrinkNetworkDeltaEventContracts.cs.meta | 11 + ShrinkNetworkEventAttribute.cs | 9 + ShrinkNetworkEventAttribute.cs.meta | 11 + ShrinkNetworkEventBusBridge.cs | 496 ++++++++++++++++++ ShrinkNetworkEventBusBridge.cs.meta | 11 + ShrinkNetworkEventBusBridgeExtensions.cs | 68 +++ ShrinkNetworkEventBusBridgeExtensions.cs.meta | 11 + ShrinkNetworkEventBusBridgeOptions.cs | 14 + ShrinkNetworkEventBusBridgeOptions.cs.meta | 11 + ShrinkNetworkEventBusComponent.cs | 35 ++ ShrinkNetworkEventBusComponent.cs.meta | 11 + ShrinkNetworkEventRegistry.cs | 56 ++ ShrinkNetworkEventRegistry.cs.meta | 11 + ShrinkNetworkEventRequestOutcome.cs | 31 ++ ShrinkNetworkEventRequestOutcome.cs.meta | 11 + Tests.meta | 8 + ...kNetwork.Integration.EventBus.Tests.asmdef | 18 + ...ork.Integration.EventBus.Tests.asmdef.meta | 7 + Tests/ShrinkNetworkEventCodeGenTests.cs | 36 ++ Tests/ShrinkNetworkEventCodeGenTests.cs.meta | 11 + package.json | 25 + package.json.meta | 7 + 43 files changed, 1417 insertions(+) create mode 100644 .gitea/workflows/publish.yml create mode 100644 .gitea/workflows/unity-verify.yml create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 CHANGELOG.md create mode 100644 CHANGELOG.md.meta create mode 100644 CodeGen.meta create mode 100644 CodeGen/Editor.meta create mode 100644 CodeGen/Editor/NetworkEventILPostProcessor.cs create mode 100644 CodeGen/Editor/NetworkEventILPostProcessor.cs.meta create mode 100644 CodeGen/Editor/Unity.ShrinkNetwork.EventBus.CodeGen.asmdef create mode 100644 CodeGen/Editor/Unity.ShrinkNetwork.EventBus.CodeGen.asmdef.meta create mode 100644 Development~/UnityProject/.gitignore create mode 100644 Development~/UnityProject/Assets/.gitkeep create mode 100644 Development~/UnityProject/Packages/manifest.json create mode 100644 Development~/UnityProject/ProjectSettings/ProjectVersion.txt create mode 100644 README.md create mode 100644 README.md.meta create mode 100644 ShrinkNetwork.Integration.EventBus.asmdef create mode 100644 ShrinkNetwork.Integration.EventBus.asmdef.meta create mode 100644 ShrinkNetworkDeltaEventContracts.cs create mode 100644 ShrinkNetworkDeltaEventContracts.cs.meta create mode 100644 ShrinkNetworkEventAttribute.cs create mode 100644 ShrinkNetworkEventAttribute.cs.meta create mode 100644 ShrinkNetworkEventBusBridge.cs create mode 100644 ShrinkNetworkEventBusBridge.cs.meta create mode 100644 ShrinkNetworkEventBusBridgeExtensions.cs create mode 100644 ShrinkNetworkEventBusBridgeExtensions.cs.meta create mode 100644 ShrinkNetworkEventBusBridgeOptions.cs create mode 100644 ShrinkNetworkEventBusBridgeOptions.cs.meta create mode 100644 ShrinkNetworkEventBusComponent.cs create mode 100644 ShrinkNetworkEventBusComponent.cs.meta create mode 100644 ShrinkNetworkEventRegistry.cs create mode 100644 ShrinkNetworkEventRegistry.cs.meta create mode 100644 ShrinkNetworkEventRequestOutcome.cs create mode 100644 ShrinkNetworkEventRequestOutcome.cs.meta create mode 100644 Tests.meta create mode 100644 Tests/ShrinkNetwork.Integration.EventBus.Tests.asmdef create mode 100644 Tests/ShrinkNetwork.Integration.EventBus.Tests.asmdef.meta create mode 100644 Tests/ShrinkNetworkEventCodeGenTests.cs create mode 100644 Tests/ShrinkNetworkEventCodeGenTests.cs.meta create mode 100644 package.json create mode 100644 package.json.meta diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..d10b31a --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,49 @@ +name: Publish UPM package + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + env: + NODE_AUTH_TOKEN: ${{ secrets.SHRINKSDK_PACKAGE_TOKEN }} + steps: + - name: Fetch tagged revision + shell: bash + run: | + set -eu + ref="${{ gitea.sha }}" + test -n "$ref" + git init . + git remote add origin "https://git.crash.work/ShrinkSDK/ShrinkNetwork.Integration.EventBus.git" + git fetch --depth=1 origin "$ref" + git checkout --detach FETCH_HEAD + + - name: Validate immutable release version + shell: bash + run: | + set -eu + tag="$(git describe --exact-match --tags HEAD)" + version="$(node -p "require('./package.json').version")" + test "$tag" = "v$version" + npm pack --dry-run + + - name: Publish to ShrinkSDK registry + shell: bash + run: | + set -eu + : "${NODE_AUTH_TOKEN:?SHRINKSDK_PACKAGE_TOKEN is required}" + npmrc="$HOME/.npmrc" + cleanup() { rm -f "$npmrc"; } + trap cleanup EXIT + printf '%s\n' \ + 'registry=https://git.crash.work/api/packages/ShrinkSDK/npm/' \ + '//git.crash.work/api/packages/ShrinkSDK/npm/:_authToken=${NODE_AUTH_TOKEN}' > "$npmrc" + npm publish --registry=https://git.crash.work/api/packages/ShrinkSDK/npm/ \ No newline at end of file diff --git a/.gitea/workflows/unity-verify.yml b/.gitea/workflows/unity-verify.yml new file mode 100644 index 0000000..fb4984d --- /dev/null +++ b/.gitea/workflows/unity-verify.yml @@ -0,0 +1,39 @@ +name: Verify standalone Unity package + +on: + workflow_dispatch: + +jobs: + editmode: + runs-on: unity-2022.3.62f3 + container: + image: docker.1panel.live/unityci/editor:ubuntu-2022.3.62f3-windows-mono-3 + volumes: + - /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-license:/root/.local/share/unity3d/Unity + - /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-entitlements:/root/.config/unity3d/Unity/licenses + steps: + - name: Fetch selected revision + shell: bash + run: | + set -eu + ref="${{ gitea.sha }}" + git init . + git remote add origin "https://git.crash.work/ShrinkSDK/ShrinkNetwork.Integration.EventBus.git" + git fetch --depth=1 origin "$ref" + git checkout --detach FETCH_HEAD + + - name: Run package EditMode tests + shell: bash + run: | + set -eu + unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)" + test -n "$unity_bin" + "$unity_bin" \ + -batchmode \ + -nographics \ + -quit \ + -projectPath "$PWD/Development~/UnityProject" \ + -runTests \ + -testPlatform EditMode \ + -testResults "$PWD/TestResults/editmode.xml" \ + -logFile "$PWD/TestResults/unity.log" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..246935d --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/Development~/UnityProject/[Ll]ibrary/ +/Development~/UnityProject/[Tt]emp/ +/Development~/UnityProject/[Oo]bj/ +/Development~/UnityProject/[Ll]ogs/ +/Development~/UnityProject/[Uu]ser[Ss]ettings/ +/Development~/UnityProject/TestResults/ +/Tools~/**/[Bb]in/ +/Tools~/**/[Oo]bj/ +*.user +*.DotSettings.user diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..700f03d --- /dev/null +++ b/.npmignore @@ -0,0 +1,8 @@ +.git/ +.gitea/ +Development~/ +Tools~/ +*.csproj +*.sln +*.user +*.DotSettings.user diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5f00f51 --- /dev/null +++ b/CHANGELOG.md @@ -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 重写,补齐桥接范式、事件语义和接入说明。 diff --git a/CHANGELOG.md.meta b/CHANGELOG.md.meta new file mode 100644 index 0000000..5fbb786 --- /dev/null +++ b/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d177dfebca8947e438bde6305bb342a7 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CodeGen.meta b/CodeGen.meta new file mode 100644 index 0000000..1f1cfec --- /dev/null +++ b/CodeGen.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c674d22418294a93b1da9df8149c49e9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CodeGen/Editor.meta b/CodeGen/Editor.meta new file mode 100644 index 0000000..6d69957 --- /dev/null +++ b/CodeGen/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a63a45e5ab8a4de682ca810f8a09cbca +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CodeGen/Editor/NetworkEventILPostProcessor.cs b/CodeGen/Editor/NetworkEventILPostProcessor.cs new file mode 100644 index 0000000..1d40d61 --- /dev/null +++ b/CodeGen/Editor/NetworkEventILPostProcessor.cs @@ -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(); + 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 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 == ""); + 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 GetAllTypes(IEnumerable 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 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); + } + } +} diff --git a/CodeGen/Editor/NetworkEventILPostProcessor.cs.meta b/CodeGen/Editor/NetworkEventILPostProcessor.cs.meta new file mode 100644 index 0000000..30f0705 --- /dev/null +++ b/CodeGen/Editor/NetworkEventILPostProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 64b8b451fc094da2a1547ade9f5ba4dd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CodeGen/Editor/Unity.ShrinkNetwork.EventBus.CodeGen.asmdef b/CodeGen/Editor/Unity.ShrinkNetwork.EventBus.CodeGen.asmdef new file mode 100644 index 0000000..e5d4051 --- /dev/null +++ b/CodeGen/Editor/Unity.ShrinkNetwork.EventBus.CodeGen.asmdef @@ -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 +} diff --git a/CodeGen/Editor/Unity.ShrinkNetwork.EventBus.CodeGen.asmdef.meta b/CodeGen/Editor/Unity.ShrinkNetwork.EventBus.CodeGen.asmdef.meta new file mode 100644 index 0000000..3bfe878 --- /dev/null +++ b/CodeGen/Editor/Unity.ShrinkNetwork.EventBus.CodeGen.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a0970354612f4fb6bdc88828308160c5 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Development~/UnityProject/.gitignore b/Development~/UnityProject/.gitignore new file mode 100644 index 0000000..aa2eb00 --- /dev/null +++ b/Development~/UnityProject/.gitignore @@ -0,0 +1,6 @@ +[Ll]ibrary/ +[Tt]emp/ +[Oo]bj/ +[Ll]ogs/ +[Uu]ser[Ss]ettings/ +TestResults/ \ No newline at end of file diff --git a/Development~/UnityProject/Assets/.gitkeep b/Development~/UnityProject/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Development~/UnityProject/Packages/manifest.json b/Development~/UnityProject/Packages/manifest.json new file mode 100644 index 0000000..16ab67c --- /dev/null +++ b/Development~/UnityProject/Packages/manifest.json @@ -0,0 +1,15 @@ +{ + "scopedRegistries": [ + { + "name": "ShrinkSDK", + "url": "https://git.crash.work/api/packages/ShrinkSDK/npm/", + "scopes": [ + "com.cneicy" + ] + } + ], + "dependencies": { + "com.unity.test-framework": "1.1.33", + "com.cneicy.shrink-network-integration-eventbus": "file:../../.." + } +} diff --git a/Development~/UnityProject/ProjectSettings/ProjectVersion.txt b/Development~/UnityProject/ProjectSettings/ProjectVersion.txt new file mode 100644 index 0000000..587f809 --- /dev/null +++ b/Development~/UnityProject/ProjectSettings/ProjectVersion.txt @@ -0,0 +1,2 @@ +m_EditorVersion: 2022.3.62f3 +m_EditorVersionWithRevision: 2022.3.62f3 (96770f904ca7) diff --git a/README.md b/README.md new file mode 100644 index 0000000..e9734f4 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# ShrinkNetwork.Integration.EventBus + +把实现 `IShrinkEvent` 的网络事件与 ShrinkEventBus 2.0 连接。广播事件可从本地 Bus 转发到远端;请求事件可在远端 `PostAsync` 后回传 `EventResult` 与取消状态;delta 事件按版本去重。 + +## 广播事件 + +```csharp +[ShrinkNetworkEvent] +[ShrinkNetworkMessage(3001, "room/player_ready")] +public sealed class PlayerReadyEvent : IShrinkEvent, IShrinkNetworkMessage +{ + public string PlayerId { get; set; } = string.Empty; +} +``` + +```csharp +EventBus.Post(new PlayerReadyEvent { PlayerId = "10001" }); +await session.PublishEventAsync(new PlayerReadyEvent { PlayerId = "10001" }); +``` + +事件必须同时实现 `IShrinkEvent`、`IShrinkNetworkMessage`,并声明 `[ShrinkNetworkEvent]` 与 `[ShrinkNetworkMessage]`。本包的 ILPostProcessor 为每个事件生成强类型入站 dispatcher 和模块注册,不在运行时扫描程序集、构造泛型方法或反射调用 handler。 + +## 远端裁决 + +```csharp +[ShrinkNetworkEvent] +[ShrinkNetworkMessage(3010, "room/can_use_skill")] +public sealed class CanUseSkillEvent : + IShrinkResultEvent, IShrinkCancelableEvent, IShrinkNetworkRequest +{ + 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()); +``` + +请求事件必须实现 `IShrinkResultEvent` 与 `IShrinkNetworkRequest`。如果需要取消回传,再实现 `IShrinkCancelableEvent`。当前响应只包含 `EventResult`、取消状态和 RPC 错误,不自动回传事件上的其它可变字段。 + +## Delta + +实现 `IShrinkNetworkDeltaEvent` 后,入站按 `SessionId + EventType + DeltaKey` 记录最高 `DeltaVersion`;旧版本和重复版本不会进入 Bus。 + +## 接入 + +ContextLoader 默认通过 `ShrinkNetworkEventBusComponent` 接桥。Standalone: + +```csharp +service.UseEventBusBridge(new ShrinkNetworkEventBusBridgeOptions +{ + SessionFilter = (session, value) => session.SessionId > 0 +}); +``` + +可用发送扩展: + +```csharp +session.PublishEventAsync(eventData); +session.RequestEventAsync(eventData); +service.BroadcastEventAsync(eventData); +``` + +远端事件重入 Bus 时会抑制回环转发。网络桥只观察 `EventBus` 全局宿主管理的 Bus;独立 `ShrinkEventBusHost` 不会被自动网络转发。 diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..a09444e --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c7592055b3962e549a3a54179b99be3d +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetwork.Integration.EventBus.asmdef b/ShrinkNetwork.Integration.EventBus.asmdef new file mode 100644 index 0000000..ce4fd80 --- /dev/null +++ b/ShrinkNetwork.Integration.EventBus.asmdef @@ -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 +} diff --git a/ShrinkNetwork.Integration.EventBus.asmdef.meta b/ShrinkNetwork.Integration.EventBus.asmdef.meta new file mode 100644 index 0000000..9a9738b --- /dev/null +++ b/ShrinkNetwork.Integration.EventBus.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5642850b24bc7d248b4d2ff56c5275e5 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkDeltaEventContracts.cs b/ShrinkNetworkDeltaEventContracts.cs new file mode 100644 index 0000000..748fbe6 --- /dev/null +++ b/ShrinkNetworkDeltaEventContracts.cs @@ -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; } + } +} diff --git a/ShrinkNetworkDeltaEventContracts.cs.meta b/ShrinkNetworkDeltaEventContracts.cs.meta new file mode 100644 index 0000000..a91b956 --- /dev/null +++ b/ShrinkNetworkDeltaEventContracts.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bd46e5dac04669543aee6fd72fd9adea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkEventAttribute.cs b/ShrinkNetworkEventAttribute.cs new file mode 100644 index 0000000..9270c18 --- /dev/null +++ b/ShrinkNetworkEventAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace ShrinkNetwork.Integration +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class ShrinkNetworkEventAttribute : Attribute + { + } +} diff --git a/ShrinkNetworkEventAttribute.cs.meta b/ShrinkNetworkEventAttribute.cs.meta new file mode 100644 index 0000000..8e78494 --- /dev/null +++ b/ShrinkNetworkEventAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3006890f24213734a9a8d7ccfb440c5f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkEventBusBridge.cs b/ShrinkNetworkEventBusBridge.cs new file mode 100644 index 0000000..4e0d717 --- /dev/null +++ b/ShrinkNetworkEventBusBridge.cs @@ -0,0 +1,496 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +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? SessionFilter; + public Dictionary AppliedDeltaVersions { get; } = new(); + } + + private static readonly object SyncRoot = new(); + private static readonly Dictionary RegisteredServices = new(); + private static readonly AsyncLocal SuppressForwardDepth = new(); + private static Dictionary _networkEventTypes = new(); + private static bool _initialized; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetStaticState() + { + EventBus.Posted -= HandleLocalEventPosted; + lock (SyncRoot) + { + RegisteredServices.Clear(); + FallbackDeltaVersions.Clear(); + } + + _networkEventTypes = new Dictionary(); + _initialized = false; + } + + private static void EnsureBridgeInitialized() + { + if (_initialized) + return; + + _initialized = true; + RefreshNetworkEventTypes(); + EventBus.Posted += HandleLocalEventPosted; + } + + public static void RegisterService(ShrinkNetworkService service, + Func? 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 services; + lock (SyncRoot) + { + services = RegisteredServices.Keys.ToList(); + } + + foreach (var service in services) + { + RegisterInboundBridgeHandlers(service); + } + } + + public static UniTask PublishAsync(ShrinkNetworkSession session, TEvent eventArgs, string? route = null) + where TEvent : IShrinkEvent, 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(IEnumerable sessions, TEvent eventArgs, + string? route = null) + where TEvent : IShrinkEvent, 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 RequestResultAsync(ShrinkNetworkSession session, + TEvent eventArgs, ShrinkRpcCallOptions? options = null) + where TEvent : IShrinkResultEvent, 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(eventArgs, + options); + + if (response.IsSuccess) + { + eventArgs.SetResult(response.Result); + if (eventArgs is IShrinkCancelableEvent cancelable) + cancelable.SetCanceled(response.IsCanceled); + } + + return response; + } + + private static void HandleLocalEventPosted(IShrinkEvent eventArgs, Type eventType, ShrinkBusKey busKey) + { + 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(IShrinkEvent eventArgs, Type eventType, + IShrinkNetworkMessage networkMessage) + { + List registrations; + lock (SyncRoot) + { + registrations = RegisteredServices.Values.ToList(); + } + + var batches = new List<(ServiceRegistration Registration, List 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 so mutable + // event payloads cannot change between target sessions. + 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 _)) + { + service.RegisterMessage(typeof(ShrinkNetworkEventResultResponse), -300001, + "__integration/event_result_response"); + } + + 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 IShrinkEvent eventArgs) + return UniTask.CompletedTask; + + return _networkEventTypes.TryGetValue(eventArgs.GetType(), out var registration) + ? registration.Dispatch(eventArgs) + : UniTask.CompletedTask; + } + + private static UniTask DispatchIncomingRequestAsync(ShrinkNetworkContext context, object message) + { + if (message is not IShrinkEvent eventArgs) + { + return UniTask.FromResult(new ShrinkNetworkEventResultResponse + { + ErrorCode = ShrinkRpcErrorCode.InvalidResponse, + ErrorMessage = "Incoming network event request does not implement IShrinkEvent." + }); + } + + return _networkEventTypes.TryGetValue(eventArgs.GetType(), out var registration) && + registration.RequestDispatch != null + ? registration.RequestDispatch(context, eventArgs) + : UniTask.FromResult(new ShrinkNetworkEventResultResponse + { + ErrorCode = ShrinkRpcErrorCode.InvalidResponse, + ErrorMessage = $"No generated network event request binding for {eventArgs.GetType().FullName}." + }); + } + + internal static async UniTask DispatchGeneratedAsync(TEvent eventArgs) + where TEvent : IShrinkEvent + { + SuppressForwardDepth.Value++; + try + { + if (TryGetDeltaEvent(eventArgs, typeof(TEvent), out var deltaEvent) && + !TryMarkIncomingDelta(typeof(TEvent), deltaEvent, null)) + { + return; + } + + await EventBus.PostAsync(eventArgs); + } + finally + { + SuppressForwardDepth.Value--; + } + } + + internal static async UniTask DispatchGeneratedRequestAsync( + ShrinkNetworkContext context, TEvent eventArgs) + where TEvent : IShrinkEvent + { + SuppressForwardDepth.Value++; + try + { + if (TryGetDeltaEvent(eventArgs, typeof(TEvent), out var deltaEvent) && + !TryMarkIncomingDelta(typeof(TEvent), deltaEvent, context)) + { + return BuildResultResponse(eventArgs); + } + + await EventBus.PostAsync(eventArgs); + return BuildResultResponse(eventArgs); + } + catch (Exception ex) + { + Debug.LogException(ex); + return new ShrinkNetworkEventResultResponse + { + ErrorCode = ShrinkRpcErrorCode.HandlerException, + ErrorMessage = ex.Message, + Result = eventArgs is IShrinkResultEvent resultEvent + ? resultEvent.Result + : EventResult.DEFAULT, + IsCanceled = eventArgs is IShrinkCancelableEvent cancelable && cancelable.IsCanceled + }; + } + finally + { + SuppressForwardDepth.Value--; + } + } + + private static void RefreshNetworkEventTypes() + { + var eventTypes = new Dictionary(); + foreach (var registration in ShrinkNetworkEventRegistry.Snapshot()) + eventTypes[registration.EventType] = registration; + + _networkEventTypes = eventTypes; + } + + private static bool TryGetNetworkEventMeta(Type eventType, + out ShrinkNetworkEventRegistration registration) + { + if (eventType == null) + { + registration = default; + return false; + } + + 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 IShrinkEvent + 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 (!typeof(IShrinkResultEvent).IsAssignableFrom(eventType)) + throw new InvalidOperationException( + $"Event type {eventType.FullName} must implement IShrinkResultEvent to use RequestResultAsync."); + } + + private static ShrinkNetworkEventResultResponse BuildResultResponse(IShrinkEvent eventArgs) + { + return new ShrinkNetworkEventResultResponse + { + Result = eventArgs is IShrinkResultEvent resultEvent + ? resultEvent.Result + : EventResult.DEFAULT, + IsCanceled = eventArgs is IShrinkCancelableEvent cancelable && cancelable.IsCanceled + }; + } + + private static bool TryGetDeltaEvent(IShrinkEvent 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 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 FallbackDeltaVersions = new(); + } + + /// + /// 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. + /// + public sealed class ShrinkNetworkUnityMainThreadDispatchScheduler : IShrinkNetworkDispatchScheduler + { + public async UniTask ScheduleAsync(Func callback) + { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + await UniTask.SwitchToMainThread(); + await callback(); + return true; + } + } +} diff --git a/ShrinkNetworkEventBusBridge.cs.meta b/ShrinkNetworkEventBusBridge.cs.meta new file mode 100644 index 0000000..5e3f614 --- /dev/null +++ b/ShrinkNetworkEventBusBridge.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aaece45108fa6d64db2ef4a496c4a373 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkEventBusBridgeExtensions.cs b/ShrinkNetworkEventBusBridgeExtensions.cs new file mode 100644 index 0000000..4e35970 --- /dev/null +++ b/ShrinkNetworkEventBusBridgeExtensions.cs @@ -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(this ShrinkNetworkSession session, TEvent eventArgs, string? route = null) + where TEvent : IShrinkEvent, IShrinkNetworkMessage + { + return ShrinkNetworkEventBusBridge.PublishAsync(session, eventArgs, route); + } + + public static UniTask BroadcastEventAsync(this ShrinkNetworkService service, TEvent eventArgs, + Func? sessionFilter = null, string? route = null) + where TEvent : IShrinkEvent, IShrinkNetworkMessage + { + if (service == null) + throw new ArgumentNullException(nameof(service)); + + IEnumerable sessions = service.Sessions.Values; + if (sessionFilter != null) + { + var filtered = new List(); + 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 RequestEventAsync( + this ShrinkNetworkSession session, + TEvent eventArgs, + ShrinkRpcCallOptions? options = null) + where TEvent : IShrinkResultEvent, IShrinkNetworkRequest + { + var response = await ShrinkNetworkEventBusBridge.RequestResultAsync(session, eventArgs, options); + return ShrinkNetworkEventRequestOutcome.FromResponse(response); + } + } +} diff --git a/ShrinkNetworkEventBusBridgeExtensions.cs.meta b/ShrinkNetworkEventBusBridgeExtensions.cs.meta new file mode 100644 index 0000000..fa87a3c --- /dev/null +++ b/ShrinkNetworkEventBusBridgeExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 942526ca2f2df5441b6c742f2476651a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkEventBusBridgeOptions.cs b/ShrinkNetworkEventBusBridgeOptions.cs new file mode 100644 index 0000000..e4f5f2d --- /dev/null +++ b/ShrinkNetworkEventBusBridgeOptions.cs @@ -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? SessionFilter { get; set; } + public IShrinkNetworkDispatchScheduler? DispatchScheduler { get; set; } + } +} diff --git a/ShrinkNetworkEventBusBridgeOptions.cs.meta b/ShrinkNetworkEventBusBridgeOptions.cs.meta new file mode 100644 index 0000000..a36ffdd --- /dev/null +++ b/ShrinkNetworkEventBusBridgeOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b24445c5c1c712a40bc2958fbf7bf313 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkEventBusComponent.cs b/ShrinkNetworkEventBusComponent.cs new file mode 100644 index 0000000..bb6e984 --- /dev/null +++ b/ShrinkNetworkEventBusComponent.cs @@ -0,0 +1,35 @@ +#nullable enable + +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using ShrinkContext; + +namespace ShrinkNetwork.Integration +{ + /// 通过网络服务键管理 Network ↔ EventBus 桥的可逆生命周期。 + 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 Inject => InjectKeys; + public IReadOnlyList Provide => ProvideKeys; + + public UniTask ApplyAsync(ShrinkCtx ctx, object? config) + { + var service = ctx.Get(NetworkServiceKey); + service.UseEventBusBridge(config as ShrinkNetworkEventBusBridgeOptions); + ctx.EffectInverse(() => + { + ShrinkNetworkEventBusBridge.UnregisterService(service); + return UniTask.CompletedTask; + }); + ctx.Set(ProvideKey, Name); + return UniTask.CompletedTask; + } + } +} diff --git a/ShrinkNetworkEventBusComponent.cs.meta b/ShrinkNetworkEventBusComponent.cs.meta new file mode 100644 index 0000000..4b698fb --- /dev/null +++ b/ShrinkNetworkEventBusComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: baeaef51e2232ff449b18fb337209ab1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkEventRegistry.cs b/ShrinkNetworkEventRegistry.cs new file mode 100644 index 0000000..7fe3914 --- /dev/null +++ b/ShrinkNetworkEventRegistry.cs @@ -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 dispatch, + Func>? 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 Dispatch { get; } + public Func>? RequestDispatch { get; } + } + + public static class ShrinkNetworkEventRegistry + { + private static readonly object Gate = new(); + private static readonly Dictionary Registrations = new(); + + public static void Register(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 Snapshot() + { + lock (Gate) + return Registrations.Values.ToArray(); + } + } +} diff --git a/ShrinkNetworkEventRegistry.cs.meta b/ShrinkNetworkEventRegistry.cs.meta new file mode 100644 index 0000000..402b6ab --- /dev/null +++ b/ShrinkNetworkEventRegistry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5a8863e85b074d7486cd06cbe46b8dd3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkEventRequestOutcome.cs b/ShrinkNetworkEventRequestOutcome.cs new file mode 100644 index 0000000..99ce3fb --- /dev/null +++ b/ShrinkNetworkEventRequestOutcome.cs @@ -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); + } + } +} diff --git a/ShrinkNetworkEventRequestOutcome.cs.meta b/ShrinkNetworkEventRequestOutcome.cs.meta new file mode 100644 index 0000000..c78eebc --- /dev/null +++ b/ShrinkNetworkEventRequestOutcome.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ea5612b9af38aa049b71da7196dda594 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests.meta b/Tests.meta new file mode 100644 index 0000000..8380388 --- /dev/null +++ b/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ba18b28ec93a44c2b78d95b2378f82d1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/ShrinkNetwork.Integration.EventBus.Tests.asmdef b/Tests/ShrinkNetwork.Integration.EventBus.Tests.asmdef new file mode 100644 index 0000000..4af32df --- /dev/null +++ b/Tests/ShrinkNetwork.Integration.EventBus.Tests.asmdef @@ -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"] +} diff --git a/Tests/ShrinkNetwork.Integration.EventBus.Tests.asmdef.meta b/Tests/ShrinkNetwork.Integration.EventBus.Tests.asmdef.meta new file mode 100644 index 0000000..170f491 --- /dev/null +++ b/Tests/ShrinkNetwork.Integration.EventBus.Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 132ab80308b64e7abf3a68bb161b7d2f +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/ShrinkNetworkEventCodeGenTests.cs b/Tests/ShrinkNetworkEventCodeGenTests.cs new file mode 100644 index 0000000..25e5307 --- /dev/null +++ b/Tests/ShrinkNetworkEventCodeGenTests.cs @@ -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); + } + } + } +} diff --git a/Tests/ShrinkNetworkEventCodeGenTests.cs.meta b/Tests/ShrinkNetworkEventCodeGenTests.cs.meta new file mode 100644 index 0000000..bfdd624 --- /dev/null +++ b/Tests/ShrinkNetworkEventCodeGenTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7d790812eb9845b0820da061de4d7fe5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..460ac06 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "com.cneicy.shrink-network-integration-eventbus", + "version": "0.1.1", + "displayName": "ShrinkNetwork - EventBus Integration", + "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": "2.0.0" + }, + "keywords": [ + "network", + "eventbus", + "integration", + "bridge", + "sync" + ], + "author": { + "name": "cneicy", + "url": "https://git.crash.work/ShrinkSDK" + }, + "documentationUrl": "https://git.crash.work/ShrinkSDK/ShrinkNetwork.Integration.EventBus", + "changelogUrl": "https://git.crash.work/ShrinkSDK/ShrinkNetwork.Integration.EventBus/src/branch/main/CHANGELOG.md" +} diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..fb67a47 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 157418d926bfc4d44946fc8f26531f74 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: