feat: integrate composable Godot runtime and EventBus debugger

This commit is contained in:
cneicy committed 2026-11-06 19:18:54 +08:00
1 parent 0d9e04c09d
commit 51f8a1d044
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
+1
View File
@@ -0,0 +1 @@
-define:SHRINK_EVENTBUS_HAS_SHARED_CODEGEN
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6b266fc005854c67a6e9e23814b5c92a
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+24 -21
View File
@@ -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
View File
@@ -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 -->
@@ -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
}
@@ -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)
@@ -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
@@ -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
+43 -12
View File
@@ -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]
+2
View File
@@ -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>
+9 -9
View File
@@ -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()();
}
}
}
+56 -44
View File
@@ -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) {