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

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