commit 05bfe9c430b61c0a33987696cd4663b261cb62d4 Author: cneicy Date: Wed Aug 26 02:50:38 2026 +0800 chore: initialize standalone UPM package 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: