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

@@ -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) {