From 51f8a1d044dab7e6cac8a3fdafc0f3abc1aeabe4 Mon Sep 17 00:00:00 2001 From: cneicy Date: Fri, 6 Nov 2026 19:18:54 +0800 Subject: [PATCH] feat: integrate composable Godot runtime and EventBus debugger --- .gitea/workflows/publish-supplemental.yml | 6 + Assets/Modules/ShrinkApp.Core | 2 +- Assets/Modules/ShrinkApp.Starter.Basic | 2 +- Assets/Modules/ShrinkCommand | 2 +- Assets/Modules/ShrinkCommand.Integration.App | 2 +- .../ShrinkCommand.Integration.EventBus | 2 +- .../Modules/ShrinkCommand.Integration.Network | 2 +- Assets/Modules/ShrinkContext.AppAdapter | 2 +- Assets/Modules/ShrinkContext.Core | 2 +- Assets/Modules/ShrinkContext.EventBusAdapter | 2 +- Assets/Modules/ShrinkDataSaver | 2 +- .../Modules/ShrinkDataSaver.Integration.App | 2 +- .../ShrinkDataSaver.Integration.EventBus | 2 +- Assets/Modules/ShrinkEventBus | 2 +- Assets/Modules/ShrinkEventBus.Entities | 2 +- Assets/Modules/ShrinkInstaller | 2 +- Assets/Modules/ShrinkModFramework | 2 +- Assets/Modules/ShrinkNetwork | 2 +- Assets/Modules/ShrinkNetwork.Integration.App | 2 +- .../ShrinkNetwork.Integration.EventBus | 2 +- Assets/Modules/ShrinkRuntime.Abstractions | 2 +- Assets/Modules/ShrinkTutorial | 2 +- Assets/csc.rsp | 1 + Assets/csc.rsp.meta | 7 + DESIGN.md | 45 +- Godot/NuGet.Config | 2 - .../Packages/ShrinkSDK.EntJoy.Godot/README.md | 31 ++ .../ShrinkEntJoyHost.cs | 96 ++++ .../ShrinkSDK.EntJoy.Godot.csproj | 21 + .../ShrinkSDK.EventBus.Godot/README.md | 41 ++ .../ShrinkSDK.EventBus.Godot.csproj | 35 ++ .../addons/shrink_event_bus/README.md | 15 + .../ShrinkEventBusDebugger.cs | 194 +++++++++ .../ShrinkEventBusDebugger.cs.uid | 1 + .../editor/event_bus_debugger_panel.gd | 409 ++++++++++++++++++ .../editor/event_bus_debugger_panel.gd.uid | 1 + .../editor/event_bus_debugger_plugin.gd | 63 +++ .../editor/event_bus_debugger_plugin.gd.uid | 1 + .../addons/shrink_event_bus/plugin.cfg | 6 + .../addons/shrink_event_bus/plugin.gd | 18 + .../addons/shrink_event_bus/plugin.gd.uid | 1 + Godot/Packages/ShrinkSDK.Godot | 2 +- Godot/README.md | 55 ++- .../ShrinkSDK.Godot.Sample/SampleRoot.cs | 15 +- .../ShrinkSDK.Godot.Sample.csproj | 4 +- .../ShrinkSDK.Godot.Sample/WorldDemo.cs | 108 +++++ .../ShrinkSDK.Godot.Sample/WorldDemo.tscn | 6 + .../ShrinkSDK.Godot.Sample/WorldDemoWorld.cs | 62 +++ .../ShrinkSDK.Godot.Sample/project.godot | 2 +- Godot/ShrinkSDK.Godot.slnx | 2 + .../Tests/NuGetConsumer/NuGetConsumer.csproj | 2 +- README.md | 18 +- .../RuntimeTests/DeferredStorageTests.cs | 168 +++++++ Tools/Release/package-versions.json | 100 +++-- Tools/Release/update-shrinksdk-versions.mjs | 15 + 55 files changed, 1481 insertions(+), 114 deletions(-) create mode 100644 Assets/csc.rsp create mode 100644 Assets/csc.rsp.meta create mode 100644 Godot/Packages/ShrinkSDK.EntJoy.Godot/README.md create mode 100644 Godot/Packages/ShrinkSDK.EntJoy.Godot/ShrinkEntJoyHost.cs create mode 100644 Godot/Packages/ShrinkSDK.EntJoy.Godot/ShrinkSDK.EntJoy.Godot.csproj create mode 100644 Godot/Packages/ShrinkSDK.EventBus.Godot/README.md create mode 100644 Godot/Packages/ShrinkSDK.EventBus.Godot/ShrinkSDK.EventBus.Godot.csproj create mode 100644 Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/README.md create mode 100644 Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/ShrinkEventBusDebugger.cs create mode 100644 Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/ShrinkEventBusDebugger.cs.uid create mode 100644 Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_panel.gd create mode 100644 Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_panel.gd.uid create mode 100644 Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_plugin.gd create mode 100644 Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_plugin.gd.uid create mode 100644 Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/plugin.cfg create mode 100644 Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/plugin.gd create mode 100644 Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/plugin.gd.uid create mode 100644 Godot/Samples/ShrinkSDK.Godot.Sample/WorldDemo.cs create mode 100644 Godot/Samples/ShrinkSDK.Godot.Sample/WorldDemo.tscn create mode 100644 Godot/Samples/ShrinkSDK.Godot.Sample/WorldDemoWorld.cs create mode 100644 Tools/AgentSupport/RuntimeTests/DeferredStorageTests.cs diff --git a/.gitea/workflows/publish-supplemental.yml b/.gitea/workflows/publish-supplemental.yml index 3ab9fa9..5d398a1 100644 --- a/.gitea/workflows/publish-supplemental.yml +++ b/.gitea/workflows/publish-supplemental.yml @@ -46,6 +46,10 @@ jobs: if (!entry) throw new Error(`Missing pinned submodule: ${path}`); await fetchSource(repo, entry.sha, `release/${path}`); } + const godotPath = 'Godot/Packages/ShrinkSDK.Godot'; + const godotEntry = tree.tree.find(item => item.path === godotPath && item.type === 'commit'); + if (!godotEntry) throw new Error(`Missing pinned submodule: ${godotPath}`); + await fetchSource('ShrinkGodot', godotEntry.sha, `release/${godotPath}`); NODE - name: Install .NET 8 SDK @@ -98,6 +102,8 @@ jobs: Assets/ShrinkInspection/DotNet~/ShrinkSDK.Inspection.csproj Assets/Modules/ShrinkNetwork/Adapters~/MessagePack/ShrinkSDK.Network.MessagePack.csproj Tools/AgentSupport/Shrink.Inspect/Shrink.Inspect.csproj + Godot/Packages/ShrinkSDK.EntJoy.Godot/ShrinkSDK.EntJoy.Godot.csproj + Godot/Packages/ShrinkSDK.EventBus.Godot/ShrinkSDK.EventBus.Godot.csproj ) for project in "${projects[@]}"; do dotnet restore "$project" --configfile Assets/Modules/ShrinkNetwork/NuGet.Config diff --git a/Assets/Modules/ShrinkApp.Core b/Assets/Modules/ShrinkApp.Core index 5efc172..124de9c 160000 --- a/Assets/Modules/ShrinkApp.Core +++ b/Assets/Modules/ShrinkApp.Core @@ -1 +1 @@ -Subproject commit 5efc172bca69305259b4ea629d933613e0047798 +Subproject commit 124de9c732781bb4b7f12bbdb186a8d1b2a8f6e0 diff --git a/Assets/Modules/ShrinkApp.Starter.Basic b/Assets/Modules/ShrinkApp.Starter.Basic index fd3788e..0127fae 160000 --- a/Assets/Modules/ShrinkApp.Starter.Basic +++ b/Assets/Modules/ShrinkApp.Starter.Basic @@ -1 +1 @@ -Subproject commit fd3788e2cec064c618eb77af719c0008ca387c8b +Subproject commit 0127fae182cd274dfbc4792d1cf1c2cf5cb58406 diff --git a/Assets/Modules/ShrinkCommand b/Assets/Modules/ShrinkCommand index 174b9c6..47beba2 160000 --- a/Assets/Modules/ShrinkCommand +++ b/Assets/Modules/ShrinkCommand @@ -1 +1 @@ -Subproject commit 174b9c6976d448c535d29393d8ebead7b7a08b3a +Subproject commit 47beba2269b2e0380a74f40a5268b4ea71301af3 diff --git a/Assets/Modules/ShrinkCommand.Integration.App b/Assets/Modules/ShrinkCommand.Integration.App index 25f4cfb..da25a40 160000 --- a/Assets/Modules/ShrinkCommand.Integration.App +++ b/Assets/Modules/ShrinkCommand.Integration.App @@ -1 +1 @@ -Subproject commit 25f4cfb82e601c70b0cbce64deeb706a7df307f8 +Subproject commit da25a40015df2a6ddbe771c8fbb677a437ad89d6 diff --git a/Assets/Modules/ShrinkCommand.Integration.EventBus b/Assets/Modules/ShrinkCommand.Integration.EventBus index 82c0833..fc6600f 160000 --- a/Assets/Modules/ShrinkCommand.Integration.EventBus +++ b/Assets/Modules/ShrinkCommand.Integration.EventBus @@ -1 +1 @@ -Subproject commit 82c0833a1130ab0161631a6867d458597e24d619 +Subproject commit fc6600fe9effa1cc62965dfb9b0504f86a9750df diff --git a/Assets/Modules/ShrinkCommand.Integration.Network b/Assets/Modules/ShrinkCommand.Integration.Network index b1c42ea..efc0f43 160000 --- a/Assets/Modules/ShrinkCommand.Integration.Network +++ b/Assets/Modules/ShrinkCommand.Integration.Network @@ -1 +1 @@ -Subproject commit b1c42ea4ab37479b745fbeeb3d7f2a0ba752ca0a +Subproject commit efc0f43b17fdc310c3a7d20ed885c0943433e37b diff --git a/Assets/Modules/ShrinkContext.AppAdapter b/Assets/Modules/ShrinkContext.AppAdapter index 7a50712..80cd594 160000 --- a/Assets/Modules/ShrinkContext.AppAdapter +++ b/Assets/Modules/ShrinkContext.AppAdapter @@ -1 +1 @@ -Subproject commit 7a5071217d774666b9cf866e362435e422d1dfd4 +Subproject commit 80cd594ac594555f2599fc87bd0add0f597877bf diff --git a/Assets/Modules/ShrinkContext.Core b/Assets/Modules/ShrinkContext.Core index d3eca1f..1852749 160000 --- a/Assets/Modules/ShrinkContext.Core +++ b/Assets/Modules/ShrinkContext.Core @@ -1 +1 @@ -Subproject commit d3eca1f155bece2f4e1de2184579e655cf7b828e +Subproject commit 18527491e1b64ffb885c5faaa51d897d0a4ee2bc diff --git a/Assets/Modules/ShrinkContext.EventBusAdapter b/Assets/Modules/ShrinkContext.EventBusAdapter index 92f55f4..e911834 160000 --- a/Assets/Modules/ShrinkContext.EventBusAdapter +++ b/Assets/Modules/ShrinkContext.EventBusAdapter @@ -1 +1 @@ -Subproject commit 92f55f4bdf5de701489d5ee0033dd7497da2a783 +Subproject commit e91183486a617678735bff71bfefe7edd0823eba diff --git a/Assets/Modules/ShrinkDataSaver b/Assets/Modules/ShrinkDataSaver index cdb682f..67d6814 160000 --- a/Assets/Modules/ShrinkDataSaver +++ b/Assets/Modules/ShrinkDataSaver @@ -1 +1 @@ -Subproject commit cdb682f484fd1d8451780de7d4d77f78e260ae13 +Subproject commit 67d6814c4b5ed59e32ad0fcd9fe06d6678de3de4 diff --git a/Assets/Modules/ShrinkDataSaver.Integration.App b/Assets/Modules/ShrinkDataSaver.Integration.App index fea752a..c522594 160000 --- a/Assets/Modules/ShrinkDataSaver.Integration.App +++ b/Assets/Modules/ShrinkDataSaver.Integration.App @@ -1 +1 @@ -Subproject commit fea752a8ff5621c2119966cc2b8655fc38d8449e +Subproject commit c5225945203592eba4489bfe23168e78ab115ff8 diff --git a/Assets/Modules/ShrinkDataSaver.Integration.EventBus b/Assets/Modules/ShrinkDataSaver.Integration.EventBus index d6b1940..9a258e8 160000 --- a/Assets/Modules/ShrinkDataSaver.Integration.EventBus +++ b/Assets/Modules/ShrinkDataSaver.Integration.EventBus @@ -1 +1 @@ -Subproject commit d6b1940982de3f3a409c8fc0492871c1d10282fe +Subproject commit 9a258e802be2a3257f8735894d4c8fc09f8edc02 diff --git a/Assets/Modules/ShrinkEventBus b/Assets/Modules/ShrinkEventBus index a92c9d2..901ad18 160000 --- a/Assets/Modules/ShrinkEventBus +++ b/Assets/Modules/ShrinkEventBus @@ -1 +1 @@ -Subproject commit a92c9d2cea55577438a49906ed45754bfcff8c1b +Subproject commit 901ad18453dd20e27240cf003846645199e699fd diff --git a/Assets/Modules/ShrinkEventBus.Entities b/Assets/Modules/ShrinkEventBus.Entities index c19d536..e084fe4 160000 --- a/Assets/Modules/ShrinkEventBus.Entities +++ b/Assets/Modules/ShrinkEventBus.Entities @@ -1 +1 @@ -Subproject commit c19d5365826d25696fa63a7b5712a1335eee8b6c +Subproject commit e084fe4c88dd84b0d905a39905fc657d7230d2c3 diff --git a/Assets/Modules/ShrinkInstaller b/Assets/Modules/ShrinkInstaller index 2e03045..5370a69 160000 --- a/Assets/Modules/ShrinkInstaller +++ b/Assets/Modules/ShrinkInstaller @@ -1 +1 @@ -Subproject commit 2e03045423d40007760aa9d23e5002c5b1a465d2 +Subproject commit 5370a69e0e478fabe116bc38ea342a5d7e6cd646 diff --git a/Assets/Modules/ShrinkModFramework b/Assets/Modules/ShrinkModFramework index 91f0141..2ab8482 160000 --- a/Assets/Modules/ShrinkModFramework +++ b/Assets/Modules/ShrinkModFramework @@ -1 +1 @@ -Subproject commit 91f0141b865ea02fde063d055e54b892ab426e5e +Subproject commit 2ab84823593d2dc309809711d4f153d3ce7d005b diff --git a/Assets/Modules/ShrinkNetwork b/Assets/Modules/ShrinkNetwork index 93c2b85..47c9785 160000 --- a/Assets/Modules/ShrinkNetwork +++ b/Assets/Modules/ShrinkNetwork @@ -1 +1 @@ -Subproject commit 93c2b8520baac7d698d2e03c839246b5fd4608f3 +Subproject commit 47c978544c2d8d35dc38f405b079946d2475031d diff --git a/Assets/Modules/ShrinkNetwork.Integration.App b/Assets/Modules/ShrinkNetwork.Integration.App index 123bd17..cc27634 160000 --- a/Assets/Modules/ShrinkNetwork.Integration.App +++ b/Assets/Modules/ShrinkNetwork.Integration.App @@ -1 +1 @@ -Subproject commit 123bd170d91ff6f704bb5d38c23ff6bd4a5f7acd +Subproject commit cc2763413bae8da4a250b9557f59287642175dea diff --git a/Assets/Modules/ShrinkNetwork.Integration.EventBus b/Assets/Modules/ShrinkNetwork.Integration.EventBus index 2dae3e3..3b30587 160000 --- a/Assets/Modules/ShrinkNetwork.Integration.EventBus +++ b/Assets/Modules/ShrinkNetwork.Integration.EventBus @@ -1 +1 @@ -Subproject commit 2dae3e389026c93298688237d2988115f68b06ac +Subproject commit 3b30587ea97f2d2f047253b027b94958d6d9cc15 diff --git a/Assets/Modules/ShrinkRuntime.Abstractions b/Assets/Modules/ShrinkRuntime.Abstractions index 52b92d3..7de22c8 160000 --- a/Assets/Modules/ShrinkRuntime.Abstractions +++ b/Assets/Modules/ShrinkRuntime.Abstractions @@ -1 +1 @@ -Subproject commit 52b92d365793a1192a2005bd0155d24fcb4df6fa +Subproject commit 7de22c85fb478b6751eb7501d067603b29c5e2f8 diff --git a/Assets/Modules/ShrinkTutorial b/Assets/Modules/ShrinkTutorial index 51c941a..a87e4dc 160000 --- a/Assets/Modules/ShrinkTutorial +++ b/Assets/Modules/ShrinkTutorial @@ -1 +1 @@ -Subproject commit 51c941a0176046fac99ab2425862a3ea4fbcb876 +Subproject commit a87e4dca328659414789e1913dafc3e4eb2fd102 diff --git a/Assets/csc.rsp b/Assets/csc.rsp new file mode 100644 index 0000000..2666199 --- /dev/null +++ b/Assets/csc.rsp @@ -0,0 +1 @@ +-define:SHRINK_EVENTBUS_HAS_SHARED_CODEGEN diff --git a/Assets/csc.rsp.meta b/Assets/csc.rsp.meta new file mode 100644 index 0000000..f2e086e --- /dev/null +++ b/Assets/csc.rsp.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6b266fc005854c67a6e9e23814b5c92a +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/DESIGN.md b/DESIGN.md index ba73f26..fbcd04f 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -85,28 +85,31 @@ ShrinkSDK Workspace/ | 包 | 版本 | 职责 | |---|---:|---| -| `com.cneicy.shrink-eventbus` | 2.2.2 | 单一事件模型、多 Bus、生成特性订阅、可选 MonoBehaviour 生命周期织入、UniTask 调度与零 GC 热路径 | -| `com.cneicy.shrink-eventbus-entities` | 0.1.5 | ShrinkEventBus 的 ECS/Burst NativeQueue writer 与 playback 适配 | -| `com.cneicy.shrink-datasaver` | 2.4.0 | 多槽位存档、设置、迁移、加密、原子写入与备份 | -| `com.cneicy.shrink-command` | 0.4.2 | 路径式命令、权限与同步/异步执行 | -| `com.cneicy.shrink-network` | 0.4.2 | 消息、RPC、权限、诊断、TCP/KCP/Loopback 与服务器生成 | -| `com.cneicy.shrink-mod-framework` | 0.3.6 | 模组发现、依赖、可逆生命周期、命名空间内容覆盖、外部 DLL revision 与 Harmony lease | -| `com.cneicy.shrink-tutorial` | 0.2.0 | 数据驱动引导、遮罩、锚点、触发与持久化 | -| `com.cneicy.shrink-context-core` | 0.3.0 | 可逆效应、coeffect、fiber、声明式 loader 与诊断 | -| `com.cneicy.shrink-app-core` | 0.2.3 | App 设置、服务门面、ClassicHost 兼容面与宿主协议 | -| `com.cneicy.shrink-app-starter-basic` | 0.3.7 | 默认 Context 组合根、配置资产和示例入口 | -| `com.cneicy.shrink-context-app-adapter` | 0.2.4 | ContextLoader 宿主、Profile/JSON、诊断与基准 | -| `com.cneicy.shrink-context-eventbus-adapter` | 0.2.4 | EventBus 生成绑定的可逆 EffectAttach 包装 | -| `com.cneicy.shrink-datasaver-integration-eventbus` | 2.2.5 | DataSaver 事件桥 | -| `com.cneicy.shrink-datasaver-integration-app` | 0.2.5 | DataSaver App installer/原生 Context 组件 | -| `com.cneicy.shrink-command-integration-eventbus` | 0.2.5 | 命令请求与生命周期事件桥 | -| `com.cneicy.shrink-command-integration-network` | 0.2.5 | `command/execute` RPC 桥 | -| `com.cneicy.shrink-command-integration-app` | 0.2.5 | Command App installer/原生 Context 组件 | -| `com.cneicy.shrink-network-integration-eventbus` | 0.2.5 | 网络事件广播、裁决结果与 delta 去重 | -| `com.cneicy.shrink-network-integration-app` | 0.2.5 | Network App installer/原生 Context 组件 | +| `com.cneicy.shrink-eventbus` | 2.3.1 | 单一事件模型、多 Bus、生成特性订阅、可选 MonoBehaviour 生命周期织入、UniTask 调度、显式延后发布与零 GC 热路径 | +| `com.cneicy.shrink-eventbus-entities` | 0.1.6 | ShrinkEventBus 的 ECS/Burst NativeQueue writer 与 playback 适配 | +| `ShrinkSDK.Godot` | 0.2.1 | Godot 4.7 宿主、组合运行时与平台服务 | +| `ShrinkSDK.EntJoy.Godot` | 0.1.1 | Godot 节点生命周期内的 EntJoy World 与 JobSystem 适配 | +| `ShrinkSDK.EventBus.Godot` | 0.1.1 | Godot 编辑器事件总线订阅与分发调试窗口 | +| `com.cneicy.shrink-datasaver` | 2.5.0 | 多槽位存档、设置、迁移、加密、可注入存储与原子写入 | +| `com.cneicy.shrink-command` | 0.4.3 | 路径式命令、权限与同步/异步执行 | +| `com.cneicy.shrink-network` | 0.4.3 | 消息、RPC、权限、诊断、TCP/KCP/Loopback 与服务器生成 | +| `com.cneicy.shrink-mod-framework` | 0.3.7 | 模组发现、依赖、可逆生命周期、命名空间内容覆盖、外部 DLL revision 与 Harmony lease | +| `com.cneicy.shrink-tutorial` | 0.2.1 | 数据驱动引导、遮罩、锚点、触发与持久化 | +| `com.cneicy.shrink-context-core` | 0.3.1 | 可逆效应、coeffect、fiber、声明式 loader 与诊断 | +| `com.cneicy.shrink-app-core` | 0.2.4 | App 设置、服务门面、ClassicHost 兼容面与宿主协议 | +| `com.cneicy.shrink-app-starter-basic` | 0.4.1 | 默认 Context 组合根、配置资产和示例入口 | +| `com.cneicy.shrink-context-app-adapter` | 0.2.5 | ContextLoader 宿主、Profile/JSON、诊断与基准 | +| `com.cneicy.shrink-context-eventbus-adapter` | 0.2.5 | EventBus 生成绑定的可逆 EffectAttach 包装 | +| `com.cneicy.shrink-datasaver-integration-eventbus` | 2.2.6 | DataSaver 事件桥 | +| `com.cneicy.shrink-datasaver-integration-app` | 0.3.1 | DataSaver App installer/原生 Context 组件 | +| `com.cneicy.shrink-command-integration-eventbus` | 0.2.6 | 命令请求与生命周期事件桥 | +| `com.cneicy.shrink-command-integration-network` | 0.2.6 | `command/execute` RPC 桥 | +| `com.cneicy.shrink-command-integration-app` | 0.2.6 | Command App installer/原生 Context 组件 | +| `com.cneicy.shrink-network-integration-eventbus` | 0.2.6 | 网络事件广播、裁决结果与 delta 去重 | +| `com.cneicy.shrink-network-integration-app` | 0.2.6 | Network App installer/原生 Context 组件 | | `com.cneicy.shrink-shared-codegen` | 0.2.1 | App、Command、Network 共用的 Editor-only IL 后处理注册表生成器 | -| `com.cneicy.shrink-runtime-abstractions` | 0.1.0 | Unity 与其他 .NET 宿主共用的平台服务合同 | -| `com.cneicy.shrink-installer` | 0.2.13 | UI Toolkit 包管理器:显示真实安装状态,按 Context 分层提供固定版本单包与推荐组合 | +| `com.cneicy.shrink-runtime-abstractions` | 0.2.0 | Unity 与其他 .NET 宿主共用的平台服务合同 | +| `com.cneicy.shrink-installer` | 0.2.15 | UI Toolkit 包管理器:显示真实安装状态,按 Context 分层提供固定版本单包与推荐组合 | `ShrinkShared.CodeGen` 不反向引用业务 asmdef,只按程序集名与类型全名读取 Cecil 元数据,因此业务包可以依赖它而不形成包循环。 diff --git a/Godot/NuGet.Config b/Godot/NuGet.Config index cc8d1e3..43e134b 100644 --- a/Godot/NuGet.Config +++ b/Godot/NuGet.Config @@ -2,8 +2,6 @@ - - diff --git a/Godot/Packages/ShrinkSDK.EntJoy.Godot/README.md b/Godot/Packages/ShrinkSDK.EntJoy.Godot/README.md new file mode 100644 index 0000000..c4573c4 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EntJoy.Godot/README.md @@ -0,0 +1,31 @@ +# ShrinkSDK.EntJoy.Godot + +Godot 4.7 C# 的 EntJoy ECS/JobSystem 生命周期适配包。它复用 EntJoy 的 `World`、查询和 Job API,不创建第二套 ECS;`ShrinkEntJoyHost` 负责在 Godot 节点生命周期内初始化/关闭 JobSystem,并在帧边界交换事件缓冲和提交待处理 Job。 + +## 安装 + +```powershell +dotnet add package ShrinkSDK.Godot --version 0.2.1 +dotnet add package ShrinkSDK.EntJoy.Godot --version 0.1.1 +``` + +该包依赖 `EntJoy.ECS` 1.0.0。EntJoy 当前 NuGet 原生运行时以 Windows x64 为主要验证目标;没有可用原生 DLL 时由 EntJoy 自己回退到托管 JobSystem。 + +## 使用 + +```csharp +using EntJoy.ECS; +using ShrinkSDK.EntJoy.Godot; + +public partial class GameRoot : ShrinkEntJoyHost +{ + public override void _Ready() + { + base._Ready(); + EntityManager entities = EntJoyWorld!.EntityManager; + // 创建实体、构造 QueryBuilder 并调度 EntJoy Job。 + } +} +``` + +`EntJoyWorld` 在 `_Ready` 后可用,在 `_ExitTree` 前释放。多个 `ShrinkEntJoyHost` 会共享进程级 JobSystem,并在最后一个宿主退出时关闭。 diff --git a/Godot/Packages/ShrinkSDK.EntJoy.Godot/ShrinkEntJoyHost.cs b/Godot/Packages/ShrinkSDK.EntJoy.Godot/ShrinkEntJoyHost.cs new file mode 100644 index 0000000..073bb39 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EntJoy.Godot/ShrinkEntJoyHost.cs @@ -0,0 +1,96 @@ +#nullable enable + +using System; +using System.Threading; +using EntJoy.ECS; +using EntJoy.JobSystem; +using Godot; +using ShrinkSDK.Godot; + +namespace ShrinkSDK.EntJoy.Godot; + +/// Godot node that owns an EntJoy World and advances its frame boundary. +public partial class ShrinkEntJoyHost : ShrinkGodotHost +{ + private static readonly object SchedulerGate = new(); + private static int SchedulerUsers; + private bool _schedulerAcquired; + private bool _worldDisposed; + private ulong _lastFrame = ulong.MaxValue; + private SceneTree? _tree; + + [Export] public string EntJoyWorldName { get; set; } = "GodotWorld"; + + public World? EntJoyWorld { get; private set; } + + public override void _Ready() + { + base._Ready(); + AcquireScheduler(); + EntJoyWorld = new World(EntJoyWorldName); + World.DefaultWorld = EntJoyWorld; + _tree = GetTree(); + _tree.ProcessFrame += OnProcessFrame; + TreeExited += OnTreeExited; + } + + public override void _Process(double delta) + { + base._Process(delta); + Advance(); + } + + private void OnProcessFrame() => Advance(); + + private void Advance() + { + if (EntJoyWorld == null || _worldDisposed) return; + var frame = Engine.GetProcessFrames(); + if (frame == _lastFrame) return; + _lastFrame = frame; + EntJoyWorld.NextFrameEvents(); + NativeJobScheduler.EndFrame(); + EntJoyWorld.CurrentFrame++; + } + + public override void _ExitTree() + { + DisposeWorld(); + base._ExitTree(); + } + + private void OnTreeExited() + { + if (_tree != null) + _tree.ProcessFrame -= OnProcessFrame; + _tree = null; + TreeExited -= OnTreeExited; + DisposeWorld(); + } + + private void AcquireScheduler() + { + lock (SchedulerGate) + { + if (SchedulerUsers++ == 0) + NativeJobScheduler.Initialize(); + _schedulerAcquired = true; + } + } + + private void DisposeWorld() + { + if (_worldDisposed) return; + _worldDisposed = true; + EntJoyWorld?.Dispose(); + EntJoyWorld = null; + + if (!_schedulerAcquired) return; + lock (SchedulerGate) + { + _schedulerAcquired = false; + if (--SchedulerUsers == 0) + NativeJobScheduler.Shutdown(); + } + } +} diff --git a/Godot/Packages/ShrinkSDK.EntJoy.Godot/ShrinkSDK.EntJoy.Godot.csproj b/Godot/Packages/ShrinkSDK.EntJoy.Godot/ShrinkSDK.EntJoy.Godot.csproj new file mode 100644 index 0000000..0798e5b --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EntJoy.Godot/ShrinkSDK.EntJoy.Godot.csproj @@ -0,0 +1,21 @@ + + + net8.0 + false + true + ShrinkSDK.EntJoy.Godot + ShrinkSDK.EntJoy.Godot + ShrinkSDK.EntJoy.Godot + 0.1.1 + Godot lifecycle integration for EntJoy headless ECS and JobSystem. + README.md + + + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.EventBus.Godot/README.md b/Godot/Packages/ShrinkSDK.EventBus.Godot/README.md new file mode 100644 index 0000000..9e8f207 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EventBus.Godot/README.md @@ -0,0 +1,41 @@ +# ShrinkSDK.EventBus.Godot + +Godot 4.7 编辑器调试窗口,用于查看 ShrinkSDK.EventBus 的订阅快照和实时分发结果。 + +## 安装 + +```powershell +dotnet add package ShrinkSDK.EventBus.Godot --version 0.1.1 +``` + +取得包后,将: + +```text +contentFiles/any/any/addons/shrink_event_bus +``` + +复制到 Godot 项目的: + +```text +addons/shrink_event_bus +``` + +然后在 `Project > Project Settings > Plugins` 启用 **ShrinkEventBus**。包依赖 `ShrinkSDK.EventBus 2.3.1` 与 `GodotSharp 4.7.2`;在 Debug 构建中把 `ShrinkEventBusDebugger` 挂到常驻节点(例如主场景根节点)。 + +## 使用 + +运行项目后打开底部调试器的 `ShrinkEventBus` 标签页。点击“刷新快照”查看订阅,勾选“启用实时记录”采集后续分发。每个调试会话独立显示数据。 + +插件通过 `shrink_event_bus:state` 调试消息接收数据。每 250 ms 最多发送一次快照,保留最近 200 条,不保留事件对象。关闭记录会解除诊断订阅,停止游戏保留最后一份可查看的数据。Release 构建不包含采集实现。 + +诊断数据来自公开的 `EventBus.ObserveDiagnostics` 与 `ShrinkEventDiagnostic`,不依赖反射或内部事件。 + +## 可复用能力 + + +Capability: Godot 编辑器中查看事件总线订阅快照与实时分发日志,按事件或 Bus 搜索并筛选处理状态 +Aliases: 事件 事件总线 可视化 调试 窗口 订阅 EventBus debugger +Limits: 需要 Debug 构建和 Godot 调试连接;订阅快照仅列出静态 EventBus 管理的 Bus;诊断记录可包含独立 Host 的分发;无事件参数和发送者对象;最多保留 200 条记录 +Extension: editor/event_bus_debugger_panel.gd 的界面;ShrinkEventBusDebugger.cs 的诊断协议适配 +Evidence: [采集桥接](addons/shrink_event_bus/ShrinkEventBusDebugger.cs); [会话插件](addons/shrink_event_bus/editor/event_bus_debugger_plugin.gd); [窗口](addons/shrink_event_bus/editor/event_bus_debugger_panel.gd) + diff --git a/Godot/Packages/ShrinkSDK.EventBus.Godot/ShrinkSDK.EventBus.Godot.csproj b/Godot/Packages/ShrinkSDK.EventBus.Godot/ShrinkSDK.EventBus.Godot.csproj new file mode 100644 index 0000000..b4a3e99 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EventBus.Godot/ShrinkSDK.EventBus.Godot.csproj @@ -0,0 +1,35 @@ + + + net8.0 + false + ShrinkSDK.EventBus.Godot + ShrinkSDK.EventBus.Godot + ShrinkSDK.EventBus.Godot + 0.1.1 + Godot editor debugger for ShrinkSDK.EventBus. + README.md + false + false + $(NoWarn);NU5128 + + + + + + + + + + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/README.md b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/README.md new file mode 100644 index 0000000..b4956d7 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/README.md @@ -0,0 +1,15 @@ +# Godot EventBus 调试器 + +运行项目后打开底部调试器的 `ShrinkEventBus` 标签页。点击“刷新快照”查看订阅,勾选“启用实时记录”采集后续分发。每个调试会话独立显示数据。 + +插件 UI 通过 Godot 调试连接接收 `shrink_event_bus:state`;运行时节点在常驻 App 下加载。每 250 ms 最多发送一次记录快照,保留最近 200 条,不保留事件对象。关闭记录解除观察者,停止游戏保留最后一份可查看的数据。 + +依赖 `ShrinkSDK.EventBus 2.3.0`:公开 `CaptureNamedBuses`、`CaptureBindings` 用于快照,`ObserveDiagnostics` 与 `ShrinkEventDiagnostic` 用于分发结果。Release 构建不包含采集实现。 + + +Capability: Godot 编辑器中查看事件总线订阅快照与实时分发日志,按事件或 Bus 搜索并筛选处理状态 +Aliases: 事件 事件总线 可视化 调试 窗口 订阅 EventBus debugger +Limits: 需要 Debug 构建和 Godot 调试连接;只观察静态 EventBus 管理的 Bus;无事件参数和发送者对象;最多保留 200 条记录 +Extension: editor/event_bus_debugger_panel.gd 的界面;ShrinkEventBusDebugger.cs 的诊断协议适配 +Evidence: [采集桥接](ShrinkEventBusDebugger.cs); [会话插件](editor/event_bus_debugger_plugin.gd); [窗口](editor/event_bus_debugger_panel.gd) + diff --git a/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/ShrinkEventBusDebugger.cs b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/ShrinkEventBusDebugger.cs new file mode 100644 index 0000000..3dfce10 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/ShrinkEventBusDebugger.cs @@ -0,0 +1,194 @@ +#nullable enable +using Godot; +#if DEBUG +using System; +using System.Collections.Generic; +using System.Linq; +using ShrinkEventBus; +using GArray = Godot.Collections.Array; +using GDictionary = Godot.Collections.Dictionary; +#endif + +namespace ShrinkSDK.EventBus.Godot; + +public partial class ShrinkEventBusDebugger : Node +{ +#if DEBUG + private const string Prefix = "shrink_event_bus"; + private const int Capacity = 200; + private readonly object _gate = new(); + private readonly Queue _history = new(); + private IDisposable? _subscription; + private bool _recording; + private bool _registered; + private long _discarded; + private double _elapsed; + + public override void _Ready() + { + if (!EngineDebugger.IsActive()) + return; + ProcessMode = ProcessModeEnum.Always; + EngineDebugger.RegisterMessageCapture(Prefix, Callable.From(HandleMessage)); + _registered = true; + SendSnapshot(); + } + + public override void _Process(double delta) + { + if (!_registered) + return; + if (!EngineDebugger.IsActive()) + { + SetRecording(false); + return; + } + _elapsed += delta; + if (_elapsed < 0.25) + return; + _elapsed = 0; + if (_recording) + SendSnapshot(); + } + + public override void _ExitTree() + { + if (!_registered) + return; + SetRecording(false); + EngineDebugger.UnregisterMessageCapture(Prefix); + _registered = false; + } + + private bool HandleMessage(string message, GArray data) + { + switch (message) + { + case "set_debug_record": + if (data is [{ VariantType: Variant.Type.Bool } _]) + SetRecording(data[0].AsBool()); + break; + case "clear_history": + lock (_gate) + { + _history.Clear(); + _discarded = 0; + } + break; + case "request_snapshot": + break; + default: + return false; + } + SendSnapshot(); + return true; + } + + private void SetRecording(bool enabled) + { + if (_recording == enabled) + return; + lock (_gate) + _recording = enabled; + if (enabled) + { + _subscription = global::ShrinkEventBus.EventBus.ObserveDiagnostics(Capture); + } + else + { + _subscription?.Dispose(); + _subscription = null; + } + } + + private void Capture(ShrinkEventDiagnostic diagnostic) + { + // 回调可能发生在工作线程:只留托管元数据,不触碰 Godot 对象或保留事件实例。 + lock (_gate) + { + if (!_recording) + return; + var record = new TraceRecord( + diagnostic.TimestampUtc, + diagnostic.EventType, + diagnostic.BusKey.ToString(), + diagnostic.Result, + diagnostic.ThreadId, + diagnostic.ElapsedMilliseconds, + diagnostic.IsAsync); + if (_history.Count == Capacity) + { + _history.Dequeue(); + _discarded++; + } + _history.Enqueue(record); + } + } + + private void SendSnapshot() + { + var groups = new GArray(); + var bindingCount = 0; + var buses = global::ShrinkEventBus.EventBus.CaptureNamedBuses(); + foreach (var bus in buses) + { + foreach (var group in global::ShrinkEventBus.EventBus.CaptureBindings(bus).GroupBy(binding => binding.EventType)) + { + var handlers = new GArray(); + foreach (var binding in group) + { + handlers.Add(new GDictionary + { + ["declaring_type"] = binding.Handler.DeclaringType?.FullName ?? "", + ["method_name"] = binding.Handler.Name, + ["is_static"] = binding.Handler.IsStatic + }); + } + bindingCount += handlers.Count; + groups.Add(new GDictionary + { + ["event_name"] = $"[{bus.Key}] {group.Key.FullName}", + ["handlers"] = handlers + }); + } + } + + TraceRecord[] history; + long discarded; + lock (_gate) + { + history = [.. _history]; + discarded = _discarded; + } + var records = new GArray(); + foreach (var record in history) + { + records.Add(new GDictionary + { + ["event_name"] = record.EventType.FullName ?? record.EventType.Name, + ["timestamp_utc"] = record.Timestamp.ToString("O"), + ["bus"] = record.Bus, + ["handled"] = record.Result.Handled, + ["accepted"] = record.Result.Accepted, + ["is_canceled"] = record.Result.Canceled, + ["failure"] = record.Result.Failure.ToString(), + ["thread_id"] = record.ThreadId, + ["elapsed_ms"] = record.ElapsedMs, + ["is_async"] = record.IsAsync + }); + } + EngineDebugger.SendMessage(Prefix + ":state", new GArray { new GDictionary + { + ["debug_enabled"] = _recording, + ["bus_count"] = buses.Count, + ["binding_count"] = bindingCount, + ["discarded_count"] = discarded, + ["subscriber_groups"] = groups, + ["recent_logs"] = records + } }); + } + + private sealed record TraceRecord(DateTime Timestamp, Type EventType, string Bus, + ShrinkPostResult Result, int ThreadId, double ElapsedMs, bool IsAsync); +#endif +} diff --git a/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/ShrinkEventBusDebugger.cs.uid b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/ShrinkEventBusDebugger.cs.uid new file mode 100644 index 0000000..b0ee5a2 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/ShrinkEventBusDebugger.cs.uid @@ -0,0 +1 @@ +uid://dcvus446brme5 diff --git a/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_panel.gd b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_panel.gd new file mode 100644 index 0000000..25cd6cd --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_panel.gd @@ -0,0 +1,409 @@ +@tool +extends VBoxContainer + +signal request_snapshot +signal toggle_debug(enabled: bool) +signal clear_requested + +const MAX_VISIBLE_LOGS := 200 + +var _event_groups: Array = [] +var _live_logs: Array = [] + +var _status_label: Label +var _debug_toggle: CheckButton +var _search_input: LineEdit +var _listener_filter: OptionButton +var _collapse_check: CheckButton +var _subscriber_list: VBoxContainer +var _live_list: VBoxContainer +var _session_active := false + + +func _init() -> void: + name = "ShrinkEventBus" + size_flags_horizontal = SIZE_EXPAND_FILL + size_flags_vertical = SIZE_EXPAND_FILL + + +func _ready() -> void: + _build_ui() + set_session_active(_session_active) + + +func set_session_active(active: bool) -> void: + _session_active = active + if not is_instance_valid(_status_label): + return + _debug_toggle.disabled = not active + _debug_toggle.set_pressed_no_signal(false) + _status_label.text = "等待运行时连接,点击刷新快照。" if active else "运行项目后连接事件总线。" + + +func apply_snapshot(snapshot: Dictionary) -> void: + if not is_instance_valid(_status_label): + return + + _debug_toggle.set_pressed_no_signal(bool(snapshot.get("debug_enabled", false))) + _event_groups = _normalize_dictionary_array(snapshot.get("subscriber_groups", [])) + _live_logs = _normalize_dictionary_array(snapshot.get("recent_logs", [])) + if _live_logs.size() > MAX_VISIBLE_LOGS: + _live_logs = _live_logs.slice(_live_logs.size() - MAX_VISIBLE_LOGS, _live_logs.size()) + + _status_label.text = "Bus: %d | 订阅: %d | 日志: %d / 200 | 已淘汰: %d" % [ + int(snapshot.get("bus_count", 0)), + int(snapshot.get("binding_count", 0)), + _live_logs.size(), + int(snapshot.get("discarded_count", 0)) + ] + + _refresh_subscribers() + _refresh_live_logs() + + +func append_live_event(record: Dictionary) -> void: + _live_logs.append(record) + if _live_logs.size() > MAX_VISIBLE_LOGS: + _live_logs.pop_front() + _refresh_live_logs() + + +func _build_ui() -> void: + if _status_label != null: + return + + var toolbar := HBoxContainer.new() + toolbar.size_flags_horizontal = SIZE_EXPAND_FILL + add_child(toolbar) + + _debug_toggle = CheckButton.new() + _debug_toggle.text = "启用实时记录" + _debug_toggle.toggled.connect(_on_debug_toggled) + toolbar.add_child(_debug_toggle) + + var refresh_button := Button.new() + refresh_button.text = "刷新快照" + refresh_button.pressed.connect(func() -> void: request_snapshot.emit()) + toolbar.add_child(refresh_button) + + var clear_button := Button.new() + clear_button.text = "清空日志" + clear_button.pressed.connect(func() -> void: clear_requested.emit()) + toolbar.add_child(clear_button) + + var spacer := Control.new() + spacer.size_flags_horizontal = SIZE_EXPAND_FILL + toolbar.add_child(spacer) + + _status_label = Label.new() + _status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + toolbar.add_child(_status_label) + + var filter_row := HBoxContainer.new() + filter_row.size_flags_horizontal = SIZE_EXPAND_FILL + add_child(filter_row) + + _search_input = LineEdit.new() + _search_input.placeholder_text = "按事件名 / Bus / 方法名过滤" + _search_input.size_flags_horizontal = SIZE_EXPAND_FILL + _search_input.text_changed.connect(_on_filter_changed) + filter_row.add_child(_search_input) + + _listener_filter = OptionButton.new() + _listener_filter.add_item("全部事件", 0) + _listener_filter.add_item("已处理 / 有订阅", 1) + _listener_filter.add_item("未处理 / 无订阅", 2) + _listener_filter.item_selected.connect(_on_filter_changed) + filter_row.add_child(_listener_filter) + + _collapse_check = CheckButton.new() + _collapse_check.text = "同类事件折叠" + _collapse_check.button_pressed = true + _collapse_check.toggled.connect(_on_filter_changed) + filter_row.add_child(_collapse_check) + + var tab_container := TabContainer.new() + tab_container.size_flags_horizontal = SIZE_EXPAND_FILL + tab_container.size_flags_vertical = SIZE_EXPAND_FILL + add_child(tab_container) + + var subscriber_scroll := ScrollContainer.new() + subscriber_scroll.name = "订阅者全览" + subscriber_scroll.size_flags_horizontal = SIZE_EXPAND_FILL + subscriber_scroll.size_flags_vertical = SIZE_EXPAND_FILL + tab_container.add_child(subscriber_scroll) + + _subscriber_list = VBoxContainer.new() + _subscriber_list.size_flags_horizontal = SIZE_EXPAND_FILL + _subscriber_list.size_flags_vertical = SIZE_EXPAND_FILL + subscriber_scroll.add_child(_subscriber_list) + + var live_scroll := ScrollContainer.new() + live_scroll.name = "实时触发日志" + live_scroll.size_flags_horizontal = SIZE_EXPAND_FILL + live_scroll.size_flags_vertical = SIZE_EXPAND_FILL + tab_container.add_child(live_scroll) + + _live_list = VBoxContainer.new() + _live_list.size_flags_horizontal = SIZE_EXPAND_FILL + _live_list.size_flags_vertical = SIZE_EXPAND_FILL + live_scroll.add_child(_live_list) + + +func _refresh_subscribers() -> void: + for child in _subscriber_list.get_children(): + _subscriber_list.remove_child(child) + child.queue_free() + + var groups := _event_groups.duplicate(true) + groups.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: + return String(a.get("event_name", "")) < String(b.get("event_name", "")) + ) + + for group in groups: + var event_name := String(group.get("event_name", "")) + var handlers := _normalize_dictionary_array(group.get("handlers", [])) + if not _passes_text_filter(event_name, handlers): + continue + if not _passes_listener_filter(handlers.size()): + continue + + var foldout := EventBusFoldout.new() + foldout.title = "%s (%d)" % [event_name, handlers.size()] + foldout.collapsed = false + _subscriber_list.add_child(foldout) + + if handlers.is_empty(): + foldout.add_content(_make_label("当前没有订阅者。")) + continue + + for handler in handlers: + foldout.add_content(_make_handler_row(handler)) + + if _subscriber_list.get_child_count() == 0: + _subscriber_list.add_child(_make_label("暂无匹配订阅信息。")) + + +func _refresh_live_logs() -> void: + for child in _live_list.get_children(): + _live_list.remove_child(child) + child.queue_free() + + var filtered := _get_filtered_logs() + if filtered.is_empty(): + _live_list.add_child(_make_label("暂无匹配日志。")) + return + + if _collapse_check.button_pressed: + for group in _collapse_records(filtered): + _live_list.add_child(_make_live_group(group)) + return + + for record in filtered: + _live_list.add_child(_make_live_record(record)) + + +func _get_filtered_logs() -> Array: + var records: Array = [] + for record in _live_logs: + if record is not Dictionary: + continue + if not _passes_listener_filter(1 if bool(record.get("handled", false)) else 0): + continue + if not _passes_text_filter( + String(record.get("event_name", "")) + " " + String(record.get("bus", "")), + _normalize_dictionary_array(record.get("handlers", [])), + _normalize_string_array(record.get("payload_lines", [])) + ): + continue + records.append(record) + return records + + +func _passes_text_filter(base_text: String, handlers: Array, payload_lines: Array = []) -> bool: + var keyword := _search_input.text.strip_edges().to_lower() + if keyword == "": + return true + + var haystack := base_text.to_lower() + for payload in payload_lines: + haystack += " " + String(payload).to_lower() + for handler in handlers: + if handler is Dictionary: + haystack += " " + String(handler.get("declaring_type", "")).to_lower() + haystack += " " + String(handler.get("method_name", "")).to_lower() + return haystack.find(keyword) != -1 + + +func _passes_listener_filter(listener_count: int) -> bool: + match _listener_filter.selected: + 1: + return listener_count > 0 + 2: + return listener_count == 0 + _: + return true + + +func _collapse_records(records: Array) -> Array: + var groups: Array = [] + var current_event_name := "" + var current_records: Array = [] + + for record in records: + var event_name := "[%s] %s" % [String(record.get("bus", "")), String(record.get("event_name", ""))] + if current_records.is_empty() or event_name == current_event_name: + current_event_name = event_name + current_records.append(record) + continue + + groups.append({ + "event_name": current_event_name, + "records": current_records.duplicate(true) + }) + current_event_name = event_name + current_records.clear() + current_records.append(record) + + if not current_records.is_empty(): + groups.append({ + "event_name": current_event_name, + "records": current_records.duplicate(true) + }) + + return groups + + +func _make_live_group(group: Dictionary) -> Control: + var records: Array = group.get("records", []) + var latest: Dictionary = records[-1] if not records.is_empty() else {} + var foldout := EventBusFoldout.new() + foldout.title = "%s x%d" % [String(group.get("event_name", "未知事件")), records.size()] + foldout.collapsed = false + foldout.add_content(_make_live_record(latest)) + + if records.size() > 1: + var details := EventBusFoldout.new() + details.title = "展开全部 %d 条" % records.size() + details.collapsed = true + for record in records: + details.add_content(_make_live_record(record)) + foldout.add_content(details) + + return foldout + + +func _make_live_record(record: Dictionary) -> Control: + var root := VBoxContainer.new() + root.add_theme_constant_override("separation", 4) + root.add_child(_make_label("[%s] %s %s UTC" % [ + String(record.get("bus", "")), String(record.get("event_name", "")), + _format_timestamp(String(record.get("timestamp_utc", ""))) + ])) + root.add_child(_make_label("%s | 接受: %s | 处理: %s | 取消: %s | %s | %.3f ms | 线程 %d" % [ + String(record.get("failure", "None")), + "是" if bool(record.get("accepted", false)) else "否", + "是" if bool(record.get("handled", false)) else "否", + "是" if bool(record.get("is_canceled", false)) else "否", + "异步" if bool(record.get("is_async", false)) else "同步", + float(record.get("elapsed_ms", 0.0)), int(record.get("thread_id", 0)) + ])) + return _wrap_card(root) + + +func _make_handler_row(handler: Dictionary) -> Control: + return _make_label("%s.%s (%s)" % [ + String(handler.get("declaring_type", "")), String(handler.get("method_name", "")), + "静态绑定方法" if bool(handler.get("is_static", false)) else "实例绑定方法" + ]) + +func _make_label(text: String) -> Label: + var label := Label.new() + label.text = text + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + return label + + +func _wrap_card(content: Control) -> PanelContainer: + var panel := PanelContainer.new() + panel.size_flags_horizontal = SIZE_EXPAND_FILL + panel.add_child(content) + return panel + + +func _normalize_dictionary_array(value) -> Array: + var result: Array = [] + if value is Array: + for item in value: + if item is Dictionary: + result.append(item) + return result + + +func _normalize_string_array(value) -> Array: + var result: Array = [] + if value is Array: + for item in value: + result.append(String(item)) + return result + + +func _format_timestamp(timestamp: String) -> String: + if timestamp == "": + return "--" + var parts := timestamp.split("T") + if parts.size() < 2: + return timestamp + return parts[1].trim_suffix("Z") + + +func _on_debug_toggled(enabled: bool) -> void: + toggle_debug.emit(enabled) + + +func _on_filter_changed(_value = null) -> void: + _refresh_subscribers() + _refresh_live_logs() + + +class EventBusFoldout: + extends VBoxContainer + + var title := "": + set(value): + title = value + if _button != null: + _button.text = value + + var collapsed := false: + set(value): + collapsed = value + if _button != null: + _button.button_pressed = not value + if _body != null: + _body.visible = not value + + var _button: CheckButton + var _body: VBoxContainer + + func _ready() -> void: + if _button != null: + return + + _button = CheckButton.new() + _button.text = title + _button.button_pressed = not collapsed + _button.toggled.connect(func(pressed: bool) -> void: + collapsed = not pressed + ) + add_child(_button) + + _body = VBoxContainer.new() + _body.visible = not collapsed + _body.size_flags_horizontal = SIZE_EXPAND_FILL + add_child(_body) + + func add_content(control: Control) -> void: + if _body == null: + _ready() + _body.add_child(control) diff --git a/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_panel.gd.uid b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_panel.gd.uid new file mode 100644 index 0000000..f0a35fa --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_panel.gd.uid @@ -0,0 +1 @@ +uid://c8jc4n74owcks diff --git a/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_plugin.gd b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_plugin.gd new file mode 100644 index 0000000..e041db4 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_plugin.gd @@ -0,0 +1,63 @@ +@tool +extends EditorDebuggerPlugin + +const PREFIX := "shrink_event_bus" +const PANEL_SCRIPT := preload("res://addons/shrink_event_bus/editor/event_bus_debugger_panel.gd") +var _panels: Dictionary = {} +var _connections: Array = [] + + +func _has_capture(prefix: String) -> bool: + return prefix == PREFIX + + +func _setup_session(session_id: int) -> void: + var session := get_session(session_id) + var panel = PANEL_SCRIPT.new() + _panels[session_id] = panel + panel.request_snapshot.connect(func(): + if session.is_active(): + session.send_message(PREFIX + ":request_snapshot", []) + ) + panel.toggle_debug.connect(func(enabled: bool): + if session.is_active(): + session.send_message(PREFIX + ":set_debug_record", [enabled]) + ) + panel.clear_requested.connect(func(): + if session.is_active(): + session.send_message(PREFIX + ":clear_history", []) + ) + var started := func(): + panel.set_session_active(true) + session.send_message(PREFIX + ":request_snapshot", []) + var stopped := func(): panel.set_session_active(false) + session.started.connect(started) + session.stopped.connect(stopped) + _connections.append([session, started, stopped]) + session.add_session_tab(panel) + panel.set_session_active(session.is_active()) + + +func dispose_panels() -> void: + for connection in _connections: + var session = connection[0] + if session.started.is_connected(connection[1]): + session.started.disconnect(connection[1]) + if session.stopped.is_connected(connection[2]): + session.stopped.disconnect(connection[2]) + _connections.clear() + for session_id in _panels: + var panel = _panels[session_id] + if is_instance_valid(panel): + get_session(session_id).remove_session_tab(panel) + panel.queue_free() + _panels.clear() + + +func _capture(message: String, data: Array, session_id: int) -> bool: + if message != PREFIX + ":state": + return false + var panel = _panels.get(session_id) + if is_instance_valid(panel) and not data.is_empty() and data[0] is Dictionary: + panel.apply_snapshot(data[0]) + return true diff --git a/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_plugin.gd.uid b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_plugin.gd.uid new file mode 100644 index 0000000..20d9675 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/editor/event_bus_debugger_plugin.gd.uid @@ -0,0 +1 @@ +uid://dlkjwusf3memk diff --git a/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/plugin.cfg b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/plugin.cfg new file mode 100644 index 0000000..e7c3b81 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/plugin.cfg @@ -0,0 +1,6 @@ +[plugin] +name="ShrinkEventBus" +description="ShrinkSDK.EventBus 2.3.1 的订阅快照与实时事件调试窗口。" +author="cneicy" +version="0.1.1" +script="plugin.gd" diff --git a/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/plugin.gd b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/plugin.gd new file mode 100644 index 0000000..9fac575 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/plugin.gd @@ -0,0 +1,18 @@ +@tool +extends EditorPlugin + +const DEBUGGER_PLUGIN_SCRIPT := preload("res://addons/shrink_event_bus/editor/event_bus_debugger_plugin.gd") + +var _debugger_plugin: EditorDebuggerPlugin + + +func _enter_tree() -> void: + _debugger_plugin = DEBUGGER_PLUGIN_SCRIPT.new() + add_debugger_plugin(_debugger_plugin) + + +func _exit_tree() -> void: + if _debugger_plugin != null: + _debugger_plugin.dispose_panels() + remove_debugger_plugin(_debugger_plugin) + _debugger_plugin = null diff --git a/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/plugin.gd.uid b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/plugin.gd.uid new file mode 100644 index 0000000..1eab076 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EventBus.Godot/addons/shrink_event_bus/plugin.gd.uid @@ -0,0 +1 @@ +uid://2r868eolxj3m diff --git a/Godot/Packages/ShrinkSDK.Godot b/Godot/Packages/ShrinkSDK.Godot index e355bbf..6c2cca5 160000 --- a/Godot/Packages/ShrinkSDK.Godot +++ b/Godot/Packages/ShrinkSDK.Godot @@ -1 +1 @@ -Subproject commit e355bbfd355048fc6bb9369f90ffaf2db662b18a +Subproject commit 6c2cca573562b236c74ba2b88e1e381a1d06afdf diff --git a/Godot/README.md b/Godot/README.md index efb3afe..0ef687b 100644 --- a/Godot/README.md +++ b/Godot/README.md @@ -1,10 +1,10 @@ -# ShrinkSDK for Godot 4.6 C# +# ShrinkSDK for Godot 4.7 C# -Godot 适配层使用共享 ShrinkSDK 运行时和 MSBuild/Cecil CodeGen。首期目标是 Godot 4.6.3 Mono、.NET 8,以及 Windows、Linux、macOS 桌面项目;不包含 Web、Android 和 iOS。 +Godot 适配层使用共享 ShrinkSDK 运行时和 MSBuild/Cecil CodeGen。桌面基线为 Godot 4.7.2 Mono / .NET 8。Android 消费工程使用 .NET 9,并由工程提供对应平台的运行与导出验证;Web 和 iOS 不在本次支持范围。 ## 直接运行 Workspace 示例 -使用 Godot 4.6.3 Mono 打开: +使用 Godot 4.7.2 Mono 打开: ```text Godot/Samples/ShrinkSDK.Godot.Sample/project.godot @@ -25,9 +25,9 @@ dotnet build .\Godot\Samples\ShrinkSDK.Godot.Sample\ShrinkSDK.Godot.Sample.cspro 项目需要: -- Godot 4.6.3 Mono; +- Godot 4.7.2 Mono; - .NET 8 SDK; -- `.csproj` 使用 `Godot.NET.Sdk/4.6.3`; +- `.csproj` 使用 `Godot.NET.Sdk/4.7.2`; - 项目级 `NuGet.Config` 保留既有源并加入 ShrinkSDK feed。 `NuGet.Config` 的最小增量如下: @@ -44,20 +44,39 @@ dotnet build .\Godot\Samples\ShrinkSDK.Godot.Sample\ShrinkSDK.Godot.Sample.cspro 安装 Godot 宿主和所需模块: ```powershell -dotnet add package ShrinkSDK.Godot --version 0.1.0 -dotnet add package ShrinkSDK.EventBus --version 2.1.0 -dotnet add package ShrinkSDK.Command --version 0.3.0 -dotnet add package ShrinkSDK.Network --version 0.3.0 -dotnet add package ShrinkSDK.DataSaver --version 2.3.0 +dotnet add package ShrinkSDK.Godot --version 0.2.1 +dotnet add package ShrinkSDK.EntJoy.Godot --version 0.1.1 +dotnet add package ShrinkSDK.EventBus --version 2.3.1 +dotnet add package ShrinkSDK.Command --version 0.4.3 +dotnet add package ShrinkSDK.Network --version 0.4.3 +dotnet add package ShrinkSDK.DataSaver --version 2.5.0 dotnet restore dotnet build ``` 只有实际使用的模块需要安装。版本以 ShrinkSDK NuGet 源中的已发布版本为准,并保留命令中的固定版本号,不要改用浮动版本。 +### EventBus 调试器(可选) + +`ShrinkSDK.EventBus.Godot 0.1.1` 提供 Godot 编辑器中的事件订阅与分发调试窗口。安装 NuGet 包后,将包内 `contentFiles/any/any/addons/shrink_event_bus` 复制到项目的 `addons/shrink_event_bus`,再在 `Project > Project Settings > Plugins` 启用 **ShrinkEventBus**。它依赖公开的 `EventBus.ObserveDiagnostics`,订阅快照列出静态 `EventBus` 管理的 Bus,分发记录也可观察独立 Host;Release 构建不包含采集实现。 + +主线程 Bus 可以使用显式延后发布: + +```csharp +var result = EventBus.PostDeferred(bus, new PlayerReadyEvent()); +if (!result.Accepted) + GD.PushWarning($"deferred post rejected: {result.Failure}"); +``` + +延后事件在下一次宿主主线程泵送时执行。`Inline`、`DedicatedThread`、`TaskPool` 或没有 `IShrinkDeferredMainThreadDispatcher` 的宿主会返回 `DeferredUnsupported`,不会静默改成同步分发。 + +### EntJoy ECS(可选) + +`ShrinkSDK.EntJoy.Godot` 将 EntJoy 的 headless ECS/JobSystem 接入 Godot 节点生命周期。主场景根节点改为继承 `ShrinkEntJoyHost` 后,可从 `EntJoyWorld` 使用 `EntityManager`、`QueryBuilder` 和 EntJoy Job API;宿主会在 `_Ready` 初始化共享 JobSystem,在每帧 `_Process` 交换事件缓冲并提交待处理 Job,在 `_ExitTree` 释放 World。详见 [Godot/Packages/ShrinkSDK.EntJoy.Godot/README.md](Packages/ShrinkSDK.EntJoy.Godot/README.md)。 + ## Installer 插件 -`ShrinkSDK.Godot.Installer` 的 nupkg 内包含完整的 `addons/shrinksdk`。取得 `0.1.3` 包后,将包内: +`ShrinkSDK.Godot.Installer` 的 nupkg 内包含完整的 `addons/shrinksdk`。取得 `0.2.0` 包后,将包内: ```text contentFiles/any/any/addons/shrinksdk @@ -110,7 +129,19 @@ public partial class GameRoot : ShrinkGodotHost } ``` -宿主负责日志、主线程派发、时间、`user://` 持久化路径、截图、应用生命周期和网络 dispatch queue。使用 `ShrinkNetworkService` 时,通过 `AddNetworkService(service)` 交给宿主逐帧泵送,释放前调用 `RemoveNetworkService(service)`。 +宿主负责日志、主线程派发、时间、`user://` 持久化路径、截图、应用生命周期和网络 dispatch queue。`Runtime` 属性暴露可组合的 `ShrinkGodotRuntime`,供不直接继承宿主节点的入口复用。使用 `ShrinkNetworkService` 时,通过 `AddNetworkService(service)` 交给宿主逐帧泵送,释放前调用 `RemoveNetworkService(service)`。 + +`ShutdownAppAsync()` 只停止 App,保留运行时继续泵送派发和网络队列;`ShutdownAsync()` 会先停止 App,再关闭整个运行时。退出流程需要等待 App 的 inverse effect 完成时使用后者。 + +## 组合式宿主 + +Godot 4.7.2 项目优先让常驻入口继承 `Node`,持有一个 `ShrinkGodotRuntime`:在 `_Ready` 中 `Initialize(this)`,在 `_Process(delta)` 中 `Tick(delta)`,在 `_Notification(what)` 中 `Notify(what)`。入口设置 `ProcessMode = ProcessModeEnum.Always`,暂停时继续处理基础服务。 + +启动前必须初始化;退出时先保存业务状态并关闭服务组合,再等待 `ShutdownAsync()`,最后调用 `SceneTree.Quit()`。正常退出由项目显式协调,强制终止不保证清理。网络注册与逐帧驱动在所有者线程调用,后台任务可以使用 `TryPost` / `TryPostDeferred`。 + +受限沙箱中的设置存储可使用 `ShrinkGodotStorage.CreateRenameOnly(rootPath)`,通过 `ShrinkDataSaverAppModuleConfig.RuntimeConfig.Storage` 注入,并调用 `ShrinkBasicComposition.CreateEntries(services, network: null, dataSaver: config)`。轮转中断从备份恢复,孤立临时文件不会自动成为正式文件。 + +运行 `WorldDemo.tscn` 可验证两个实际世界节点的候选恢复、失败保留原世界、提交替换与订阅释放,以及暂停派发和正常退出。成功标记为 `SHRINK_GODOT_WORLD_PASS`。 ## CodeGen 织入 diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs b/Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs index 453f4de..d4beca2 100644 --- a/Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs @@ -12,9 +12,10 @@ using ShrinkDataSaver; using ShrinkEventBus; using ShrinkNetwork; using ShrinkSDK.Godot; +using ShrinkSDK.EntJoy.Godot; using ShrinkTutorial; -public partial class SampleRoot : ShrinkGodotHost +public partial class SampleRoot : ShrinkEntJoyHost { public override void _Ready() { @@ -24,6 +25,10 @@ public partial class SampleRoot : ShrinkGodotHost private async UniTaskVoid RunSmokeAsync() { + if (EntJoyWorld == null) + throw new InvalidOperationException("EntJoy world was not created by ShrinkEntJoyHost."); + var smokeEntity = EntJoyWorld.EntityManager.NewEntity(typeof(GodotEntJoySmokeComponent)); + EntJoyWorld.EntityManager.Set(smokeEntity, new GodotEntJoySmokeComponent { Value = 42 }); var marker = RequireWoven(GetType().Assembly); var goldenPath = ProjectSettings.GlobalizePath("res://../../../Assets/AgentSupportTests/Runtime/Resources/ShrinkV2Golden.bytes"); var golden = System.IO.File.ReadAllBytes(goldenPath); @@ -60,7 +65,10 @@ public partial class SampleRoot : ShrinkGodotHost }); var tutorialCompleted = tutorial.CompleteStep(); + await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); + await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); if (GodotSubscriber.Calls != 1 || GodotStaticSubscriber.Calls != 1 || savedValue != 42 || + EntJoyWorld == null || EntJoyWorld.CurrentFrame < 1 || !app.IsRunning || !GodotInstaller.Initialized || activeFibers != 3 || !tutorialStarted || !tutorialCompleted || !tutorialStorage.Saved || !registries.Contains(nameof(ShrinkCommandStaticRegistryAttribute)) || @@ -91,6 +99,11 @@ public sealed class MemoryTutorialStorage : IShrinkTutorialStorage public readonly struct GodotSmokeEvent : IShrinkEvent { } +public struct GodotEntJoySmokeComponent : EntJoy.ECS.IComponentData +{ + public int Value; +} + [ShrinkEventSubscriber] public sealed class GodotSubscriber { diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/ShrinkSDK.Godot.Sample.csproj b/Godot/Samples/ShrinkSDK.Godot.Sample/ShrinkSDK.Godot.Sample.csproj index 16cfb49..4a64040 100644 --- a/Godot/Samples/ShrinkSDK.Godot.Sample/ShrinkSDK.Godot.Sample.csproj +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/ShrinkSDK.Godot.Sample.csproj @@ -1,7 +1,8 @@ - + net8.0 true + $(NoWarn);0436 true <_ShrinkCodeGenTaskAssembly>$(MSBuildThisFileDirectory)..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\bin\$(Configuration)\net8.0\ShrinkSDK.CodeGen.Task.dll @@ -11,6 +12,7 @@ + diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/WorldDemo.cs b/Godot/Samples/ShrinkSDK.Godot.Sample/WorldDemo.cs new file mode 100644 index 0000000..8f1732b --- /dev/null +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/WorldDemo.cs @@ -0,0 +1,108 @@ +#nullable enable +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Cysharp.Threading.Tasks; +using Godot; +using ShrinkApp; +using ShrinkDataSaver; +using ShrinkEventBus; +using ShrinkSDK.Godot; + +public partial class WorldDemo : Node +{ + private readonly ShrinkGodotRuntime _runtime = new(); + private WorldDemoWorld? _active; + + public override void _Ready() + { + ProcessMode = ProcessModeEnum.Always; + _runtime.Initialize(this); + RunAsync().Forget(); + } + public override void _Process(double delta) => _runtime.Tick(delta); + public override void _Notification(int what) => _runtime.Notify(what); + + private bool SwitchWorld(bool failRestore = false) + { + var candidate = new WorldDemoWorld(); + try + { + AddChild(candidate); + candidate.Restore(failRestore); + } + catch (InvalidOperationException) + { + candidate.Free(); + return false; + } + var old = _active; + _active = candidate; + old?.Free(); + candidate.Commit(); + return true; + } + + private static void Check(bool value, string message) + { + if (!value) throw new InvalidOperationException(message); + } + + private async UniTaskVoid RunAsync() + { + try + { + var uninitialized = new ShrinkGodotRuntime(); + var rejected = false; + try { await uninitialized.StartAppAsync(); } + catch (InvalidOperationException) { rejected = true; } + Check(rejected, "Start must require initialization."); + ShrinkGodotRuntime.RequireWoven(GetType().Assembly); + await _runtime.StartAppAsync(new ShrinkAppSettings + { + DisabledModuleIds = ["shrink.command", "shrink.datasaver", "shrink.network"] + }); + var root = Path.Combine(_runtime.PersistentDataPath, "world-demo"); + var storage = ShrinkGodotStorage.CreateRenameOnly(root); + await storage.WriteAsync("probe", new byte[] { 1 }); + await storage.WriteAsync("probe", new byte[] { 2 }); + await storage.WriteAsync("probe", new byte[] { 3 }); + Check((await storage.ReadAsync("probe.bak1"))[0] == 2, "First backup mismatch."); + Check((await storage.ReadAsync("probe.bak2"))[0] == 1, "Second backup mismatch."); + + Check(SwitchWorld(), "First world failed."); + var first = _active!; + var firstKey = first.Key; + Check(first.IsInsideTree() && first.ReadyCount == 1 && first.EffectCount == 0, + "Restore must suppress effects and commit must publish ready."); + Check(!SwitchWorld(true) && ReferenceEquals(_active, first) && first.IsInsideTree(), + "Failed candidate replaced the active world."); + Check(EventBus.CaptureNamedBuses().Count(bus => bus.Key == firstKey) == 1, + "Candidate failure damaged the original bus."); + Check(SwitchWorld(), "Second world failed."); + Check(!GodotObject.IsInstanceValid(first) && !EventBus.TryGetBus(firstKey, out _), + "Old node or subscription bus survived replacement."); + var active = _active!; + GetTree().Paused = true; + Check(EventBus.PostDeferred(active.Bus, new DemoEffect()).Accepted, "Deferred rejected."); + Check(active.EffectCount == 0, "Effect ran synchronously."); + for (var frame = 0; frame < 4 && active.EffectCount == 0; frame++) + await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); + Check(active.EffectCount == 1, "Paused app did not pump."); + GetTree().Paused = false; + active.Free(); + _active = null; + await _runtime.ShutdownAsync(); + Check(_runtime.IsShutdown && !_runtime.TryPost(() => { }), "Closed runtime accepted dispatch."); + Check(!await Task.Run(() => _runtime.TryPostDeferred(() => { })), "Worker posted after shutdown."); + GD.Print("SHRINK_GODOT_WORLD_PASS"); + GetTree().Quit(0); + } + catch (Exception exception) + { + GD.PushError($"SHRINK_GODOT_WORLD_FAIL {exception}"); + GetTree().Quit(1); + } + } +} diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/WorldDemo.tscn b/Godot/Samples/ShrinkSDK.Godot.Sample/WorldDemo.tscn new file mode 100644 index 0000000..222547e --- /dev/null +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/WorldDemo.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource path="res://WorldDemo.cs" type="Script" id="1"] + +[node name="WorldDemo" type="Node"] +script = ExtResource("1") diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/WorldDemoWorld.cs b/Godot/Samples/ShrinkSDK.Godot.Sample/WorldDemoWorld.cs new file mode 100644 index 0000000..9cc6540 --- /dev/null +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/WorldDemoWorld.cs @@ -0,0 +1,62 @@ +#nullable enable +using System; +using Godot; +using ShrinkEventBus; + +public partial class WorldDemoWorld : Node +{ + private IDisposable? _binding; + private bool _restoring = true; + private readonly DemoWorldSubscriber _subscriber = new(); + public ShrinkBusKey Key { get; } = ShrinkBusKey.World(Guid.NewGuid().ToString("N")); + public IShrinkEventBus Bus { get; private set; } = null!; + public int ReadyCount => _subscriber.ReadyCount; + public int EffectCount => _subscriber.EffectCount; + + public override void _EnterTree() + { + Bus = EventBus.CreateBus(Key, new ShrinkBusOptions + { + Scheduler = ShrinkBusSchedulerKind.MainThread, + DispatchMode = ShrinkDispatchMode.Ordered, + QueueCapacity = 32 + }); + _binding = EventBus.Attach(_subscriber, Key); + } + + public void Restore(bool fail) + { + Post(new DemoEffect()); + if (fail) throw new InvalidOperationException("Candidate restore failed."); + } + + public void Commit() + { + _restoring = false; + Post(new DemoWorldReady()); + } + + private void Post(T value) where T : IShrinkEvent + { + if (!_restoring) Bus.Post(value); + } + + public override void _ExitTree() + { + _binding?.Dispose(); + _binding = null; + EventBus.RemoveBus(Key); + } +} + +public readonly struct DemoWorldReady : IShrinkEvent { } +public readonly struct DemoEffect : IShrinkEvent { } + +[ShrinkEventSubscriber] +public sealed class DemoWorldSubscriber +{ + public int ReadyCount { get; private set; } + public int EffectCount { get; private set; } + [ShrinkSubscribe] private void OnReady(DemoWorldReady value) => ReadyCount++; + [ShrinkSubscribe] private void OnEffect(DemoEffect value) => EffectCount++; +} diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/project.godot b/Godot/Samples/ShrinkSDK.Godot.Sample/project.godot index fa314e9..873d7db 100644 --- a/Godot/Samples/ShrinkSDK.Godot.Sample/project.godot +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="ShrinkSDK Godot Sample" run/main_scene="res://Main.tscn" -config/features=PackedStringArray("4.6", "C#") +config/features=PackedStringArray("4.7", "C#") [display] diff --git a/Godot/ShrinkSDK.Godot.slnx b/Godot/ShrinkSDK.Godot.slnx index 127c076..24e8e5e 100644 --- a/Godot/ShrinkSDK.Godot.slnx +++ b/Godot/ShrinkSDK.Godot.slnx @@ -21,6 +21,8 @@ + + diff --git a/Godot/Tests/NuGetConsumer/NuGetConsumer.csproj b/Godot/Tests/NuGetConsumer/NuGetConsumer.csproj index b3c734d..735ecd1 100644 --- a/Godot/Tests/NuGetConsumer/NuGetConsumer.csproj +++ b/Godot/Tests/NuGetConsumer/NuGetConsumer.csproj @@ -5,7 +5,7 @@ ShrinkSDK.NuGetConsumer - + diff --git a/README.md b/README.md index 14961d7..2d369a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ShrinkSDK Workspace -ShrinkSDK 是一组可独立组合的 Unity / Godot C# 运行时模块。共享内核以 `netstandard2.1` 为最低目标;Unity 侧支持 Unity 2022.3,Godot 侧首期支持 Godot 4.6.3 Mono 和桌面平台。 +ShrinkSDK 是一组可独立组合的 Unity / Godot C# 运行时模块。共享内核以 `netstandard2.1` 为最低目标;Unity 侧支持 Unity 2022.3,Godot 侧首期支持 Godot 4.7.2 Mono 和桌面平台。Godot 可选集成 `ShrinkSDK.EntJoy.Godot`,提供 EntJoy headless ECS/JobSystem 的节点生命周期接入。 Workspace 用于跨包集成、Unity 场景验证、Godot 消费者验证和 CodeGen 织入测试。每个 `Assets/Modules/` 是独立公开仓库的 Git submodule;根仓库保留相邻 `.meta`,不要删除或重新生成这些文件。 @@ -47,7 +47,7 @@ https://git.crash.work/ShrinkSDK/Installer.git#main } ], "dependencies": { - "com.cneicy.shrink-app-starter-basic": "0.3.0" + "com.cneicy.shrink-app-starter-basic": "0.4.1" } } ``` @@ -55,7 +55,7 @@ https://git.crash.work/ShrinkSDK/Installer.git#main 如果项目已经有 `scopedRegistries` 或 `dependencies`,只合并对应条目,不要覆盖原有配置。Git 安装同样建议固定 tag,例如: ```text -https://git.crash.work/ShrinkSDK/ShrinkEventBus.git#v2.1.0 +https://git.crash.work/ShrinkSDK/ShrinkEventBus.git#v2.3.1 ``` ### 快速开始 @@ -64,9 +64,9 @@ https://git.crash.work/ShrinkSDK/ShrinkEventBus.git#v2.1.0 EventBus、Command、Network 和 App 的特性注册由 Unity ILPostProcessor 在编译期织入,不需要运行时反射扫描或手工维护注册表。修改带 ShrinkSDK 特性的代码后,等待 Unity 完成重新编译,再进入 Play Mode。 -## Godot 4.6 C# 安装与使用 +## Godot 4.7 C# 安装与使用 -Godot 项目需要 Godot 4.6.3 Mono 和 .NET 8 SDK。使用现有 Workspace 验证时,直接用 Godot 打开: +Godot 项目需要 Godot 4.7.2 Mono 和 .NET 8 SDK。使用现有 Workspace 验证时,直接用 Godot 打开: ```text Godot/Samples/ShrinkSDK.Godot.Sample/project.godot @@ -87,9 +87,9 @@ Godot/Samples/ShrinkSDK.Godot.Sample/project.godot 然后为 Godot 的 `.csproj` 安装宿主和需要的模块,例如: ```powershell -dotnet add package ShrinkSDK.Godot --version 0.1.0 -dotnet add package ShrinkSDK.App.Starter.Basic --version 0.3.0 -dotnet add package ShrinkSDK.EventBus --version 2.1.0 +dotnet add package ShrinkSDK.Godot --version 0.2.1 +dotnet add package ShrinkSDK.App.Starter.Basic --version 0.4.1 +dotnet add package ShrinkSDK.EventBus --version 2.3.1 dotnet restore dotnet build ``` @@ -118,7 +118,7 @@ public partial class GameRoot : ShrinkGodotHost } ``` -`ShrinkGodotHost` 在 `_Ready` 中安装平台服务,在 `_Process` 中泵送主线程和网络队列,并转发暂停、恢复与退出生命周期。Godot Installer 插件的安装、包管理面板、CodeGen 状态和命令行验证方式见 [Godot/README.md](Godot/README.md)。 +`ShrinkGodotHost` 在 `_Ready` 中安装平台服务,在帧边界泵送主线程和网络队列,并转发暂停、恢复与退出生命周期。`ShutdownAppAsync()` 只停止 App,`ShutdownAsync()` 关闭整个运行时。Godot Installer 插件的安装、包管理面板、CodeGen 状态和命令行验证方式见 [Godot/README.md](Godot/README.md)。 ## 开发与发布 diff --git a/Tools/AgentSupport/RuntimeTests/DeferredStorageTests.cs b/Tools/AgentSupport/RuntimeTests/DeferredStorageTests.cs new file mode 100644 index 0000000..6a3b2a5 --- /dev/null +++ b/Tools/AgentSupport/RuntimeTests/DeferredStorageTests.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using NUnit.Framework; +using ShrinkDataSaver; +using ShrinkEventBus; +using ShrinkSDK.Runtime; + +namespace Shrink.Inspect.RuntimeTests; + +[TestFixture, NonParallelizable] +public sealed class DeferredStorageTests +{ + private IShrinkMainThreadDispatcher _previous = null!; + + [SetUp] + public void SetUp() => _previous = ShrinkEventBusRuntime.MainThreadDispatcher; + + [TearDown] + public void TearDown() => ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(_previous); + + [Test] + public void DeferredUsesSeparateDispatcherAndNextPumpForRecursion() + { + var dispatcher = new Dispatcher(); + ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(dispatcher); + using var bus = EventBus.CreateBus(ShrinkBusKey.World(Guid.NewGuid().ToString()), + new ShrinkBusOptions { Scheduler = ShrinkBusSchedulerKind.MainThread }); + var seen = new List(); + using var observer = EventBus.ObserveDiagnostics(trace => + { + if (trace.BusKey != bus.Key) return; + seen.Add(1); + if (seen.Count == 1) EventBus.PostDeferred(bus, new Evt()); + }); + Assert.That(EventBus.PostDeferred(bus, new Evt()).Accepted, Is.True); + Assert.That(seen, Is.Empty); + dispatcher.Pump(); + Assert.That(seen.Count, Is.EqualTo(1)); + dispatcher.Pump(); + Assert.That(seen.Count, Is.EqualTo(2)); + Assert.That(dispatcher.InlineCalls, Is.Zero); + EventBus.RemoveBus(bus.Key); + } + + [Test] + public void OverflowDuringCallbackDoesNotExecuteNewEventsInSamePump() + { + var dispatcher = new Dispatcher(); + ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(dispatcher); + using var bus = EventBus.CreateBus(ShrinkBusKey.World(Guid.NewGuid().ToString()), + new ShrinkBusOptions { Scheduler = ShrinkBusSchedulerKind.MainThread, + QueueCapacity = 2, OverflowPolicy = ShrinkQueueOverflowPolicy.DropOldest }); + var count = 0; + using var observer = EventBus.ObserveDiagnostics(trace => + { + if (trace.BusKey != bus.Key) return; + if (++count == 1) + for (var i = 0; i < 3; i++) EventBus.PostDeferred(bus, new Evt()); + }); + EventBus.PostDeferred(bus, new Evt()); + EventBus.PostDeferred(bus, new Evt()); + dispatcher.Pump(); + Assert.That(count, Is.EqualTo(2)); + dispatcher.Pump(); + Assert.That(count, Is.EqualTo(4)); + EventBus.RemoveBus(bus.Key); + } + + [Test] + public void RejectedDispatcherDoesNotConsumeCapacity() + { + var dispatcher = new Dispatcher { Reject = true }; + ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(dispatcher); + using var bus = EventBus.CreateBus(ShrinkBusKey.World(Guid.NewGuid().ToString()), + new ShrinkBusOptions { Scheduler = ShrinkBusSchedulerKind.MainThread, QueueCapacity = 1 }); + Assert.That(EventBus.PostDeferred(bus, new Evt()).Failure, Is.EqualTo(ShrinkPostFailure.SchedulerRejected)); + dispatcher.Reject = false; + Assert.That(EventBus.PostDeferred(bus, new Evt()).Accepted, Is.True); + dispatcher.Pump(); + EventBus.RemoveBus(bus.Key); + } + + [Test] + public void NormalAndDeferredShareCapacityAndDisposeDropsPending() + { + var dispatcher = new Dispatcher { IsMainThread = false }; + ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(dispatcher); + using var bus = EventBus.CreateBus(ShrinkBusKey.World(Guid.NewGuid().ToString()), + new ShrinkBusOptions { Scheduler = ShrinkBusSchedulerKind.MainThread, QueueCapacity = 1 }); + Assert.That(bus.Post(new Evt()).Accepted, Is.True); + Assert.That(EventBus.PostDeferred(bus, new Evt()).Failure, Is.EqualTo(ShrinkPostFailure.QueueFull)); + var dispatched = 0; + using var observation = EventBus.ObserveDiagnostics(trace => + { + if (trace.BusKey == bus.Key) dispatched++; + }); + EventBus.RemoveBus(bus.Key); + dispatcher.Pump(); + Assert.That(dispatched, Is.Zero); + } + + [Test] + public async Task RenameRecoveryNeverPromotesPartialTempAndKeepsBackups() + { + var root = Path.Combine(Path.GetTempPath(), "shrink-storage-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + try + { + Assert.Throws(() => new LocalStorageProvider(root, LocalStorageCommitStrategy.RenameOnly)); + var storage = new LocalStorageProvider(root, LocalStorageCommitStrategy.RenameOnly, + (source, target) => File.Move(source, target, true)); + File.WriteAllText(Path.Combine(root, "save.tmp"), "partial"); + Assert.That(await storage.ExistsAsync("save"), Is.False); + await storage.WriteAsync("save", new byte[] { 1 }); + await storage.WriteAsync("save", new byte[] { 2 }); + await storage.WriteAsync("save", new byte[] { 3 }); + Assert.That(await storage.ReadAsync("save.bak1"), Is.EqualTo(new byte[] { 2 })); + Assert.That(await storage.ReadAsync("save.bak2"), Is.EqualTo(new byte[] { 1 })); + File.Delete(Path.Combine(root, "save")); + File.WriteAllText(Path.Combine(root, "save.tmp"), "partial"); + Assert.That(await storage.ExistsAsync("save"), Is.True); + Assert.That(await storage.ReadAsync("save"), Is.EqualTo(new byte[] { 2 })); + await storage.DeleteAsync("save"); + Assert.That(await storage.ExistsAsync("save"), Is.False, "Deleted saves must not recover from backups."); + } + finally { Directory.Delete(root, true); } + } + + [Test] + public async Task DeleteReportsLockedFileInsteadOfClaimingSuccess() + { + if (!OperatingSystem.IsWindows()) Assert.Ignore("Windows file sharing semantics."); + var root = Path.Combine(Path.GetTempPath(), "shrink-delete-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + try + { + var storage = new LocalStorageProvider(root); + await storage.WriteAsync("save", new byte[] { 1 }); + using var locked = new FileStream(Path.Combine(root, "save"), FileMode.Open, FileAccess.Read, FileShare.None); + Assert.ThrowsAsync(async () => await storage.DeleteAsync("save")); + } + finally { Directory.Delete(root, true); } + } + + private readonly struct Evt : IShrinkEvent { } + + private sealed class Dispatcher : IShrinkMainThreadDispatcher, IShrinkDeferredMainThreadDispatcher + { + private readonly Queue _pending = new(); + public bool IsMainThread { get; set; } = true; + public bool Reject { get; set; } + public int InlineCalls { get; private set; } + public bool TryPost(Action action) { InlineCalls++; action(); return true; } + public bool TryPostDeferred(Action action) + { + if (Reject) return false; + _pending.Enqueue(action); + return true; + } + public void Pump() + { + var count = _pending.Count; + while (count-- > 0) _pending.Dequeue()(); + } + } +} diff --git a/Tools/Release/package-versions.json b/Tools/Release/package-versions.json index 97e2c6f..e6cdb08 100644 --- a/Tools/Release/package-versions.json +++ b/Tools/Release/package-versions.json @@ -4,12 +4,12 @@ { "directory": "ShrinkApp.Core", "name": "com.cneicy.shrink-app-core", - "version": "0.2.3", + "version": "0.2.4", "repository": "https://git.crash.work/ShrinkSDK/ShrinkApp.Core.git", "nugetPackages": [ { "id": "ShrinkSDK.App.Core", - "version": "0.2.3", + "version": "0.2.4", "project": "Assets/Modules/ShrinkApp.Core/DotNet~/ShrinkSDK.App.Core.csproj" } ] @@ -17,12 +17,12 @@ { "directory": "ShrinkApp.Starter.Basic", "name": "com.cneicy.shrink-app-starter-basic", - "version": "0.3.7", + "version": "0.4.1", "repository": "https://git.crash.work/ShrinkSDK/ShrinkApp.Starter.Basic.git", "nugetPackages": [ { "id": "ShrinkSDK.App.Starter.Basic", - "version": "0.3.7", + "version": "0.4.1", "project": "Assets/Modules/ShrinkApp.Starter.Basic/DotNet~/ShrinkSDK.App.Starter.Basic.csproj" } ] @@ -30,12 +30,12 @@ { "directory": "ShrinkCommand", "name": "com.cneicy.shrink-command", - "version": "0.4.2", + "version": "0.4.3", "repository": "https://git.crash.work/ShrinkSDK/ShrinkCommand.git", "nugetPackages": [ { "id": "ShrinkSDK.Command", - "version": "0.4.2", + "version": "0.4.3", "project": "Assets/Modules/ShrinkCommand/DotNet~/ShrinkSDK.Command.csproj" } ] @@ -43,12 +43,12 @@ { "directory": "ShrinkCommand.Integration.App", "name": "com.cneicy.shrink-command-integration-app", - "version": "0.2.5", + "version": "0.2.6", "repository": "https://git.crash.work/ShrinkSDK/ShrinkCommand.Integration.App.git", "nugetPackages": [ { "id": "ShrinkSDK.Command.Integration.App", - "version": "0.2.5", + "version": "0.2.6", "project": "Assets/Modules/ShrinkCommand.Integration.App/DotNet~/ShrinkSDK.Command.Integration.App.csproj" } ] @@ -56,12 +56,12 @@ { "directory": "ShrinkCommand.Integration.EventBus", "name": "com.cneicy.shrink-command-integration-eventbus", - "version": "0.2.5", + "version": "0.2.6", "repository": "https://git.crash.work/ShrinkSDK/ShrinkCommand.Integration.EventBus.git", "nugetPackages": [ { "id": "ShrinkSDK.Command.Integration.EventBus", - "version": "0.2.5", + "version": "0.2.6", "project": "Assets/Modules/ShrinkCommand.Integration.EventBus/DotNet~/ShrinkSDK.Command.Integration.EventBus.csproj" } ] @@ -69,12 +69,12 @@ { "directory": "ShrinkCommand.Integration.Network", "name": "com.cneicy.shrink-command-integration-network", - "version": "0.2.5", + "version": "0.2.6", "repository": "https://git.crash.work/ShrinkSDK/ShrinkCommand.Integration.Network.git", "nugetPackages": [ { "id": "ShrinkSDK.Command.Integration.Network", - "version": "0.2.5", + "version": "0.2.6", "project": "Assets/Modules/ShrinkCommand.Integration.Network/DotNet~/ShrinkSDK.Command.Integration.Network.csproj" } ] @@ -82,12 +82,12 @@ { "directory": "ShrinkContext.AppAdapter", "name": "com.cneicy.shrink-context-app-adapter", - "version": "0.2.4", + "version": "0.2.5", "repository": "https://git.crash.work/ShrinkSDK/ShrinkContext.AppAdapter.git", "nugetPackages": [ { "id": "ShrinkSDK.Context.AppAdapter", - "version": "0.2.4", + "version": "0.2.5", "project": "Assets/Modules/ShrinkContext.AppAdapter/DotNet~/ShrinkSDK.Context.AppAdapter.csproj" } ] @@ -95,12 +95,12 @@ { "directory": "ShrinkContext.Core", "name": "com.cneicy.shrink-context-core", - "version": "0.3.0", + "version": "0.3.1", "repository": "https://git.crash.work/ShrinkSDK/ShrinkContext.Core.git", "nugetPackages": [ { "id": "ShrinkSDK.Context.Core", - "version": "0.3.0", + "version": "0.3.1", "project": "Assets/Modules/ShrinkContext.Core/DotNet~/ShrinkSDK.Context.Core.csproj" } ] @@ -108,12 +108,12 @@ { "directory": "ShrinkContext.EventBusAdapter", "name": "com.cneicy.shrink-context-eventbus-adapter", - "version": "0.2.4", + "version": "0.2.5", "repository": "https://git.crash.work/ShrinkSDK/ShrinkContext.EventBusAdapter.git", "nugetPackages": [ { "id": "ShrinkSDK.Context.EventBusAdapter", - "version": "0.2.4", + "version": "0.2.5", "project": "Assets/Modules/ShrinkContext.EventBusAdapter/DotNet~/ShrinkSDK.Context.EventBusAdapter.csproj" } ] @@ -121,12 +121,12 @@ { "directory": "ShrinkDataSaver", "name": "com.cneicy.shrink-datasaver", - "version": "2.4.0", + "version": "2.5.0", "repository": "https://git.crash.work/ShrinkSDK/ShrinkDataSaver.git", "nugetPackages": [ { "id": "ShrinkSDK.DataSaver", - "version": "2.4.0", + "version": "2.5.0", "project": "Assets/Modules/ShrinkDataSaver/DotNet~/ShrinkSDK.DataSaver.csproj" } ] @@ -134,12 +134,12 @@ { "directory": "ShrinkDataSaver.Integration.App", "name": "com.cneicy.shrink-datasaver-integration-app", - "version": "0.2.5", + "version": "0.3.1", "repository": "https://git.crash.work/ShrinkSDK/ShrinkDataSaver.Integration.App.git", "nugetPackages": [ { "id": "ShrinkSDK.DataSaver.Integration.App", - "version": "0.2.5", + "version": "0.3.1", "project": "Assets/Modules/ShrinkDataSaver.Integration.App/DotNet~/ShrinkSDK.DataSaver.Integration.App.csproj" } ] @@ -147,12 +147,12 @@ { "directory": "ShrinkDataSaver.Integration.EventBus", "name": "com.cneicy.shrink-datasaver-integration-eventbus", - "version": "2.2.5", + "version": "2.2.6", "repository": "https://git.crash.work/ShrinkSDK/ShrinkDataSaver.Integration.EventBus.git", "nugetPackages": [ { "id": "ShrinkSDK.DataSaver.Integration.EventBus", - "version": "2.2.5", + "version": "2.2.6", "project": "Assets/Modules/ShrinkDataSaver.Integration.EventBus/DotNet~/ShrinkSDK.DataSaver.Integration.EventBus.csproj" } ] @@ -160,12 +160,12 @@ { "directory": "ShrinkEventBus", "name": "com.cneicy.shrink-eventbus", - "version": "2.2.2", + "version": "2.3.1", "repository": "https://git.crash.work/ShrinkSDK/ShrinkEventBus.git", "nugetPackages": [ { "id": "ShrinkSDK.EventBus", - "version": "2.2.2", + "version": "2.3.1", "project": "Assets/Modules/ShrinkEventBus/DotNet~/ShrinkSDK.EventBus.csproj" } ] @@ -173,18 +173,18 @@ { "directory": "ShrinkEventBus.Entities", "name": "com.cneicy.shrink-eventbus-entities", - "version": "0.1.5", + "version": "0.1.6", "repository": "https://git.crash.work/ShrinkSDK/ShrinkEventBus.Entities.git" }, { "directory": "ShrinkInstaller", "name": "com.cneicy.shrink-installer", - "version": "0.2.13", + "version": "0.2.15", "repository": "https://git.crash.work/ShrinkSDK/Installer.git", "nugetPackages": [ { "id": "ShrinkSDK.Godot.Installer", - "version": "0.1.3", + "version": "0.2.0", "project": "Assets/Modules/ShrinkInstaller/Godot~/ShrinkSDK.Godot.Installer.csproj" } ] @@ -192,17 +192,17 @@ { "directory": "ShrinkModFramework", "name": "com.cneicy.shrink-mod-framework", - "version": "0.3.6", + "version": "0.3.7", "repository": "https://git.crash.work/ShrinkSDK/ShrinkModFramework.git", "nugetPackages": [ { "id": "ShrinkSDK.ModFramework", - "version": "0.3.6", + "version": "0.3.7", "project": "Assets/Modules/ShrinkModFramework/DotNet~/ShrinkSDK.ModFramework.csproj" }, { "id": "ShrinkSDK.ModFramework.Godot", - "version": "0.1.5", + "version": "0.1.6", "project": "Assets/Modules/ShrinkModFramework/Godot~/ShrinkSDK.ModFramework.Godot.csproj" } ] @@ -210,12 +210,12 @@ { "directory": "ShrinkNetwork", "name": "com.cneicy.shrink-network", - "version": "0.4.2", + "version": "0.4.3", "repository": "https://git.crash.work/ShrinkSDK/ShrinkNetwork.git", "nugetPackages": [ { "id": "ShrinkSDK.Network", - "version": "0.4.2", + "version": "0.4.3", "project": "Assets/Modules/ShrinkNetwork/DotNet~/ShrinkSDK.Network.csproj" } ] @@ -223,12 +223,12 @@ { "directory": "ShrinkNetwork.Integration.App", "name": "com.cneicy.shrink-network-integration-app", - "version": "0.2.5", + "version": "0.2.6", "repository": "https://git.crash.work/ShrinkSDK/ShrinkNetwork.Integration.App.git", "nugetPackages": [ { "id": "ShrinkSDK.Network.Integration.App", - "version": "0.2.5", + "version": "0.2.6", "project": "Assets/Modules/ShrinkNetwork.Integration.App/DotNet~/ShrinkSDK.Network.Integration.App.csproj" } ] @@ -236,12 +236,12 @@ { "directory": "ShrinkNetwork.Integration.EventBus", "name": "com.cneicy.shrink-network-integration-eventbus", - "version": "0.2.5", + "version": "0.2.6", "repository": "https://git.crash.work/ShrinkSDK/ShrinkNetwork.Integration.EventBus.git", "nugetPackages": [ { "id": "ShrinkSDK.Network.Integration.EventBus", - "version": "0.2.5", + "version": "0.2.6", "project": "Assets/Modules/ShrinkNetwork.Integration.EventBus/DotNet~/ShrinkSDK.Network.Integration.EventBus.csproj" } ] @@ -249,12 +249,12 @@ { "directory": "ShrinkRuntime.Abstractions", "name": "com.cneicy.shrink-runtime-abstractions", - "version": "0.1.0", + "version": "0.2.0", "repository": "https://git.crash.work/ShrinkSDK/ShrinkRuntime.Abstractions.git", "nugetPackages": [ { "id": "ShrinkSDK.Runtime.Abstractions", - "version": "0.1.0", + "version": "0.2.0", "project": "Assets/Modules/ShrinkRuntime.Abstractions/DotNet~/ShrinkSDK.Runtime.Abstractions.csproj" } ] @@ -275,26 +275,38 @@ { "directory": "ShrinkTutorial", "name": "com.cneicy.shrink-tutorial", - "version": "0.2.0", + "version": "0.2.1", "repository": "https://git.crash.work/ShrinkSDK/ShrinkTutorial.git", "nugetPackages": [ { "id": "ShrinkSDK.Tutorial", - "version": "0.2.0", + "version": "0.2.1", "project": "Assets/Modules/ShrinkTutorial/DotNet~/ShrinkSDK.Tutorial.csproj" }, { "id": "ShrinkSDK.Tutorial.Godot", - "version": "0.1.0", + "version": "0.1.1", "project": "Assets/Modules/ShrinkTutorial/Godot~/ShrinkSDK.Tutorial.Godot.csproj" } ] } ], "standaloneNugetPackages": [ + { + "id": "ShrinkSDK.EntJoy.Godot", + "version": "0.1.1", + "project": "Godot/Packages/ShrinkSDK.EntJoy.Godot/ShrinkSDK.EntJoy.Godot.csproj", + "repository": "https://git.crash.work/ShrinkSDK/Workspace.git" + }, + { + "id": "ShrinkSDK.EventBus.Godot", + "version": "0.1.1", + "project": "Godot/Packages/ShrinkSDK.EventBus.Godot/ShrinkSDK.EventBus.Godot.csproj", + "repository": "https://git.crash.work/ShrinkSDK/Workspace.git" + }, { "id": "ShrinkSDK.Godot", - "version": "0.1.5", + "version": "0.2.1", "project": "Godot/Packages/ShrinkSDK.Godot/ShrinkSDK.Godot.csproj", "repository": "https://git.crash.work/ShrinkSDK/ShrinkGodot.git" } diff --git a/Tools/Release/update-shrinksdk-versions.mjs b/Tools/Release/update-shrinksdk-versions.mjs index 0db23e8..ee443e6 100644 --- a/Tools/Release/update-shrinksdk-versions.mjs +++ b/Tools/Release/update-shrinksdk-versions.mjs @@ -270,6 +270,21 @@ function buildModel() { } } + // Workspace-owned Godot adapters are not separate Git submodules. + const godotPackages = path.join(workspaceRoot, 'Godot', 'Packages'); + if (fs.existsSync(godotPackages)) { + for (const entry of fs.readdirSync(godotPackages, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const relative = `Godot/Packages/${entry.name}`; + if (modules.some(module => module.path === relative)) continue; + const owner = { path: relative, url: 'https://git.crash.work/ShrinkSDK/Workspace.git' }; + for (const projectPath of walkFiles(path.join(workspaceRoot, relative), + candidate => candidate.toLowerCase().endsWith('.csproj'))) { + addProject(readProject(projectPath, owner, null)); + } + } + } + for (const upmPackage of upmPackages) { const coupled = upmPackage.nugetPackages.filter(project => project.coupledToUpm); if (coupled.length > 1) {