commit f9b05b4178cf7ab1bb2c67f560bb82867466210e Author: cneicy Date: Wed Aug 26 02:49:49 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..f2add20 --- /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/ShrinkCommand.Integration.Network.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..0de59f0 --- /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/ShrinkCommand.Integration.Network.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..3885224 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +本文件记录 `ShrinkCommand.Integration.Network` 在当前工作区中的包内变更。 + +## [0.1.0] - 2026-04-07 + +### Added + +- 增加 `command/execute` RPC 桥接。 +- 远程会话可自动映射为 `IShrinkCommandSource`,统一走命令运行时。 +- 命令执行结果可直接通过 `ShrinkNetwork` 返回给远端调用方。 + +### Changed + +- 桥接层公共类型补齐 `#nullable enable`,对齐当前运行时与宿主侧的可空语义。 +- 保持桥接层职责边界,命令核心仍不直接依赖网络实现。 diff --git a/CHANGELOG.md.meta b/CHANGELOG.md.meta new file mode 100644 index 0000000..b599f33 --- /dev/null +++ b/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: cc0d137c46b092f49a54bc3afcd7c161 +TextScriptImporter: + 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..09f0738 --- /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-command-integration-network": "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..08fe4e3 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# ShrinkCommand.Integration.Network + +`ShrinkCommand` 与 `ShrinkNetwork` 的桥接层。 + +## 能力 + +- 通过网络 RPC 执行命令 +- 远程命令来源自动映射为 `IShrinkCommandSource` +- 继续复用命令系统自己的权限与来源限制 + +## 接入 + +```csharp +commandService = new ShrinkCommandService(); +commandService.AutoRegisterAll(); + +networkService.UseCommandBridge(commandService); +``` + +客户端调用: + +```csharp +var response = await session.ExecuteCommandAsync("help"); +``` diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..4995074 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c85d4974b8207cf45848e1379ad7baa3 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkCommand.Integration.Network.asmdef b/ShrinkCommand.Integration.Network.asmdef new file mode 100644 index 0000000..da2194a --- /dev/null +++ b/ShrinkCommand.Integration.Network.asmdef @@ -0,0 +1,19 @@ +{ + "name": "ShrinkCommand.Integration.Network", + "rootNamespace": "ShrinkCommand.Integration", + "references": [ + "ShrinkCommand.Runtime", + "ShrinkNetwork.Runtime", + "ShrinkContext.Core.Runtime", + "UniTask" + ], + "optionalUnityReferences": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/ShrinkCommand.Integration.Network.asmdef.meta b/ShrinkCommand.Integration.Network.asmdef.meta new file mode 100644 index 0000000..f36a41e --- /dev/null +++ b/ShrinkCommand.Integration.Network.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d9c305f415a0515409b2795a809e7240 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkCommandNetworkIntegrationComponent.cs b/ShrinkCommandNetworkIntegrationComponent.cs new file mode 100644 index 0000000..e880650 --- /dev/null +++ b/ShrinkCommandNetworkIntegrationComponent.cs @@ -0,0 +1,49 @@ +#nullable enable +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using ShrinkCommand; +using ShrinkContext; +using ShrinkNetwork; + +namespace ShrinkCommand.Integration +{ + /// + /// 命令-网络集成组件(论文 6.5 集成组件模式): + /// 核心(command / network)互不依赖,本组件注入双方服务键,全部就绪才激活, + /// 任一提供者退役即自动停用——依赖接线由响应式余效应结构性完成,替代手工桥接编排。 + /// + /// 已知边界:ShrinkNetworkService 当前无 handler 注销 API,停用不撤销已注册的 + /// command/execute 处理器(传输解绑后不可达;网络服务实例重建时自然消失)。 + /// + public sealed class ShrinkCommandNetworkIntegrationComponent : IShrinkComponent + { + /// 与 ShrinkNetworkAppComponent.ServiceKey 保持一致(避免跨包硬引用)。 + public const string NetworkServiceKey = "shrink.service.network"; + + public const string CommandServiceKey = "shrink.service.command"; + public const string ProvideKey = "shrink.integration.command-network"; + + private static readonly string[] InjectKeys = { CommandServiceKey, NetworkServiceKey }; + private static readonly string[] ProvideKeys = { ProvideKey }; + + public string Name => "shrink.integration.command-network"; + + public IReadOnlyList Inject => InjectKeys; + + public IReadOnlyList Provide => ProvideKeys; + + public UniTask ApplyAsync(ShrinkCtx ctx, object? config) + { + var commandService = ctx.Get(CommandServiceKey); + var networkService = ctx.Get(NetworkServiceKey); + var options = config as ShrinkNetworkCommandBridgeOptions; + + ShrinkNetworkCommandBridge.RegisterService(networkService, commandService, options); + ctx.Set(ProvideKey, ShrinkNetworkCommandBridge.ExecuteRoute); + + // 桥注册按网络服务实例幂等去重;可逆面为集成键的发布/撤回 + return UniTask.CompletedTask; + } + } +} diff --git a/ShrinkCommandNetworkIntegrationComponent.cs.meta b/ShrinkCommandNetworkIntegrationComponent.cs.meta new file mode 100644 index 0000000..487675e --- /dev/null +++ b/ShrinkCommandNetworkIntegrationComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c5710d88a762cde489282366255f755c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkCommandBridge.cs b/ShrinkNetworkCommandBridge.cs new file mode 100644 index 0000000..b2f6d31 --- /dev/null +++ b/ShrinkNetworkCommandBridge.cs @@ -0,0 +1,86 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using ShrinkNetwork; + +namespace ShrinkCommand.Integration +{ + public static class ShrinkNetworkCommandBridge + { + public const string ExecuteRoute = "command/execute"; + + private static readonly object SyncRoot = new(); + private static readonly HashSet RegisteredServices = new(); + + public static void RegisterService( + ShrinkNetworkService networkService, + ShrinkCommandService commandService, + ShrinkNetworkCommandBridgeOptions? options = null) + { + if (networkService == null) + throw new ArgumentNullException(nameof(networkService)); + if (commandService == null) + throw new ArgumentNullException(nameof(commandService)); + + lock (SyncRoot) + { + if (RegisteredServices.Contains(networkService)) + return; + RegisteredServices.Add(networkService); + } + + options ??= new ShrinkNetworkCommandBridgeOptions(); + + EnsureMessagesRegistered(networkService); + var requirement = new ShrinkNetworkPermissionRequirement(options.Authority, options.Permission); + networkService.RegisterRequestHandler( + (context, request) => HandleExecuteAsync(commandService, options, context, request), + requirement); + } + + public static void EnsureMessagesRegistered(ShrinkNetworkService service) + { + if (!service.MessageRegistry.TryGetMeta(typeof(ShrinkNetworkCommandRequest), out _)) + { + var requestAttribute = (ShrinkNetworkMessageAttribute)Attribute.GetCustomAttribute( + typeof(ShrinkNetworkCommandRequest), + typeof(ShrinkNetworkMessageAttribute), + false)!; + service.RegisterMessage(typeof(ShrinkNetworkCommandRequest), requestAttribute.Opcode, requestAttribute.Route); + } + + if (!service.MessageRegistry.TryGetMeta(typeof(ShrinkNetworkCommandResponse), out _)) + { + var responseAttribute = (ShrinkNetworkMessageAttribute)Attribute.GetCustomAttribute( + typeof(ShrinkNetworkCommandResponse), + typeof(ShrinkNetworkMessageAttribute), + false)!; + service.RegisterMessage(typeof(ShrinkNetworkCommandResponse), responseAttribute.Opcode, responseAttribute.Route); + } + } + + private static async UniTask HandleExecuteAsync( + ShrinkCommandService commandService, + ShrinkNetworkCommandBridgeOptions options, + ShrinkNetworkContext context, + ShrinkNetworkCommandRequest request) + { + var source = options.SourceFactory?.Invoke(context, request) ?? new ShrinkNetworkCommandSource(context, request); + var result = await commandService.ExecuteAsync(source, request.CommandLine); + var output = source is ShrinkNetworkCommandSource networkSource + ? networkSource.BuildOutputMessage(result.Message) + : result.Message; + + return new ShrinkNetworkCommandResponse + { + ServiceName = string.IsNullOrWhiteSpace(request.ServiceName) ? options.ServiceName : request.ServiceName.Trim(), + CommandLine = request.CommandLine ?? string.Empty, + OutputMessage = output, + ErrorCode = result.IsSuccess ? 0 : ShrinkRpcErrorCode.HandlerException, + ErrorMessage = result.IsSuccess ? string.Empty : output + }; + } + } +} diff --git a/ShrinkNetworkCommandBridge.cs.meta b/ShrinkNetworkCommandBridge.cs.meta new file mode 100644 index 0000000..4a9c4f0 --- /dev/null +++ b/ShrinkNetworkCommandBridge.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f22899cbeb3779d4eb1338217fa4d38f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkCommandBridgeExtensions.cs b/ShrinkNetworkCommandBridgeExtensions.cs new file mode 100644 index 0000000..72e5cad --- /dev/null +++ b/ShrinkNetworkCommandBridgeExtensions.cs @@ -0,0 +1,52 @@ +#nullable enable + +using System; +using Cysharp.Threading.Tasks; +using ShrinkNetwork; + +namespace ShrinkCommand.Integration +{ + public static class ShrinkNetworkCommandBridgeExtensions + { + public static ShrinkNetworkService UseCommandBridge( + this ShrinkNetworkService networkService, + ShrinkCommandService commandService, + ShrinkNetworkCommandBridgeOptions? options = null) + { + if (networkService == null) + throw new ArgumentNullException(nameof(networkService)); + if (commandService == null) + throw new ArgumentNullException(nameof(commandService)); + + ShrinkNetworkCommandBridge.RegisterService(networkService, commandService, options); + return networkService; + } + + public static async UniTask ExecuteCommandAsync( + this ShrinkNetworkSession session, + string commandLine, + string? serviceName = null, + string? clientLabel = null, + ShrinkRpcCallOptions? options = null) + { + if (session == null) + throw new ArgumentNullException(nameof(session)); + + ShrinkNetworkCommandBridge.EnsureMessagesRegistered(session.Service); + return await session.RpcAsync( + new ShrinkNetworkCommandRequest + { + ServiceName = string.IsNullOrWhiteSpace(serviceName) + ? ShrinkCommandConstants.DefaultServiceName + : serviceName.Trim(), + CommandLine = commandLine ?? string.Empty, + ClientLabel = clientLabel ?? string.Empty + }, + options ?? new ShrinkRpcCallOptions + { + TimeoutMs = 5000, + DebugLabel = "RemoteCommand" + }); + } + } +} diff --git a/ShrinkNetworkCommandBridgeExtensions.cs.meta b/ShrinkNetworkCommandBridgeExtensions.cs.meta new file mode 100644 index 0000000..b8592cf --- /dev/null +++ b/ShrinkNetworkCommandBridgeExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 11bf32dbf7be0e14cbddf1f03ded5cbb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkCommandBridgeOptions.cs b/ShrinkNetworkCommandBridgeOptions.cs new file mode 100644 index 0000000..1235a08 --- /dev/null +++ b/ShrinkNetworkCommandBridgeOptions.cs @@ -0,0 +1,15 @@ +#nullable enable + +using System; +using ShrinkNetwork; + +namespace ShrinkCommand.Integration +{ + public sealed class ShrinkNetworkCommandBridgeOptions + { + public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName; + public ShrinkNetworkAuthority Authority { get; set; } = ShrinkNetworkAuthority.ClientOnly; + public string Permission { get; set; } = string.Empty; + public Func? SourceFactory { get; set; } + } +} diff --git a/ShrinkNetworkCommandBridgeOptions.cs.meta b/ShrinkNetworkCommandBridgeOptions.cs.meta new file mode 100644 index 0000000..8fba3e9 --- /dev/null +++ b/ShrinkNetworkCommandBridgeOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 174745a3cab12534c8d4b09788063fe9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkCommandContracts.cs b/ShrinkNetworkCommandContracts.cs new file mode 100644 index 0000000..c398275 --- /dev/null +++ b/ShrinkNetworkCommandContracts.cs @@ -0,0 +1,20 @@ +using ShrinkNetwork; + +namespace ShrinkCommand.Integration +{ + [ShrinkNetworkMessage(1501, ShrinkNetworkCommandBridge.ExecuteRoute)] + public sealed class ShrinkNetworkCommandRequest : IShrinkNetworkRequest + { + public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName; + public string CommandLine { get; set; } = string.Empty; + public string ClientLabel { get; set; } = string.Empty; + } + + [ShrinkNetworkMessage(1502, "command/execute_response")] + public sealed class ShrinkNetworkCommandResponse : ShrinkRpcResponseBase + { + public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName; + public string CommandLine { get; set; } = string.Empty; + public string OutputMessage { get; set; } = string.Empty; + } +} diff --git a/ShrinkNetworkCommandContracts.cs.meta b/ShrinkNetworkCommandContracts.cs.meta new file mode 100644 index 0000000..7100270 --- /dev/null +++ b/ShrinkNetworkCommandContracts.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8b44977ad838cce438d99ea565848485 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ShrinkNetworkCommandSource.cs b/ShrinkNetworkCommandSource.cs new file mode 100644 index 0000000..5ba1a2d --- /dev/null +++ b/ShrinkNetworkCommandSource.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using Cysharp.Threading.Tasks; +using ShrinkNetwork; + +namespace ShrinkCommand.Integration +{ + public sealed class ShrinkNetworkCommandSource : IShrinkCommandSource + { + private readonly List _lines = new(); + private readonly ShrinkNetworkContext _context; + private readonly ShrinkNetworkCommandRequest _request; + + public ShrinkNetworkCommandSource(ShrinkNetworkContext context, ShrinkNetworkCommandRequest request) + { + _context = context; + _request = request; + SourceId = $"network:{context.Session.SessionId}"; + DisplayName = ResolveDisplayName(context, request); + } + + public string SourceId { get; } + public string DisplayName { get; } + public bool IsConsole => false; + public ShrinkNetworkSession Session => _context.Session; + + public bool HasPermission(string permission) + { + return string.IsNullOrWhiteSpace(permission) || Session.HasPermission(permission); + } + + public UniTask WriteLineAsync(string message, CancellationToken cancellationToken = default) + { + if (!string.IsNullOrWhiteSpace(message)) + _lines.Add(message); + return UniTask.CompletedTask; + } + + public string BuildOutputMessage(string resultMessage) + { + if (_lines.Count == 0) + return resultMessage ?? string.Empty; + + var builder = new StringBuilder(); + for (var index = 0; index < _lines.Count; index++) + { + if (index > 0) + builder.AppendLine(); + builder.Append(_lines[index]); + } + + if (!string.IsNullOrWhiteSpace(resultMessage)) + { + if (builder.Length > 0 && !string.Equals(_lines[_lines.Count - 1], resultMessage, StringComparison.Ordinal)) + builder.AppendLine(); + if (!string.Equals(_lines[_lines.Count - 1], resultMessage, StringComparison.Ordinal)) + builder.Append(resultMessage); + } + + return builder.ToString(); + } + + private static string ResolveDisplayName(ShrinkNetworkContext context, ShrinkNetworkCommandRequest request) + { + if (context.Session.Items.TryGetValue("auth.name", out var authName) && authName is string authNameText && + !string.IsNullOrWhiteSpace(authNameText)) + { + return authNameText.Trim(); + } + + if (!string.IsNullOrWhiteSpace(request.ClientLabel)) + return request.ClientLabel.Trim(); + + return context.Session.RemoteAddress; + } + } +} diff --git a/ShrinkNetworkCommandSource.cs.meta b/ShrinkNetworkCommandSource.cs.meta new file mode 100644 index 0000000..91b91b9 --- /dev/null +++ b/ShrinkNetworkCommandSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bf31a6f53f1bb0c4ca0af60b92dce81f +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..5ea746b --- /dev/null +++ b/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a9777f2e6be37f64e8ec2df9b9afef87 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/IntegrationComponentsTests.cs b/Tests/IntegrationComponentsTests.cs new file mode 100644 index 0000000..df1664e --- /dev/null +++ b/Tests/IntegrationComponentsTests.cs @@ -0,0 +1,151 @@ +#nullable enable +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using NUnit.Framework; +using ShrinkCommand.Integration; +using ShrinkCommand.Integration.App; +using ShrinkContext; +using ShrinkDataSaver.Integration.App; +using ShrinkNetwork; +using ShrinkNetwork.Integration.App; + +namespace ShrinkCommand.Integration.Network.Tests +{ + /// + /// 阶段 3 集成组件模式(论文 6.5): + /// 核心(command/network/datasaver)互不依赖,集成组件注入双方服务键; + /// 全部提供者就绪才激活,任一退役即停用——依赖接线由响应式余效应完成。 + /// + public class IntegrationComponentsTests + { + private ShrinkContextRuntime _runtime = null!; + + [SetUp] + public void SetUp() + { + _runtime = new ShrinkContextRuntime(); + } + + [TearDown] + public void TearDown() + { + _runtime.ShutdownAsync().GetAwaiter().GetResult(); + } + + private static void Run(UniTask task) => task.GetAwaiter().GetResult(); + + [Test] + public void Integration_ActivatesOnlyWhenBothProvidersActive() + { + var integration = _runtime.Use(new ShrinkCommandNetworkIntegrationComponent()); + Assert.AreEqual(ShrinkFiberState.Inactive, integration.State, "两个服务键都缺失时等待"); + + var command = _runtime.Use(new ShrinkCommandAppComponent()); + Assert.AreEqual(ShrinkFiberState.Active, command.State); + Assert.AreEqual(ShrinkFiberState.Inactive, integration.State, "仅命令服务就绪仍等待网络服务"); + + var network = _runtime.Use(new ShrinkNetworkAppComponent()); + Assert.AreEqual(ShrinkFiberState.Active, network.State); + Assert.AreEqual(ShrinkFiberState.Active, integration.State, + "双键齐备后集成组件激活并完成桥注册"); + } + + [Test] + public void EitherProviderRetire_DeactivatesIntegration_AndReactivatesOnReturn() + { + var command = _runtime.Use(new ShrinkCommandAppComponent()); + var network = _runtime.Use(new ShrinkNetworkAppComponent()); + var integration = _runtime.Use(new ShrinkCommandNetworkIntegrationComponent()); + Assert.AreEqual(ShrinkFiberState.Active, integration.State); + + Run(_runtime.RetireAsync(network)); + Assert.AreEqual(ShrinkFiberState.Inactive, integration.State, + "网络提供者退役,集成组件自动停用(服务键撤回触发)"); + Assert.AreEqual(ShrinkFiberState.Active, command.State, "命令核心不受影响"); + + var networkAgain = _runtime.Use(new ShrinkNetworkAppComponent()); + Assert.AreEqual(ShrinkFiberState.Active, integration.State, + "提供者回归后集成组件自动重新激活"); + } + + [Test] + public void CommandComponent_ProvidesDefaultServiceKey() + { + var fiber = _runtime.Use(new ShrinkCommandAppComponent()); + Assert.AreEqual(ShrinkFiberState.Active, fiber.State); + Assert.IsTrue(_runtime.TryGetRaw(_runtime.RootContext, + ShrinkCommandAppComponent.ServiceKey, out var service)); + Assert.AreSame(ShrinkCommandRuntime.Default, service); + + Run(_runtime.RetireAsync(fiber)); + Assert.IsFalse(_runtime.TryGetRaw(_runtime.RootContext, + ShrinkCommandAppComponent.ServiceKey, out _), "退役撤回服务键"); + } + + [Test] + public void DataSaverComponent_Activates_FlushesOnDeactivate_WithdrawsKey() + { + var fiber = _runtime.Use(new ShrinkDataSaverAppComponent()); + Assert.AreEqual(ShrinkFiberState.Active, fiber.State); + Assert.IsTrue(_runtime.TryGetRaw(_runtime.RootContext, + ShrinkDataSaverAppComponent.ServiceKey, out _)); + + Assert.DoesNotThrow(() => Run(_runtime.RetireAsync(fiber)), + "停用执行补偿式收尾(设置最终落盘;未初始化环境安全跳过)"); + Assert.IsFalse(_runtime.TryGetRaw(_runtime.RootContext, + ShrinkDataSaverAppComponent.ServiceKey, out _)); + } + + [Test] + public void DataSaverIntercept_ReadOnlyViewAllowsReadsAndDeniesWriterCapability() + { + var provider = _runtime.Use(new ShrinkDataSaverAppComponent()); + Assert.AreEqual(ShrinkFiberState.Active, provider.State); + + var intercept = new Dictionary> + { + [ShrinkDataSaverAppComponent.ServiceKey] = new Dictionary + { + [ShrinkDataSaverAccess.MetadataKey] = ShrinkDataSaverAccess.ReadOnly + } + }; + var consumerComponent = new DataSaverAccessConsumer(); + var consumer = _runtime.Use(consumerComponent, intercept: intercept); + + Assert.AreEqual(ShrinkFiberState.Active, consumer.State); + var reader = consumerComponent.GetReader(); + Assert.IsNotNull(reader); + Assert.IsFalse(reader is IShrinkDataSaverWriter, + "只读上下文取得的对象不应再暴露写接口"); + Assert.Throws(() => consumerComponent.GetWriter()); + + Assert.IsTrue(_runtime.TryGetRaw(_runtime.RootContext, + ShrinkDataSaverAppComponent.ServiceKey, out _), + "编排层 raw read 保留原始服务;intercept 只介导组件访问"); + } + + private sealed class DataSaverAccessConsumer : IShrinkComponent + { + private ShrinkCtx? _ctx; + + public string Name => "test.datasaver.read-only-consumer"; + public IReadOnlyList Inject { get; } = new[] { ShrinkDataSaverAppComponent.ServiceKey }; + public IReadOnlyList Provide => Array.Empty(); + + public UniTask ApplyAsync(ShrinkCtx ctx, object? config) + { + _ctx = ctx; + return UniTask.CompletedTask; + } + + public IShrinkDataSaverReader GetReader() => Context.Get( + ShrinkDataSaverAppComponent.ServiceKey); + + public IShrinkDataSaverWriter GetWriter() => Context.Get( + ShrinkDataSaverAppComponent.ServiceKey); + + private ShrinkCtx Context => _ctx ?? throw new InvalidOperationException("Consumer is not active."); + } + } +} diff --git a/Tests/IntegrationComponentsTests.cs.meta b/Tests/IntegrationComponentsTests.cs.meta new file mode 100644 index 0000000..a08526a --- /dev/null +++ b/Tests/IntegrationComponentsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5c9445def3a931642955715272969a72 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/ShrinkCommand.Integration.Network.Tests.asmdef b/Tests/ShrinkCommand.Integration.Network.Tests.asmdef new file mode 100644 index 0000000..d4ca15b --- /dev/null +++ b/Tests/ShrinkCommand.Integration.Network.Tests.asmdef @@ -0,0 +1,28 @@ +{ + "name": "ShrinkCommand.Integration.Network.Tests", + "rootNamespace": "ShrinkCommand.Integration.Network.Tests", + "references": [ + "ShrinkCommand.Integration.Network", + "ShrinkCommand.Integration.App", + "ShrinkDataSaver.Integration.App", + "ShrinkNetwork.Integration.App", + "ShrinkCommand.Runtime", + "ShrinkNetwork.Runtime", + "ShrinkContext.Core.Runtime", + "UniTask", + "UnityEngine.TestRunner", + "UnityEditor.TestRunner" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Tests/ShrinkCommand.Integration.Network.Tests.asmdef.meta b/Tests/ShrinkCommand.Integration.Network.Tests.asmdef.meta new file mode 100644 index 0000000..3e0e6bd --- /dev/null +++ b/Tests/ShrinkCommand.Integration.Network.Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: dd3e105f80569534b9ba2d6fa913a2cc +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..7d709a8 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "com.cneicy.shrink-command-integration-network", + "version": "0.1.0", + "displayName": "ShrinkCommand - Network Integration", + "description": "ShrinkCommand 与 ShrinkNetwork 的桥接层,允许远程会话通过 RPC 执行命令。", + "unity": "2022.3", + "dependencies": { + "com.cneicy.shrink-command": "0.2.0", + "com.cneicy.shrink-network": "0.2.0", + "com.cneicy.shrink-context-core": "0.1.0" + }, + "keywords": [ + "command", + "network", + "integration", + "rpc" + ], + "author": { + "name": "cneicy", + "url": "https://git.crash.work/ShrinkSDK" + }, + "documentationUrl": "https://git.crash.work/ShrinkSDK/ShrinkCommand.Integration.Network", + "changelogUrl": "https://git.crash.work/ShrinkSDK/ShrinkCommand.Integration.Network/src/branch/main/CHANGELOG.md" +} diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..22d2898 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 73eb22917e75fcf4b8f1c7df5490014e +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: