feat: integrate composable Godot runtime and EventBus debugger
This commit is contained in:
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]
|
||||
|
||||
|
||||
Reference in new issue
Block a user