feat: integrate composable Godot runtime and EventBus debugger
This commit is contained in:
55 files changed
+1481
-114
No files matched your search
@@ -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
|
||||
|
||||
Submodule Assets/Modules/ShrinkApp.Core updated: 5efc172bca...124de9c732.
Submodule Assets/Modules/ShrinkApp.Starter.Basic updated: fd3788e2ce...0127fae182.
Submodule Assets/Modules/ShrinkCommand updated: 174b9c6976...47beba2269.
Submodule Assets/Modules/ShrinkCommand.Integration.App updated: 25f4cfb82e...da25a40015.
Submodule Assets/Modules/ShrinkCommand.Integration.EventBus updated: 82c0833a11...fc6600fe9e.
Submodule Assets/Modules/ShrinkCommand.Integration.Network updated: b1c42ea4ab...efc0f43b17.
Submodule Assets/Modules/ShrinkContext.AppAdapter updated: 7a5071217d...80cd594ac5.
Submodule Assets/Modules/ShrinkContext.Core updated: d3eca1f155...18527491e1.
Submodule Assets/Modules/ShrinkContext.EventBusAdapter updated: 92f55f4bdf...e91183486a.
Submodule Assets/Modules/ShrinkDataSaver updated: cdb682f484...67d6814c4b.
Submodule Assets/Modules/ShrinkDataSaver.Integration.App updated: fea752a8ff...c522594520.
Submodule Assets/Modules/ShrinkDataSaver.Integration.EventBus updated: d6b1940982...9a258e802b.
Submodule Assets/Modules/ShrinkEventBus updated: a92c9d2cea...901ad18453.
Submodule Assets/Modules/ShrinkEventBus.Entities updated: c19d536582...e084fe4c88.
Submodule Assets/Modules/ShrinkInstaller updated: 2e03045423...5370a69e0e.
Submodule Assets/Modules/ShrinkModFramework updated: 91f0141b86...2ab8482359.
Submodule Assets/Modules/ShrinkNetwork updated: 93c2b8520b...47c978544c.
Submodule Assets/Modules/ShrinkNetwork.Integration.App updated: 123bd170d9...cc2763413b.
Submodule Assets/Modules/ShrinkNetwork.Integration.EventBus updated: 2dae3e3890...3b30587ea9.
Submodule Assets/Modules/ShrinkRuntime.Abstractions updated: 52b92d3657...7de22c85fb.
Submodule Assets/Modules/ShrinkTutorial updated: 51c941a017...a87e4dca32.
@@ -0,0 +1 @@
|
||||
-define:SHRINK_EVENTBUS_HAS_SHARED_CODEGEN
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b266fc005854c67a6e9e23814b5c92a
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 元数据,因此业务包可以依赖它而不形成包循环。
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="Godot Local" value="C:\Users\im\Documents\godothub\engine\4.6.3-stable-mono\GodotSharp\Tools\nupkgs" />
|
||||
<add key="ShrinkSDK Local" value="Artifacts" />
|
||||
<add key="ShrinkSDK" value="https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json" />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
|
||||
@@ -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,并在最后一个宿主退出时关闭。
|
||||
@@ -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;
|
||||
|
||||
/// <summary>Godot node that owns an EntJoy World and advances its frame boundary.</summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<AssemblyName>ShrinkSDK.EntJoy.Godot</AssemblyName>
|
||||
<RootNamespace>ShrinkSDK.EntJoy.Godot</RootNamespace>
|
||||
<PackageId>ShrinkSDK.EntJoy.Godot</PackageId>
|
||||
<Version>0.1.1</Version>
|
||||
<Description>Godot lifecycle integration for EntJoy headless ECS and JobSystem.</Description>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="*.cs" />
|
||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||
<ProjectReference Include="..\ShrinkSDK.Godot\ShrinkSDK.Godot.csproj" />
|
||||
<!-- The adapter exposes the runtime API; consumers that use EntJoy source-generated
|
||||
jobs can add EntJoy.ECS directly and opt into its analyzer explicitly. -->
|
||||
<PackageReference Include="EntJoy.ECS" Version="1.0.0" ExcludeAssets="analyzers" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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`,不依赖反射或内部事件。
|
||||
|
||||
## 可复用能力
|
||||
|
||||
<!-- shrink:capabilities -->
|
||||
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)
|
||||
<!-- /shrink:capabilities -->
|
||||
@@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<AssemblyName>ShrinkSDK.EventBus.Godot</AssemblyName>
|
||||
<RootNamespace>ShrinkSDK.EventBus.Godot</RootNamespace>
|
||||
<PackageId>ShrinkSDK.EventBus.Godot</PackageId>
|
||||
<Version>0.1.1</Version>
|
||||
<Description>Godot editor debugger for ShrinkSDK.EventBus.</Description>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<IncludeSymbols>false</IncludeSymbols>
|
||||
<NoWarn>$(NoWarn);NU5128</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="addons\shrink_event_bus\ShrinkEventBusDebugger.cs" />
|
||||
<PackageReference Include="GodotSharp" Version="4.7.2" />
|
||||
<PackageReference Include="ShrinkSDK.EventBus" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="addons\shrink_event_bus\README.md" Pack="true"
|
||||
PackagePath="contentFiles\any\any\addons\shrink_event_bus\README.md" BuildAction="None" />
|
||||
<None Include="addons\shrink_event_bus\plugin.cfg" Pack="true"
|
||||
PackagePath="contentFiles\any\any\addons\shrink_event_bus\plugin.cfg" BuildAction="None" />
|
||||
<None Include="addons\shrink_event_bus\plugin.gd" Pack="true"
|
||||
PackagePath="contentFiles\any\any\addons\shrink_event_bus\plugin.gd" BuildAction="None" />
|
||||
<None Include="addons\shrink_event_bus\*.uid" Pack="true"
|
||||
PackagePath="contentFiles\any\any\addons\shrink_event_bus\" BuildAction="None" />
|
||||
<None Include="addons\shrink_event_bus\editor\*" Pack="true"
|
||||
PackagePath="contentFiles\any\any\addons\shrink_event_bus\editor\" BuildAction="None" />
|
||||
<None Include="addons\shrink_event_bus\ShrinkEventBusDebugger.cs" Pack="true"
|
||||
PackagePath="contentFiles\any\any\addons\shrink_event_bus\ShrinkEventBusDebugger.cs" BuildAction="None" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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 构建不包含采集实现。
|
||||
|
||||
<!-- shrink:capabilities -->
|
||||
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)
|
||||
<!-- /shrink:capabilities -->
|
||||
+194
@@ -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<TraceRecord> _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<string, GArray, bool>(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
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
uid://dcvus446brme5
|
||||
+409
@@ -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)
|
||||
+1
@@ -0,0 +1 @@
|
||||
uid://c8jc4n74owcks
|
||||
+63
@@ -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
|
||||
+1
@@ -0,0 +1 @@
|
||||
uid://dlkjwusf3memk
|
||||
@@ -0,0 +1,6 @@
|
||||
[plugin]
|
||||
name="ShrinkEventBus"
|
||||
description="ShrinkSDK.EventBus 2.3.1 的订阅快照与实时事件调试窗口。"
|
||||
author="cneicy"
|
||||
version="0.1.1"
|
||||
script="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
|
||||
@@ -0,0 +1 @@
|
||||
uid://2r868eolxj3m
|
||||
Submodule Godot/Packages/ShrinkSDK.Godot updated: e355bbfd35...6c2cca5735.
+43
-12
@@ -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 织入
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<Project Sdk="Godot.NET.Sdk/4.6.3">
|
||||
<Project Sdk="Godot.NET.Sdk/4.7.2">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<NoWarn>$(NoWarn);0436</NoWarn>
|
||||
<ShrinkCodeGenEnabled>true</ShrinkCodeGenEnabled>
|
||||
<_ShrinkCodeGenTaskAssembly>$(MSBuildThisFileDirectory)..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\bin\$(Configuration)\net8.0\ShrinkSDK.CodeGen.Task.dll</_ShrinkCodeGenTaskAssembly>
|
||||
</PropertyGroup>
|
||||
@@ -11,6 +12,7 @@
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkCommand\DotNet~\ShrinkSDK.Command.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkNetwork\DotNet~\ShrinkSDK.Network.csproj" />
|
||||
<ProjectReference Include="..\..\Packages\ShrinkSDK.Godot\ShrinkSDK.Godot.csproj" />
|
||||
<ProjectReference Include="..\..\Packages\ShrinkSDK.EntJoy.Godot\ShrinkSDK.EntJoy.Godot.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkApp.Starter.Basic\DotNet~\ShrinkSDK.App.Starter.Basic.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkTutorial\DotNet~\ShrinkSDK.Tutorial.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkTutorial\Godot~\ShrinkSDK.Tutorial.Godot.csproj" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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>(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++;
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
<Project Path="../Assets/Modules/ShrinkTutorial/DotNet~/ShrinkSDK.Tutorial.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkTutorial/Godot~/ShrinkSDK.Tutorial.Godot.csproj" />
|
||||
<Project Path="Packages/ShrinkSDK.Godot/ShrinkSDK.Godot.csproj" />
|
||||
<Project Path="Packages/ShrinkSDK.EntJoy.Godot/ShrinkSDK.EntJoy.Godot.csproj" />
|
||||
<Project Path="Packages/ShrinkSDK.EventBus.Godot/ShrinkSDK.EventBus.Godot.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/CodeGen/">
|
||||
<Project Path="../Assets/Modules/ShrinkShared.CodeGen/DotNet~/ShrinkSDK.CodeGen.Core/ShrinkSDK.CodeGen.Core.csproj" />
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<AssemblyName>ShrinkSDK.NuGetConsumer</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ShrinkSDK.EventBus" Version="2.2.2" />
|
||||
<PackageReference Include="ShrinkSDK.EventBus" Version="2.3.0" />
|
||||
<PackageReference Include="ShrinkSDK.Command" Version="0.4.2" />
|
||||
<PackageReference Include="ShrinkSDK.Network" Version="0.4.2" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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/<Module>` 是独立公开仓库的 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)。
|
||||
|
||||
## 开发与发布
|
||||
|
||||
|
||||
@@ -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<int>();
|
||||
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<ArgumentException>(() => 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<IOException>(async () => await storage.DeleteAsync("save"));
|
||||
}
|
||||
finally { Directory.Delete(root, true); }
|
||||
}
|
||||
|
||||
private readonly struct Evt : IShrinkEvent { }
|
||||
|
||||
private sealed class Dispatcher : IShrinkMainThreadDispatcher, IShrinkDeferredMainThreadDispatcher
|
||||
{
|
||||
private readonly Queue<Action> _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()();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user