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

-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>