From 88ba3cdc48c3d27641d451a89badafa90be35169 Mon Sep 17 00:00:00 2001 From: cneicy Date: Sat, 5 Sep 2026 02:37:43 +0800 Subject: [PATCH] feat: add Godot C# compatibility and shared codegen weaving --- Assets/Modules/ShrinkApp.Core | 2 +- Assets/Modules/ShrinkApp.Starter.Basic | 2 +- Assets/Modules/ShrinkCommand | 2 +- Assets/Modules/ShrinkCommand.Integration.App | 2 +- .../ShrinkCommand.Integration.EventBus | 2 +- .../Modules/ShrinkCommand.Integration.Network | 2 +- Assets/Modules/ShrinkContext.AppAdapter | 2 +- Assets/Modules/ShrinkContext.Core | 2 +- Assets/Modules/ShrinkDataSaver | 2 +- .../Modules/ShrinkDataSaver.Integration.App | 2 +- .../ShrinkDataSaver.Integration.EventBus | 2 +- Assets/Modules/ShrinkEventBus | 2 +- Assets/Modules/ShrinkInstaller | 2 +- Assets/Modules/ShrinkModFramework | 2 +- Assets/Modules/ShrinkNetwork | 2 +- Assets/Modules/ShrinkNetwork.Integration.App | 2 +- .../ShrinkNetwork.Integration.EventBus | 2 +- .../Modules/ShrinkRuntime.Abstractions.meta | 8 + .../ShrinkRuntime.Abstractions/Runtime.meta | 8 + .../Runtime/ShrinkRuntime.Abstractions.asmdef | 5 + .../ShrinkRuntime.Abstractions.asmdef.meta | 7 + .../Runtime/ShrinkRuntimeServices.cs | 127 ++++++++++++++ .../Runtime/ShrinkRuntimeServices.cs.meta | 11 ++ .../ShrinkRuntime.Abstractions/package.json | 7 + .../package.json.meta | 7 + Assets/Modules/ShrinkShared.CodeGen | 2 +- Assets/Modules/ShrinkTutorial | 2 +- Godot/.gitignore | 7 + .../AnalyzerReleases.Shipped.md | 9 + .../ShrinkCodeGenAnalyzer.cs | 75 ++++++++ .../ShrinkSDK.CodeGen.Analyzers.csproj | 13 ++ .../ShrinkSDK.CodeGen.Core.csproj | 15 ++ .../ShrinkSDK.CodeGen.Task.csproj | 34 ++++ .../ShrinkSDK.CodeGen.Task/ShrinkWeaveTask.cs | 98 +++++++++++ .../buildTransitive/ShrinkSDK.CodeGen.props | 5 + .../buildTransitive/ShrinkSDK.CodeGen.targets | 18 ++ Godot/Directory.Build.props | 13 ++ .../ShrinkSDK.Godot.Installer.csproj | 19 ++ .../shrinksdk/ShrinkGodotInstallerPlugin.cs | 133 ++++++++++++++ .../shrinksdk/ShrinkProjectPackageEditor.cs | 148 ++++++++++++++++ Godot/Installer/addons/shrinksdk/plugin.cfg | 7 + Godot/NuGet.Config | 10 ++ .../Runtime/ShrinkAppCore.cs | 137 +++++++++++++++ .../ShrinkSDK.App.Core.csproj | 20 +++ .../Runtime/ShrinkBasicComposition.cs | 34 ++++ .../ShrinkSDK.App.Starter.Basic.csproj | 21 +++ ...inkSDK.Command.Integration.EventBus.csproj | 20 +++ ...rinkSDK.Command.Integration.Network.csproj | 20 +++ .../ShrinkSDK.Command.csproj | 18 ++ .../Runtime/ShrinkContextAppHost.cs | 31 ++++ .../ShrinkSDK.Context.AppAdapter.csproj | 17 ++ .../ShrinkSDK.Context.Core.csproj | 17 ++ ...kSDK.DataSaver.Integration.EventBus.csproj | 20 +++ .../Runtime/ShrinkDataSaverRuntime.cs | 43 +++++ .../ShrinkSDK.DataSaver.csproj | 29 ++++ .../ShrinkSDK.EventBus.csproj | 19 ++ .../ShrinkSDK.Godot/ShrinkGodotHost.cs | 154 +++++++++++++++++ .../ShrinkSDK.Godot/ShrinkSDK.Godot.csproj | 20 +++ .../ShrinkGodotModLoader.cs | 66 +++++++ .../ShrinkSDK.ModFramework.Godot.csproj | 15 ++ .../Runtime/ShrinkModContext.cs | 25 +++ .../ShrinkSDK.ModFramework.csproj | 21 +++ ...inkSDK.Network.Integration.EventBus.csproj | 20 +++ .../ShrinkSDK.Network.csproj | 22 +++ .../ShrinkSDK.Runtime.Abstractions.csproj | 13 ++ .../ShrinkGodotTutorialOverlay.cs | 89 ++++++++++ .../ShrinkSDK.Tutorial.Godot.csproj | 15 ++ .../Runtime/ShrinkTutorialCore.cs | 76 ++++++++ .../ShrinkSDK.Tutorial.csproj | 19 ++ .../Samples/ShrinkSDK.Godot.Sample/Main.tscn | 6 + .../ShrinkSDK.Godot.Sample/SampleRoot.cs | 124 +++++++++++++ .../ShrinkSDK.Godot.Sample/SampleRoot.cs.uid | 1 + .../ShrinkSDK.Godot.Sample.csproj | 20 +++ .../ShrinkSDK.Godot.Sample.sln | 24 +++ .../ShrinkSDK.Godot.Sample/Tutorial.tscn | 12 ++ .../ShrinkSDK.Godot.Sample/TutorialDemo.cs | 49 ++++++ .../TutorialDemo.cs.uid | 1 + .../ShrinkSDK.Godot.Sample/export_presets.cfg | 52 ++++++ .../ShrinkSDK.Godot.Sample/project.godot | 30 ++++ Godot/ShrinkSDK.Godot.slnx | 45 +++++ .../CodeGenFixture/CodeGenFixture.csproj | 20 +++ Godot/Tests/CodeGenFixture/Program.cs | 79 +++++++++ .../CodeGenValidation.csproj | 12 ++ Godot/Tests/CodeGenValidation/Program.cs | 163 ++++++++++++++++++ .../InstallerFixture/InstallerFixture.csproj | 10 ++ Godot/Tests/InstallerFixture/Program.cs | 65 +++++++ .../DuplicateContracts.cs | 7 + .../InvalidCodeGenFixture.csproj | 15 ++ Godot/Tests/ModFixture/FixtureMod.cs | 27 +++ Godot/Tests/ModFixture/ModFixture.csproj | 15 ++ .../ModHostFixture/ModHostFixture.csproj | 13 ++ Godot/Tests/ModHostFixture/Program.cs | 37 ++++ .../Tests/NuGetConsumer/NuGetConsumer.csproj | 12 ++ Godot/Tests/NuGetConsumer/Program.cs | 46 +++++ .../UnityAdapterCompile.csproj | 23 +++ 95 files changed, 2712 insertions(+), 19 deletions(-) create mode 100644 Assets/Modules/ShrinkRuntime.Abstractions.meta create mode 100644 Assets/Modules/ShrinkRuntime.Abstractions/Runtime.meta create mode 100644 Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntime.Abstractions.asmdef create mode 100644 Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntime.Abstractions.asmdef.meta create mode 100644 Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntimeServices.cs create mode 100644 Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntimeServices.cs.meta create mode 100644 Assets/Modules/ShrinkRuntime.Abstractions/package.json create mode 100644 Assets/Modules/ShrinkRuntime.Abstractions/package.json.meta create mode 100644 Godot/.gitignore create mode 100644 Godot/CodeGen/ShrinkSDK.CodeGen.Analyzers/AnalyzerReleases.Shipped.md create mode 100644 Godot/CodeGen/ShrinkSDK.CodeGen.Analyzers/ShrinkCodeGenAnalyzer.cs create mode 100644 Godot/CodeGen/ShrinkSDK.CodeGen.Analyzers/ShrinkSDK.CodeGen.Analyzers.csproj create mode 100644 Godot/CodeGen/ShrinkSDK.CodeGen.Core/ShrinkSDK.CodeGen.Core.csproj create mode 100644 Godot/CodeGen/ShrinkSDK.CodeGen.Task/ShrinkSDK.CodeGen.Task.csproj create mode 100644 Godot/CodeGen/ShrinkSDK.CodeGen.Task/ShrinkWeaveTask.cs create mode 100644 Godot/CodeGen/ShrinkSDK.CodeGen.Task/buildTransitive/ShrinkSDK.CodeGen.props create mode 100644 Godot/CodeGen/ShrinkSDK.CodeGen.Task/buildTransitive/ShrinkSDK.CodeGen.targets create mode 100644 Godot/Directory.Build.props create mode 100644 Godot/Installer/ShrinkSDK.Godot.Installer.csproj create mode 100644 Godot/Installer/addons/shrinksdk/ShrinkGodotInstallerPlugin.cs create mode 100644 Godot/Installer/addons/shrinksdk/ShrinkProjectPackageEditor.cs create mode 100644 Godot/Installer/addons/shrinksdk/plugin.cfg create mode 100644 Godot/NuGet.Config create mode 100644 Godot/Packages/ShrinkSDK.App.Core/Runtime/ShrinkAppCore.cs create mode 100644 Godot/Packages/ShrinkSDK.App.Core/ShrinkSDK.App.Core.csproj create mode 100644 Godot/Packages/ShrinkSDK.App.Starter.Basic/Runtime/ShrinkBasicComposition.cs create mode 100644 Godot/Packages/ShrinkSDK.App.Starter.Basic/ShrinkSDK.App.Starter.Basic.csproj create mode 100644 Godot/Packages/ShrinkSDK.Command.Integration.EventBus/ShrinkSDK.Command.Integration.EventBus.csproj create mode 100644 Godot/Packages/ShrinkSDK.Command.Integration.Network/ShrinkSDK.Command.Integration.Network.csproj create mode 100644 Godot/Packages/ShrinkSDK.Command/ShrinkSDK.Command.csproj create mode 100644 Godot/Packages/ShrinkSDK.Context.AppAdapter/Runtime/ShrinkContextAppHost.cs create mode 100644 Godot/Packages/ShrinkSDK.Context.AppAdapter/ShrinkSDK.Context.AppAdapter.csproj create mode 100644 Godot/Packages/ShrinkSDK.Context.Core/ShrinkSDK.Context.Core.csproj create mode 100644 Godot/Packages/ShrinkSDK.DataSaver.Integration.EventBus/ShrinkSDK.DataSaver.Integration.EventBus.csproj create mode 100644 Godot/Packages/ShrinkSDK.DataSaver/Runtime/ShrinkDataSaverRuntime.cs create mode 100644 Godot/Packages/ShrinkSDK.DataSaver/ShrinkSDK.DataSaver.csproj create mode 100644 Godot/Packages/ShrinkSDK.EventBus/ShrinkSDK.EventBus.csproj create mode 100644 Godot/Packages/ShrinkSDK.Godot/ShrinkGodotHost.cs create mode 100644 Godot/Packages/ShrinkSDK.Godot/ShrinkSDK.Godot.csproj create mode 100644 Godot/Packages/ShrinkSDK.ModFramework.Godot/ShrinkGodotModLoader.cs create mode 100644 Godot/Packages/ShrinkSDK.ModFramework.Godot/ShrinkSDK.ModFramework.Godot.csproj create mode 100644 Godot/Packages/ShrinkSDK.ModFramework/Runtime/ShrinkModContext.cs create mode 100644 Godot/Packages/ShrinkSDK.ModFramework/ShrinkSDK.ModFramework.csproj create mode 100644 Godot/Packages/ShrinkSDK.Network.Integration.EventBus/ShrinkSDK.Network.Integration.EventBus.csproj create mode 100644 Godot/Packages/ShrinkSDK.Network/ShrinkSDK.Network.csproj create mode 100644 Godot/Packages/ShrinkSDK.Runtime.Abstractions/ShrinkSDK.Runtime.Abstractions.csproj create mode 100644 Godot/Packages/ShrinkSDK.Tutorial.Godot/ShrinkGodotTutorialOverlay.cs create mode 100644 Godot/Packages/ShrinkSDK.Tutorial.Godot/ShrinkSDK.Tutorial.Godot.csproj create mode 100644 Godot/Packages/ShrinkSDK.Tutorial/Runtime/ShrinkTutorialCore.cs create mode 100644 Godot/Packages/ShrinkSDK.Tutorial/ShrinkSDK.Tutorial.csproj create mode 100644 Godot/Samples/ShrinkSDK.Godot.Sample/Main.tscn create mode 100644 Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs create mode 100644 Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs.uid create mode 100644 Godot/Samples/ShrinkSDK.Godot.Sample/ShrinkSDK.Godot.Sample.csproj create mode 100644 Godot/Samples/ShrinkSDK.Godot.Sample/ShrinkSDK.Godot.Sample.sln create mode 100644 Godot/Samples/ShrinkSDK.Godot.Sample/Tutorial.tscn create mode 100644 Godot/Samples/ShrinkSDK.Godot.Sample/TutorialDemo.cs create mode 100644 Godot/Samples/ShrinkSDK.Godot.Sample/TutorialDemo.cs.uid create mode 100644 Godot/Samples/ShrinkSDK.Godot.Sample/export_presets.cfg create mode 100644 Godot/Samples/ShrinkSDK.Godot.Sample/project.godot create mode 100644 Godot/ShrinkSDK.Godot.slnx create mode 100644 Godot/Tests/CodeGenFixture/CodeGenFixture.csproj create mode 100644 Godot/Tests/CodeGenFixture/Program.cs create mode 100644 Godot/Tests/CodeGenValidation/CodeGenValidation.csproj create mode 100644 Godot/Tests/CodeGenValidation/Program.cs create mode 100644 Godot/Tests/InstallerFixture/InstallerFixture.csproj create mode 100644 Godot/Tests/InstallerFixture/Program.cs create mode 100644 Godot/Tests/InvalidCodeGenFixture/DuplicateContracts.cs create mode 100644 Godot/Tests/InvalidCodeGenFixture/InvalidCodeGenFixture.csproj create mode 100644 Godot/Tests/ModFixture/FixtureMod.cs create mode 100644 Godot/Tests/ModFixture/ModFixture.csproj create mode 100644 Godot/Tests/ModHostFixture/ModHostFixture.csproj create mode 100644 Godot/Tests/ModHostFixture/Program.cs create mode 100644 Godot/Tests/NuGetConsumer/NuGetConsumer.csproj create mode 100644 Godot/Tests/NuGetConsumer/Program.cs create mode 100644 Godot/Tests/UnityAdapterCompile/UnityAdapterCompile.csproj diff --git a/Assets/Modules/ShrinkApp.Core b/Assets/Modules/ShrinkApp.Core index eac1fa5..a5c5a70 160000 --- a/Assets/Modules/ShrinkApp.Core +++ b/Assets/Modules/ShrinkApp.Core @@ -1 +1 @@ -Subproject commit eac1fa595bf9f2289aabc76dd90a6f98baa28eb5 +Subproject commit a5c5a700754a317308aedffd6c7576b41f113685 diff --git a/Assets/Modules/ShrinkApp.Starter.Basic b/Assets/Modules/ShrinkApp.Starter.Basic index 85b4b59..f8906fa 160000 --- a/Assets/Modules/ShrinkApp.Starter.Basic +++ b/Assets/Modules/ShrinkApp.Starter.Basic @@ -1 +1 @@ -Subproject commit 85b4b591aec44b8e97d6bfef071e1c1943123c2b +Subproject commit f8906faaec430a593656ccc541bb673a54c133d0 diff --git a/Assets/Modules/ShrinkCommand b/Assets/Modules/ShrinkCommand index 438cc27..4ed7a91 160000 --- a/Assets/Modules/ShrinkCommand +++ b/Assets/Modules/ShrinkCommand @@ -1 +1 @@ -Subproject commit 438cc27213cfb962d3816042c1623ad9e2b5d861 +Subproject commit 4ed7a91c79bdcc0d53155d7410472deef6e3b1dc diff --git a/Assets/Modules/ShrinkCommand.Integration.App b/Assets/Modules/ShrinkCommand.Integration.App index ef07791..57246d5 160000 --- a/Assets/Modules/ShrinkCommand.Integration.App +++ b/Assets/Modules/ShrinkCommand.Integration.App @@ -1 +1 @@ -Subproject commit ef07791e2c75944342997661cfc2aa6c9dfd9d04 +Subproject commit 57246d51949636d5f7e7e19d08676de94d06192f diff --git a/Assets/Modules/ShrinkCommand.Integration.EventBus b/Assets/Modules/ShrinkCommand.Integration.EventBus index d94eeaf..53720a3 160000 --- a/Assets/Modules/ShrinkCommand.Integration.EventBus +++ b/Assets/Modules/ShrinkCommand.Integration.EventBus @@ -1 +1 @@ -Subproject commit d94eeaf0e82159d4bfeb968a23539853b99f46cb +Subproject commit 53720a3b717d1978f18582f32a99fa90eb9f75b5 diff --git a/Assets/Modules/ShrinkCommand.Integration.Network b/Assets/Modules/ShrinkCommand.Integration.Network index 2c955bc..bf31561 160000 --- a/Assets/Modules/ShrinkCommand.Integration.Network +++ b/Assets/Modules/ShrinkCommand.Integration.Network @@ -1 +1 @@ -Subproject commit 2c955bcffa95b73ae22f4d64cf332b98205a9dde +Subproject commit bf315619f080e0949361041900e9ee17251d3d4a diff --git a/Assets/Modules/ShrinkContext.AppAdapter b/Assets/Modules/ShrinkContext.AppAdapter index 0d1a7d0..8b3121b 160000 --- a/Assets/Modules/ShrinkContext.AppAdapter +++ b/Assets/Modules/ShrinkContext.AppAdapter @@ -1 +1 @@ -Subproject commit 0d1a7d0fb5cddd96da7c941544ae37b8c7756850 +Subproject commit 8b3121bc21798ff39503366653e57f534e40c6ca diff --git a/Assets/Modules/ShrinkContext.Core b/Assets/Modules/ShrinkContext.Core index 02a0ece..a4c78ea 160000 --- a/Assets/Modules/ShrinkContext.Core +++ b/Assets/Modules/ShrinkContext.Core @@ -1 +1 @@ -Subproject commit 02a0ece826630cda775c6a39b19485fba6152740 +Subproject commit a4c78ea7a71470bcb5234b4b4cbfe705a3d18128 diff --git a/Assets/Modules/ShrinkDataSaver b/Assets/Modules/ShrinkDataSaver index 1bfb99d..0030f5b 160000 --- a/Assets/Modules/ShrinkDataSaver +++ b/Assets/Modules/ShrinkDataSaver @@ -1 +1 @@ -Subproject commit 1bfb99d4c3767894276ffb68797d8a446444139c +Subproject commit 0030f5b794dc2f6730f193579acac861a852c5d7 diff --git a/Assets/Modules/ShrinkDataSaver.Integration.App b/Assets/Modules/ShrinkDataSaver.Integration.App index 4c83b6e..e7da032 160000 --- a/Assets/Modules/ShrinkDataSaver.Integration.App +++ b/Assets/Modules/ShrinkDataSaver.Integration.App @@ -1 +1 @@ -Subproject commit 4c83b6ead66e32636125f3df23cd5d4c9cc7b553 +Subproject commit e7da03273edf1af4eb8e2f5c883541c28f5ee61d diff --git a/Assets/Modules/ShrinkDataSaver.Integration.EventBus b/Assets/Modules/ShrinkDataSaver.Integration.EventBus index 66c7065..599b8e3 160000 --- a/Assets/Modules/ShrinkDataSaver.Integration.EventBus +++ b/Assets/Modules/ShrinkDataSaver.Integration.EventBus @@ -1 +1 @@ -Subproject commit 66c70653b19a1e1f914b66f92a01e07f715ef362 +Subproject commit 599b8e34c6975cff4807cd182128205940d03299 diff --git a/Assets/Modules/ShrinkEventBus b/Assets/Modules/ShrinkEventBus index a6011cd..8aa4c79 160000 --- a/Assets/Modules/ShrinkEventBus +++ b/Assets/Modules/ShrinkEventBus @@ -1 +1 @@ -Subproject commit a6011cd01d10eaa43c08eab790e796ecb6306259 +Subproject commit 8aa4c796043651fa6641804d6ae620d954e2610d diff --git a/Assets/Modules/ShrinkInstaller b/Assets/Modules/ShrinkInstaller index 012728f..2abf367 160000 --- a/Assets/Modules/ShrinkInstaller +++ b/Assets/Modules/ShrinkInstaller @@ -1 +1 @@ -Subproject commit 012728ffacb02707dd06a87e614274ddd999c344 +Subproject commit 2abf367f368b58a2bc4f3c0a49cdb97e486f4bb2 diff --git a/Assets/Modules/ShrinkModFramework b/Assets/Modules/ShrinkModFramework index 9755e18..fea6ec5 160000 --- a/Assets/Modules/ShrinkModFramework +++ b/Assets/Modules/ShrinkModFramework @@ -1 +1 @@ -Subproject commit 9755e180874a3aff074a0050d33d3add5f599a4c +Subproject commit fea6ec56fcef6cd72d67ba3948deca384c5c8145 diff --git a/Assets/Modules/ShrinkNetwork b/Assets/Modules/ShrinkNetwork index dd13a01..26241cb 160000 --- a/Assets/Modules/ShrinkNetwork +++ b/Assets/Modules/ShrinkNetwork @@ -1 +1 @@ -Subproject commit dd13a01bc94dbadae87aa40fda3a2d41e2455ff8 +Subproject commit 26241cb5347a33d30da4dce246eff29b9778fe4f diff --git a/Assets/Modules/ShrinkNetwork.Integration.App b/Assets/Modules/ShrinkNetwork.Integration.App index 68e96ed..2cd552e 160000 --- a/Assets/Modules/ShrinkNetwork.Integration.App +++ b/Assets/Modules/ShrinkNetwork.Integration.App @@ -1 +1 @@ -Subproject commit 68e96edc104cb69bbf9053ae9daf70d71d86107b +Subproject commit 2cd552ef2571aebe3f7cd8ee5c2af8316aa9c66e diff --git a/Assets/Modules/ShrinkNetwork.Integration.EventBus b/Assets/Modules/ShrinkNetwork.Integration.EventBus index 005ebf4..eb7fa55 160000 --- a/Assets/Modules/ShrinkNetwork.Integration.EventBus +++ b/Assets/Modules/ShrinkNetwork.Integration.EventBus @@ -1 +1 @@ -Subproject commit 005ebf43cff16ce4c465af7d90ed4f1531df4253 +Subproject commit eb7fa55656ac954cd3ab528aac384a4a12261b53 diff --git a/Assets/Modules/ShrinkRuntime.Abstractions.meta b/Assets/Modules/ShrinkRuntime.Abstractions.meta new file mode 100644 index 0000000..4069f9b --- /dev/null +++ b/Assets/Modules/ShrinkRuntime.Abstractions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 10936b7d408f403449df0946277a2d00 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkRuntime.Abstractions/Runtime.meta b/Assets/Modules/ShrinkRuntime.Abstractions/Runtime.meta new file mode 100644 index 0000000..a4cfb82 --- /dev/null +++ b/Assets/Modules/ShrinkRuntime.Abstractions/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fdcff332146fc1d439fc0c82285637da +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntime.Abstractions.asmdef b/Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntime.Abstractions.asmdef new file mode 100644 index 0000000..e191c12 --- /dev/null +++ b/Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntime.Abstractions.asmdef @@ -0,0 +1,5 @@ +{ + "name": "ShrinkRuntime.Abstractions", + "rootNamespace": "ShrinkSDK.Runtime", + "autoReferenced": true +} diff --git a/Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntime.Abstractions.asmdef.meta b/Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntime.Abstractions.asmdef.meta new file mode 100644 index 0000000..8cd5fd9 --- /dev/null +++ b/Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntime.Abstractions.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9285a23e48a9fd04ea5d3a7507010d2b +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntimeServices.cs b/Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntimeServices.cs new file mode 100644 index 0000000..afcb260 --- /dev/null +++ b/Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntimeServices.cs @@ -0,0 +1,127 @@ +#nullable enable + +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace ShrinkSDK.Runtime +{ + public enum ShrinkLogLevel { Trace, Debug, Information, Warning, Error } + + public interface IShrinkLogger + { + void Log(ShrinkLogLevel level, string message, Exception? exception = null); + } + + public interface IShrinkMainThreadDispatcher + { + bool IsMainThread { get; } + bool TryPost(Action action); + } + + public interface IShrinkClock + { + DateTimeOffset UtcNow { get; } + double UnscaledTimeSeconds { get; } + } + + public interface IShrinkPathProvider + { + string PersistentDataPath { get; } + string TemporaryDataPath { get; } + } + + public interface IShrinkScreenshotProvider + { + ValueTask CapturePngAsync(CancellationToken cancellationToken = default); + } + + public interface IShrinkApplicationLifecycle + { + event Action? Paused; + event Action? Resumed; + event Action? Exiting; + } + + public static class ShrinkLoggerExtensions + { + public static void Error(this IShrinkLogger logger, string message, Exception? exception = null) => + logger.Log(ShrinkLogLevel.Error, message, exception); + } + + public static class ShrinkRuntimeServices + { + private static IShrinkLogger _logger = new ConsoleLogger(); + private static IShrinkMainThreadDispatcher _dispatcher = new InlineDispatcher(); + private static IShrinkClock _clock = new SystemClock(); + private static IShrinkPathProvider _paths = new SystemPaths(); + + public static IShrinkLogger Logger => _logger; + public static IShrinkMainThreadDispatcher Dispatcher => _dispatcher; + public static IShrinkClock Clock => _clock; + public static IShrinkPathProvider Paths => _paths; + public static IShrinkScreenshotProvider? Screenshots { get; private set; } + public static IShrinkApplicationLifecycle? Lifecycle { get; private set; } + + public static void Configure(IShrinkLogger logger, IShrinkMainThreadDispatcher dispatcher, + IShrinkClock clock, IShrinkPathProvider paths, + IShrinkScreenshotProvider? screenshots = null, IShrinkApplicationLifecycle? lifecycle = null) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + _clock = clock ?? throw new ArgumentNullException(nameof(clock)); + _paths = paths ?? throw new ArgumentNullException(nameof(paths)); + Screenshots = screenshots; + Lifecycle = lifecycle; + } + + private sealed class ConsoleLogger : IShrinkLogger + { + public void Log(ShrinkLogLevel level, string message, Exception? exception = null) + { + Console.WriteLine($"[{level}] {message}"); + if (exception != null) Console.WriteLine(exception); + } + } + + private sealed class InlineDispatcher : IShrinkMainThreadDispatcher + { + public bool IsMainThread => true; + public bool TryPost(Action action) + { + if (action == null) return false; + action(); + return true; + } + } + + private sealed class SystemClock : IShrinkClock + { + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; + public double UnscaledTimeSeconds => _stopwatch.Elapsed.TotalSeconds; + } + + private sealed class SystemPaths : IShrinkPathProvider + { + public string PersistentDataPath => Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ShrinkSDK"); + public string TemporaryDataPath => Path.GetTempPath(); + } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] + public sealed class ShrinkCodeGenWovenAttribute : Attribute + { + public ShrinkCodeGenWovenAttribute(string weaverVersion, string inputMvid) + { + WeaverVersion = weaverVersion; + InputMvid = inputMvid; + } + + public string WeaverVersion { get; } + public string InputMvid { get; } + } +} diff --git a/Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntimeServices.cs.meta b/Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntimeServices.cs.meta new file mode 100644 index 0000000..d660e28 --- /dev/null +++ b/Assets/Modules/ShrinkRuntime.Abstractions/Runtime/ShrinkRuntimeServices.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f9b07df5ab1dc345be31f33e94077d2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkRuntime.Abstractions/package.json b/Assets/Modules/ShrinkRuntime.Abstractions/package.json new file mode 100644 index 0000000..50f75d9 --- /dev/null +++ b/Assets/Modules/ShrinkRuntime.Abstractions/package.json @@ -0,0 +1,7 @@ +{ + "name": "com.cneicy.shrink-runtime-abstractions", + "version": "0.1.0", + "displayName": "ShrinkSDK Runtime Abstractions", + "description": "Engine-neutral runtime services and CodeGen marker contracts.", + "unity": "2022.3" +} diff --git a/Assets/Modules/ShrinkRuntime.Abstractions/package.json.meta b/Assets/Modules/ShrinkRuntime.Abstractions/package.json.meta new file mode 100644 index 0000000..8d8096e --- /dev/null +++ b/Assets/Modules/ShrinkRuntime.Abstractions/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2f07481a8e08f454fa1d85c5afe4c92d +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkShared.CodeGen b/Assets/Modules/ShrinkShared.CodeGen index bfb463e..5fd9b25 160000 --- a/Assets/Modules/ShrinkShared.CodeGen +++ b/Assets/Modules/ShrinkShared.CodeGen @@ -1 +1 @@ -Subproject commit bfb463e4bc0b71d9f1d229e0c23764eeace7bd3d +Subproject commit 5fd9b258d78a4eccff03b1da12ffd9f9a62526a4 diff --git a/Assets/Modules/ShrinkTutorial b/Assets/Modules/ShrinkTutorial index 46ed178..5d66389 160000 --- a/Assets/Modules/ShrinkTutorial +++ b/Assets/Modules/ShrinkTutorial @@ -1 +1 @@ -Subproject commit 46ed178e0b63cd23bcdd9619f1e4c52885b217e3 +Subproject commit 5d66389bd6c7e3a2e4cd7b1893c14c92a0deeddc diff --git a/Godot/.gitignore b/Godot/.gitignore new file mode 100644 index 0000000..5a0a67c --- /dev/null +++ b/Godot/.gitignore @@ -0,0 +1,7 @@ +**/bin/ +**/obj/ +**/.godot/ +Artifacts/ +Exports/ +!*.csproj +!*.sln diff --git a/Godot/CodeGen/ShrinkSDK.CodeGen.Analyzers/AnalyzerReleases.Shipped.md b/Godot/CodeGen/ShrinkSDK.CodeGen.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..332da7f --- /dev/null +++ b/Godot/CodeGen/ShrinkSDK.CodeGen.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,9 @@ +## Release 0.1.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +SHRINK001 | ShrinkSDK.CodeGen | Error | Invalid event subscriber signature +SHRINK002 | ShrinkSDK.CodeGen | Error | Duplicate network opcode and route +SHRINK003 | ShrinkSDK.CodeGen | Error | Invalid application installer diff --git a/Godot/CodeGen/ShrinkSDK.CodeGen.Analyzers/ShrinkCodeGenAnalyzer.cs b/Godot/CodeGen/ShrinkSDK.CodeGen.Analyzers/ShrinkCodeGenAnalyzer.cs new file mode 100644 index 0000000..64ae6e9 --- /dev/null +++ b/Godot/CodeGen/ShrinkSDK.CodeGen.Analyzers/ShrinkCodeGenAnalyzer.cs @@ -0,0 +1,75 @@ +#nullable enable + +using System; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace ShrinkSDK.CodeGen.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ShrinkCodeGenAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor InvalidEventHandler = new( + "SHRINK001", "Invalid event subscriber signature", + "Method '{0}' must use void(TEvent), UniTask(TEvent), or UniTask(TEvent, CancellationToken)", + "ShrinkSDK.CodeGen", DiagnosticSeverity.Error, true); + private static readonly DiagnosticDescriptor DuplicateNetworkContract = new( + "SHRINK002", "Duplicate network opcode and route", + "Network opcode/route '{0}' is already declared by '{1}'", + "ShrinkSDK.CodeGen", DiagnosticSeverity.Error, true); + private static readonly DiagnosticDescriptor InvalidInstaller = new( + "SHRINK003", "Invalid application installer", + "Type '{0}' has ShrinkAppModuleInstaller but does not implement IShrinkAppModuleInstaller", + "ShrinkSDK.CodeGen", DiagnosticSeverity.Error, true); + + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(InvalidEventHandler, DuplicateNetworkContract, InvalidInstaller); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(start => + { + var networkContracts = new ConcurrentDictionary(StringComparer.Ordinal); + start.RegisterSymbolAction(symbolContext => AnalyzeMethod(symbolContext), SymbolKind.Method); + start.RegisterSymbolAction(symbolContext => AnalyzeType(symbolContext, networkContracts), SymbolKind.NamedType); + }); + } + + private static void AnalyzeMethod(SymbolAnalysisContext context) + { + var method = (IMethodSymbol)context.Symbol; + if (!HasAttribute(method, "ShrinkEventBus.ShrinkSubscribeAttribute")) return; + var parametersValid = method.Parameters.Length == 1 || + method.Parameters.Length == 2 && method.Parameters[1].Type.ToDisplayString() == "System.Threading.CancellationToken"; + var returnName = method.ReturnType.ToDisplayString(); + var returnValid = method.Parameters.Length == 1 && (method.ReturnsVoid || returnName == "Cysharp.Threading.Tasks.UniTask") || + method.Parameters.Length == 2 && returnName == "Cysharp.Threading.Tasks.UniTask"; + if (!parametersValid || !returnValid) + context.ReportDiagnostic(Diagnostic.Create(InvalidEventHandler, method.Locations.FirstOrDefault(), method.Name)); + } + + private static void AnalyzeType(SymbolAnalysisContext context, ConcurrentDictionary networkContracts) + { + var type = (INamedTypeSymbol)context.Symbol; + if (HasAttribute(type, "ShrinkApp.ShrinkAppModuleInstallerAttribute") && + !type.AllInterfaces.Any(item => item.ToDisplayString() == "ShrinkApp.IShrinkAppModuleInstaller")) + context.ReportDiagnostic(Diagnostic.Create(InvalidInstaller, type.Locations.FirstOrDefault(), type.Name)); + + var network = type.GetAttributes().FirstOrDefault(attribute => + attribute.AttributeClass?.ToDisplayString() == "ShrinkNetwork.ShrinkNetworkMessageAttribute"); + if (network == null || network.ConstructorArguments.Length == 0) return; + var opcode = network.ConstructorArguments[0].Value?.ToString() ?? string.Empty; + var route = network.ConstructorArguments.Length > 1 ? network.ConstructorArguments[1].Value as string ?? string.Empty : string.Empty; + var key = opcode + ":" + route; + if (!networkContracts.TryAdd(key, type) && networkContracts.TryGetValue(key, out var previous)) + context.ReportDiagnostic(Diagnostic.Create(DuplicateNetworkContract, type.Locations.FirstOrDefault(), key, previous.Name)); + } + + private static bool HasAttribute(ISymbol symbol, string fullName) => + symbol.GetAttributes().Any(attribute => attribute.AttributeClass?.ToDisplayString() == fullName); +} diff --git a/Godot/CodeGen/ShrinkSDK.CodeGen.Analyzers/ShrinkSDK.CodeGen.Analyzers.csproj b/Godot/CodeGen/ShrinkSDK.CodeGen.Analyzers/ShrinkSDK.CodeGen.Analyzers.csproj new file mode 100644 index 0000000..78c53ff --- /dev/null +++ b/Godot/CodeGen/ShrinkSDK.CodeGen.Analyzers/ShrinkSDK.CodeGen.Analyzers.csproj @@ -0,0 +1,13 @@ + + + netstandard2.0 + ShrinkSDK.CodeGen.Analyzers + ShrinkSDK.CodeGen.Analyzers + false + false + true + + + + + diff --git a/Godot/CodeGen/ShrinkSDK.CodeGen.Core/ShrinkSDK.CodeGen.Core.csproj b/Godot/CodeGen/ShrinkSDK.CodeGen.Core/ShrinkSDK.CodeGen.Core.csproj new file mode 100644 index 0000000..6656b11 --- /dev/null +++ b/Godot/CodeGen/ShrinkSDK.CodeGen.Core/ShrinkSDK.CodeGen.Core.csproj @@ -0,0 +1,15 @@ + + + net8.0 + ShrinkSDK.CodeGen.Core + ShrinkSDK.CodeGen + ShrinkSDK.CodeGen.Core + 0.1.0 + true + false + + + + + + diff --git a/Godot/CodeGen/ShrinkSDK.CodeGen.Task/ShrinkSDK.CodeGen.Task.csproj b/Godot/CodeGen/ShrinkSDK.CodeGen.Task/ShrinkSDK.CodeGen.Task.csproj new file mode 100644 index 0000000..d89da22 --- /dev/null +++ b/Godot/CodeGen/ShrinkSDK.CodeGen.Task/ShrinkSDK.CodeGen.Task.csproj @@ -0,0 +1,34 @@ + + + net8.0 + ShrinkSDK.CodeGen.Task + ShrinkSDK.CodeGen + ShrinkSDK.CodeGen + 0.1.0 + MSBuild integration for ShrinkSDK Cecil weaving. + true + tools + true + true + $(TargetsForTfmSpecificBuildOutput);IncludeShrinkCodeGenDependencies + + + + + + + + + + + + + + + + + + + + diff --git a/Godot/CodeGen/ShrinkSDK.CodeGen.Task/ShrinkWeaveTask.cs b/Godot/CodeGen/ShrinkSDK.CodeGen.Task/ShrinkWeaveTask.cs new file mode 100644 index 0000000..a7a8639 --- /dev/null +++ b/Godot/CodeGen/ShrinkSDK.CodeGen.Task/ShrinkWeaveTask.cs @@ -0,0 +1,98 @@ +#nullable enable + +using System; +using System.IO; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace ShrinkSDK.CodeGen; + +public sealed class ShrinkWeaveTask : Task +{ + [Required] + public string AssemblyPath { get; set; } = string.Empty; + + public string? PdbPath { get; set; } + public ITaskItem[] ReferencePaths { get; set; } = Array.Empty(); + public string? StrongNameKeyFile { get; set; } + + [Output] public bool Changed { get; private set; } + [Output] public int InstanceSubscriberCount { get; private set; } + [Output] public int StaticSubscriberCount { get; private set; } + [Output] public int RegistryEntryCount { get; private set; } + + public override bool Execute() + { + if (!File.Exists(AssemblyPath)) + { + Log.LogError($"[ShrinkSDK.CodeGen] Intermediate assembly does not exist: {AssemblyPath}"); + return false; + } + + var assemblyDirectory = Path.GetDirectoryName(Path.GetFullPath(AssemblyPath))!; + var temporaryAssembly = Path.Combine(assemblyDirectory, $".{Path.GetFileName(AssemblyPath)}.{Guid.NewGuid():N}.tmp"); + var hasPdb = !string.IsNullOrWhiteSpace(PdbPath) && File.Exists(PdbPath); + var temporaryPdb = hasPdb + ? Path.Combine(assemblyDirectory, $".{Path.GetFileName(PdbPath)}.{Guid.NewGuid():N}.tmp") + : null; + try + { + var result = ShrinkAssemblyWeaver.Weave( + AssemblyPath, + hasPdb ? PdbPath : null, + ReferencePaths.Select(item => item.ItemSpec), + temporaryAssembly, + temporaryPdb, + ShrinkCodeGenPlatform.EngineNeutral, + StrongNameKeyFile); + foreach (var diagnostic in result.Diagnostics) + { + switch (diagnostic.Severity) + { + case ShrinkCodeGenDiagnosticSeverity.Warning: + Log.LogWarning($"[ShrinkSDK.CodeGen] {diagnostic.Message}"); + break; + case ShrinkCodeGenDiagnosticSeverity.Error: + Log.LogError($"[ShrinkSDK.CodeGen] {diagnostic.Message}"); + break; + default: + Log.LogMessage(MessageImportance.Low, $"[ShrinkSDK.CodeGen] {diagnostic.Message}"); + break; + } + } + if (!result.Succeeded) + return false; + + Changed = result.Changed; + InstanceSubscriberCount = result.InstanceSubscribers; + StaticSubscriberCount = result.StaticSubscribers; + RegistryEntryCount = result.RegistryEntries; + if (Changed) + { + File.Move(temporaryAssembly, AssemblyPath, true); + if (hasPdb && temporaryPdb != null) + File.Move(temporaryPdb, PdbPath!, true); + } + Log.LogMessage(MessageImportance.High, + $"[ShrinkSDK.CodeGen] {Path.GetFileName(AssemblyPath)} woven: instance={InstanceSubscriberCount}, static={StaticSubscriberCount}, registry={RegistryEntryCount}."); + return true; + } + catch (Exception exception) + { + Log.LogErrorFromException(exception, true); + return false; + } + finally + { + TryDelete(temporaryAssembly); + if (temporaryPdb != null) TryDelete(temporaryPdb); + } + } + + private static void TryDelete(string path) + { + try { if (File.Exists(path)) File.Delete(path); } + catch { } + } +} diff --git a/Godot/CodeGen/ShrinkSDK.CodeGen.Task/buildTransitive/ShrinkSDK.CodeGen.props b/Godot/CodeGen/ShrinkSDK.CodeGen.Task/buildTransitive/ShrinkSDK.CodeGen.props new file mode 100644 index 0000000..54942f1 --- /dev/null +++ b/Godot/CodeGen/ShrinkSDK.CodeGen.Task/buildTransitive/ShrinkSDK.CodeGen.props @@ -0,0 +1,5 @@ + + + true + + diff --git a/Godot/CodeGen/ShrinkSDK.CodeGen.Task/buildTransitive/ShrinkSDK.CodeGen.targets b/Godot/CodeGen/ShrinkSDK.CodeGen.Task/buildTransitive/ShrinkSDK.CodeGen.targets new file mode 100644 index 0000000..307f232 --- /dev/null +++ b/Godot/CodeGen/ShrinkSDK.CodeGen.Task/buildTransitive/ShrinkSDK.CodeGen.targets @@ -0,0 +1,18 @@ + + + <_ShrinkCodeGenTaskAssembly Condition="'$(_ShrinkCodeGenTaskAssembly)' == ''">$(MSBuildThisFileDirectory)..\tools\net8.0\ShrinkSDK.CodeGen.Task.dll + + + + + <_ShrinkCodeGenIntermediateAssembly>$(IntermediateOutputPath)$(TargetName).dll + + + + diff --git a/Godot/Directory.Build.props b/Godot/Directory.Build.props new file mode 100644 index 0000000..304f5dd --- /dev/null +++ b/Godot/Directory.Build.props @@ -0,0 +1,13 @@ + + + latest + enable + true + true + https://git.crash.work/ShrinkSDK/Workspace + ShrinkSDK + ShrinkSDK + true + snupkg + + diff --git a/Godot/Installer/ShrinkSDK.Godot.Installer.csproj b/Godot/Installer/ShrinkSDK.Godot.Installer.csproj new file mode 100644 index 0000000..a82c48f --- /dev/null +++ b/Godot/Installer/ShrinkSDK.Godot.Installer.csproj @@ -0,0 +1,19 @@ + + + net8.0 + false + ShrinkSDK.Godot.Installer + ShrinkSDK.Godot.Installer + 0.1.1 + Godot Editor package manager and CodeGen diagnostics for ShrinkSDK. + + + + + + + + + + + diff --git a/Godot/Installer/addons/shrinksdk/ShrinkGodotInstallerPlugin.cs b/Godot/Installer/addons/shrinksdk/ShrinkGodotInstallerPlugin.cs new file mode 100644 index 0000000..ec1cc0a --- /dev/null +++ b/Godot/Installer/addons/shrinksdk/ShrinkGodotInstallerPlugin.cs @@ -0,0 +1,133 @@ +#nullable enable + +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Godot; +using ShrinkSDK.Installer; + +namespace ShrinkSDK.Godot.Editor; + +[Tool] +public partial class ShrinkGodotInstallerPlugin : EditorPlugin +{ + private EditorDock? _panel; + private Label? _status; + private RichTextLabel? _output; + private readonly System.Collections.Generic.Dictionary _installActions = + new(StringComparer.OrdinalIgnoreCase); + private readonly System.Collections.Generic.Dictionary _removeActions = + new(StringComparer.OrdinalIgnoreCase); + private string ProjectDirectory => ProjectSettings.GlobalizePath("res://"); + private string ProjectPath => Directory.GetFiles(ProjectDirectory, "*.csproj", SearchOption.TopDirectoryOnly).Single(); + + public override void _EnterTree() + { + _panel = new EditorDock { Name = "ShrinkSDK", Title = "ShrinkSDK", DefaultSlot = EditorDock.DockSlot.LeftBr }; + var content = new VBoxContainer(); + _panel.AddChild(content); + content.AddChild(new Label { Text = "ShrinkSDK Packages" }); + _installActions.Clear(); + _removeActions.Clear(); + foreach (var package in ShrinkProjectPackageEditor.RecommendedPackages) + { + var row = new HBoxContainer(); + row.AddChild(new Label { Text = $"{package.Key} {package.Value}", SizeFlagsHorizontal = Control.SizeFlags.ExpandFill }); + var install = new Button { Text = "Install / Update" }; + install.Pressed += () => ChangePackage(package.Key, package.Value); + row.AddChild(install); + _installActions[package.Key] = install; + var remove = new Button { Text = "Remove" }; + remove.Pressed += () => ChangePackage(package.Key, null); + row.AddChild(remove); + _removeActions[package.Key] = remove; + content.AddChild(row); + } + var restore = new Button { Text = "Restore and Build" }; + restore.Pressed += BuildProject; + content.AddChild(restore); + _status = new Label { Text = "CodeGen: not built" }; + content.AddChild(_status); + _output = new RichTextLabel { FitContent = true, CustomMinimumSize = new Vector2(360, 180) }; + content.AddChild(_output); + AddDock(_panel); + RefreshInstalledState(); + } + + public override void _ExitTree() + { + if (_panel == null) return; + RemoveDock(_panel); + _panel.QueueFree(); + _panel = null; + } + + private async void ChangePackage(string packageId, string? version) + { + try + { + ShrinkProjectPackageEditor.EnsureShrinkFeed(Path.Combine(ProjectDirectory, "NuGet.Config")); + ShrinkProjectPackageEditor.SetPackageReference(ProjectPath, packageId, version); + await RunBuild("restore"); + } + catch (Exception exception) { ShowFailure(exception); } + } + + private async void BuildProject() + { + try { await RunBuild("build"); } + catch (Exception exception) { ShowFailure(exception); } + } + + private async System.Threading.Tasks.Task RunBuild(string command) + { + _status!.Text = $"CodeGen: running dotnet {command}"; + var result = await ShrinkProjectPackageEditor.RunDotnetAsync(ProjectDirectory, $"{command} \"{ProjectPath}\" /nr:false"); + _output!.Text = result.Output; + var match = Regex.Matches(result.Output, @"\[ShrinkSDK\.CodeGen\].*woven: instance=(\d+), static=(\d+), registry=(\d+)").Cast().LastOrDefault(); + _status.Text = result.ExitCode == 0 + ? match == null ? "CodeGen: build succeeded (no attributed registrations)" : $"CodeGen: woven; instance={match.Groups[1].Value}, static={match.Groups[2].Value}, registry={match.Groups[3].Value}" + : "CodeGen: build failed"; + RefreshInstalledState(); + } + + private void RefreshInstalledState() + { + if (_output == null) return; + var installed = File.Exists(ProjectPath) + ? ShrinkProjectPackageEditor.ReadPackageReferences(ProjectPath) + : new System.Collections.Generic.Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var package in ShrinkProjectPackageEditor.RecommendedPackages) + { + var hasInstalled = installed.TryGetValue(package.Key, out var installedVersion); + var relation = hasInstalled + ? ShrinkPackageVersion.Classify(installedVersion, package.Value) + : ShrinkPackageVersionRelation.Unknown; + if (_installActions.TryGetValue(package.Key, out var install)) + { + install.Text = !hasInstalled + ? $"Install {package.Value}" + : relation switch + { + ShrinkPackageVersionRelation.Equal => $"Installed {installedVersion}", + ShrinkPackageVersionRelation.Newer => $"Newer {installedVersion}", + ShrinkPackageVersionRelation.Older => $"Upgrade {installedVersion} → {package.Value}", + _ => $"Update {installedVersion} → {package.Value}" + }; + install.Disabled = hasInstalled && relation is + ShrinkPackageVersionRelation.Equal or ShrinkPackageVersionRelation.Newer; + } + if (_removeActions.TryGetValue(package.Key, out var remove)) + remove.Disabled = !hasInstalled; + } + _output.Text = "Direct packages: " + string.Join(", ", installed.Select(item => $"{item.Key}@{item.Value}")); + } + + private void ShowFailure(Exception exception) + { + _status!.Text = "CodeGen: operation failed"; + _output!.Text = exception.ToString(); + GD.PushError(exception.ToString()); + } +} diff --git a/Godot/Installer/addons/shrinksdk/ShrinkProjectPackageEditor.cs b/Godot/Installer/addons/shrinksdk/ShrinkProjectPackageEditor.cs new file mode 100644 index 0000000..7397f76 --- /dev/null +++ b/Godot/Installer/addons/shrinksdk/ShrinkProjectPackageEditor.cs @@ -0,0 +1,148 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace ShrinkSDK.Godot.Editor; + +public static class ShrinkProjectPackageEditor +{ + public static readonly IReadOnlyDictionary RecommendedPackages = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["ShrinkSDK.Godot"] = "0.1.0", + ["ShrinkSDK.EventBus"] = "2.1.0", + ["ShrinkSDK.Context.Core"] = "0.2.0", + ["ShrinkSDK.Command"] = "0.3.0", + ["ShrinkSDK.Command.Integration.EventBus"] = "0.2.0", + ["ShrinkSDK.Command.Integration.Network"] = "0.2.0", + ["ShrinkSDK.Command.Integration.App"] = "0.2.0", + ["ShrinkSDK.Network"] = "0.3.0", + ["ShrinkSDK.Network.Integration.EventBus"] = "0.2.0", + ["ShrinkSDK.Network.Integration.App"] = "0.2.0", + ["ShrinkSDK.DataSaver"] = "2.3.0", + ["ShrinkSDK.DataSaver.Integration.EventBus"] = "2.2.0", + ["ShrinkSDK.DataSaver.Integration.App"] = "0.2.0", + ["ShrinkSDK.App.Core"] = "0.2.0", + ["ShrinkSDK.Context.AppAdapter"] = "0.2.0", + ["ShrinkSDK.App.Starter.Basic"] = "0.3.0", + ["ShrinkSDK.ModFramework"] = "0.3.0", + ["ShrinkSDK.ModFramework.Godot"] = "0.1.0", + ["ShrinkSDK.Tutorial"] = "0.2.0", + ["ShrinkSDK.Tutorial.Godot"] = "0.1.0" + }; + + public static IReadOnlyDictionary ReadPackageReferences(string projectPath) + { + var document = XDocument.Load(projectPath, LoadOptions.PreserveWhitespace); + return document.Descendants("PackageReference") + .Select(element => new + { + Id = ((string?)element.Attribute("Include") ?? (string?)element.Attribute("Update"))?.Trim(), + Version = ((string?)element.Attribute("Version") ?? + (string?)element.Attribute("VersionOverride") ?? + element.Element("Version")?.Value)?.Trim() ?? string.Empty + }) + .Where(item => !string.IsNullOrWhiteSpace(item.Id)) + .GroupBy(item => item.Id!, StringComparer.OrdinalIgnoreCase) + .ToDictionary(group => group.Key, group => group.First().Version, StringComparer.OrdinalIgnoreCase); + } + + public static void SetPackageReference(string projectPath, string packageId, string? version) + { + var document = XDocument.Load(projectPath, LoadOptions.PreserveWhitespace); + var root = document.Root ?? throw new InvalidDataException("The C# project has no root element."); + var references = root.Descendants("PackageReference").Where(element => + string.Equals((string?)element.Attribute("Include"), packageId, StringComparison.OrdinalIgnoreCase)).ToArray(); + foreach (var duplicate in references.Skip(1)) duplicate.Remove(); + if (string.IsNullOrWhiteSpace(version)) + { + foreach (var reference in references) reference.Remove(); + } + else if (references.Length > 0) + { + var reference = references[0]; + if (reference.Attribute("VersionOverride") != null) + reference.SetAttributeValue("VersionOverride", version); + else if (reference.Attribute("Version") != null) + reference.SetAttributeValue("Version", version); + else if (reference.Element("Version") != null) + reference.Element("Version")!.Value = version; + else + reference.SetAttributeValue("Version", version); + } + else + { + var group = root.Elements("ItemGroup").FirstOrDefault(element => element.Elements("PackageReference").Any()); + if (group == null) + { + group = new XElement("ItemGroup"); + root.Add(group); + } + group.Add(new XElement("PackageReference", new XAttribute("Include", packageId), new XAttribute("Version", version))); + } + WriteAtomically(projectPath, document); + } + + public static void EnsureShrinkFeed(string nugetConfigPath) + { + XDocument document; + if (File.Exists(nugetConfigPath)) document = XDocument.Load(nugetConfigPath, LoadOptions.PreserveWhitespace); + else document = new XDocument(new XDeclaration("1.0", "utf-8", null), new XElement("configuration")); + var root = document.Root ?? throw new InvalidDataException("NuGet.Config has no root element."); + var sources = root.Element("packageSources"); + if (sources == null) + { + sources = new XElement("packageSources"); + root.Add(sources); + } + var existing = sources.Elements("add").FirstOrDefault(element => + string.Equals((string?)element.Attribute("key"), "ShrinkSDK", StringComparison.OrdinalIgnoreCase)); + if (existing == null) + sources.Add(new XElement("add", new XAttribute("key", "ShrinkSDK"), + new XAttribute("value", "https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json"))); + else + existing.SetAttributeValue("value", "https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json"); + WriteAtomically(nugetConfigPath, document); + } + + public static async Task<(int ExitCode, string Output)> RunDotnetAsync(string projectDirectory, + string arguments, CancellationToken cancellationToken = default) + { + var output = new StringBuilder(); + using var process = new Process + { + StartInfo = new ProcessStartInfo("dotnet", arguments) + { + WorkingDirectory = projectDirectory, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + process.OutputDataReceived += (_, args) => { if (args.Data != null) output.AppendLine(args.Data); }; + process.ErrorDataReceived += (_, args) => { if (args.Data != null) output.AppendLine(args.Data); }; + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + await process.WaitForExitAsync(cancellationToken); + return (process.ExitCode, output.ToString()); + } + + private static void WriteAtomically(string path, XDocument document) + { + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))!); + var temporaryPath = path + ".shrinksdk.tmp"; + using (var writer = new StreamWriter(temporaryPath, false, new UTF8Encoding(false))) + document.Save(writer, SaveOptions.DisableFormatting); + File.Move(temporaryPath, path, true); + } +} diff --git a/Godot/Installer/addons/shrinksdk/plugin.cfg b/Godot/Installer/addons/shrinksdk/plugin.cfg new file mode 100644 index 0000000..1715616 --- /dev/null +++ b/Godot/Installer/addons/shrinksdk/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="ShrinkSDK Installer" +description="Installs and diagnoses ShrinkSDK NuGet modules and CodeGen weaving." +author="ShrinkSDK" +version="0.1.1" +script="ShrinkGodotInstallerPlugin.cs" diff --git a/Godot/NuGet.Config b/Godot/NuGet.Config new file mode 100644 index 0000000..cc8d1e3 --- /dev/null +++ b/Godot/NuGet.Config @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.App.Core/Runtime/ShrinkAppCore.cs b/Godot/Packages/ShrinkSDK.App.Core/Runtime/ShrinkAppCore.cs new file mode 100644 index 0000000..e9d112b --- /dev/null +++ b/Godot/Packages/ShrinkSDK.App.Core/Runtime/ShrinkAppCore.cs @@ -0,0 +1,137 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using Cysharp.Threading.Tasks; + +namespace ShrinkApp; + +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public sealed class ShrinkAppModuleInstallerAttribute : Attribute { } + +public interface IShrinkAppModuleInstaller +{ + string ModuleId { get; } + int Order { get; } + IReadOnlyList DependsOn { get; } + void RegisterServices(ShrinkAppContext context); + UniTask InitializeAsync(ShrinkAppContext context); +} + +public interface IShrinkAppModuleShutdown +{ + UniTask ShutdownAsync(ShrinkAppContext context); +} + +public sealed class ShrinkAppSettings +{ + public bool VerboseLogging { get; set; } + public string[] DisabledModuleIds { get; set; } = Array.Empty(); +} + +public sealed class ShrinkAppServices +{ + private readonly Dictionary _services = new(); + public void Register(T service) where T : class => _services[typeof(T)] = service ?? throw new ArgumentNullException(nameof(service)); + public bool TryGet(out T? service) where T : class + { + service = _services.TryGetValue(typeof(T), out var value) ? value as T : null; + return service != null; + } + public T GetRequired() where T : class => TryGet(out var value) ? value! : throw new InvalidOperationException($"Required service is not registered: {typeof(T).FullName}"); + public bool TryUnregister(T service) where T : class => _services.TryGetValue(typeof(T), out var value) && ReferenceEquals(value, service) && _services.Remove(typeof(T)); +} + +public sealed class ShrinkAppContext +{ + public ShrinkAppContext(object? host, ShrinkAppSettings settings, ShrinkAppServices services) + { + Host = host; + Settings = settings ?? throw new ArgumentNullException(nameof(settings)); + Services = services ?? throw new ArgumentNullException(nameof(services)); + } + public object? Host { get; } + public ShrinkAppSettings Settings { get; } + public ShrinkAppServices Services { get; } + public static ShrinkAppContext CreateStandalone(ShrinkAppServices services, ShrinkAppSettings? settings = null) => new(null, settings ?? new ShrinkAppSettings(), services); +} + +public sealed class ShrinkAppRuntime +{ + private readonly List _started = new(); + public ShrinkAppRuntime(object? host = null, ShrinkAppSettings? settings = null, ShrinkAppServices? services = null) + { + Context = new ShrinkAppContext(host, settings ?? new ShrinkAppSettings(), services ?? new ShrinkAppServices()); + } + public ShrinkAppContext Context { get; } + public bool IsRunning { get; private set; } + public IReadOnlyList StartedModules => _started.Select(item => item.ModuleId).ToArray(); + + public async UniTask StartAsync() + { + if (IsRunning) return; + var installers = Sort(CreateInstallers(), Context.Settings.DisabledModuleIds); + try + { + foreach (var installer in installers) installer.RegisterServices(Context); + foreach (var installer in installers) + { + await installer.InitializeAsync(Context); + _started.Add(installer); + } + IsRunning = true; + } + catch + { + await ShutdownStartedAsync(); + throw; + } + } + + public async UniTask ShutdownAsync() + { + await ShutdownStartedAsync(); + IsRunning = false; + } + + private async UniTask ShutdownStartedAsync() + { + for (var index = _started.Count - 1; index >= 0; index--) + if (_started[index] is IShrinkAppModuleShutdown shutdown) await shutdown.ShutdownAsync(Context); + _started.Clear(); + } + + private static IReadOnlyList CreateInstallers() => + ShrinkAppInstallers.GetDiscoveredInstallerTypes().Select(type => + Activator.CreateInstance(type) as IShrinkAppModuleInstaller ?? throw new InvalidOperationException($"Failed to create installer: {type.FullName}")) + .ToArray(); + + private static IReadOnlyList Sort(IEnumerable source, IEnumerable disabledIds) + { + var disabled = new HashSet(disabledIds ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + var byId = source.Where(item => !disabled.Contains(item.ModuleId)).ToDictionary(item => item.ModuleId, StringComparer.OrdinalIgnoreCase); + var states = new Dictionary(StringComparer.OrdinalIgnoreCase); + var result = new List(); + foreach (var installer in byId.Values.OrderBy(item => item.Order).ThenBy(item => item.ModuleId, StringComparer.OrdinalIgnoreCase)) + Visit(installer, byId, states, result); + return result; + } + + private static void Visit(IShrinkAppModuleInstaller installer, IReadOnlyDictionary byId, + IDictionary states, ICollection result) + { + var state = states.TryGetValue(installer.ModuleId, out var value) ? value : 0; + if (state == 2) return; + if (state == 1) throw new InvalidOperationException($"Circular installer dependency: {installer.ModuleId}"); + states[installer.ModuleId] = 1; + foreach (var dependencyId in installer.DependsOn ?? Array.Empty()) + { + if (!byId.TryGetValue(dependencyId, out var dependency)) + throw new InvalidOperationException($"Installer dependency missing. Module={installer.ModuleId}, DependsOn={dependencyId}"); + Visit(dependency, byId, states, result); + } + states[installer.ModuleId] = 2; + result.Add(installer); + } +} diff --git a/Godot/Packages/ShrinkSDK.App.Core/ShrinkSDK.App.Core.csproj b/Godot/Packages/ShrinkSDK.App.Core/ShrinkSDK.App.Core.csproj new file mode 100644 index 0000000..490f95d --- /dev/null +++ b/Godot/Packages/ShrinkSDK.App.Core/ShrinkSDK.App.Core.csproj @@ -0,0 +1,20 @@ + + + netstandard2.1 + false + ShrinkApp.Core.Runtime + ShrinkApp + ShrinkSDK.App.Core + 0.2.0 + ShrinkSDK engine-neutral application composition runtime. + false + + + + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.App.Starter.Basic/Runtime/ShrinkBasicComposition.cs b/Godot/Packages/ShrinkSDK.App.Starter.Basic/Runtime/ShrinkBasicComposition.cs new file mode 100644 index 0000000..813b350 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.App.Starter.Basic/Runtime/ShrinkBasicComposition.cs @@ -0,0 +1,34 @@ +#nullable enable + +using System.Collections.Generic; +using ShrinkApp; +using ShrinkCommand.Integration.App; +using ShrinkContext; +using ShrinkContext.AppAdapter; +using ShrinkDataSaver.Integration.App; +using ShrinkNetwork.Integration.App; + +namespace ShrinkApp.Starter.Basic; + +public static class ShrinkBasicComposition +{ + public static ShrinkContextAppHost CreateHost(ShrinkAppServices? services = null) + { + var host = new ShrinkContextAppHost(services); + host.Register("shrink.command"); + host.Register("shrink.datasaver"); + host.Register("shrink.network"); + return host; + } + + public static IReadOnlyList CreateEntries(ShrinkAppServices services, + ShrinkNetworkAppModuleConfig? network = null) => new[] + { + new ShrinkLoaderEntry("datasaver", "shrink.datasaver", services), + new ShrinkLoaderEntry("command", "shrink.command", services), + new ShrinkLoaderEntry("network", "shrink.network", network ?? new ShrinkNetworkAppModuleConfig + { + Services = services + }) + }; +} diff --git a/Godot/Packages/ShrinkSDK.App.Starter.Basic/ShrinkSDK.App.Starter.Basic.csproj b/Godot/Packages/ShrinkSDK.App.Starter.Basic/ShrinkSDK.App.Starter.Basic.csproj new file mode 100644 index 0000000..29c6f67 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.App.Starter.Basic/ShrinkSDK.App.Starter.Basic.csproj @@ -0,0 +1,21 @@ + + + netstandard2.1 + false + ShrinkApp.Starter.Basic + ShrinkApp.Starter.Basic + ShrinkSDK.App.Starter.Basic + 0.3.0 + Engine-neutral basic ShrinkApp composition. + false + + + + + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.Command.Integration.EventBus/ShrinkSDK.Command.Integration.EventBus.csproj b/Godot/Packages/ShrinkSDK.Command.Integration.EventBus/ShrinkSDK.Command.Integration.EventBus.csproj new file mode 100644 index 0000000..a278416 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.Command.Integration.EventBus/ShrinkSDK.Command.Integration.EventBus.csproj @@ -0,0 +1,20 @@ + + + netstandard2.1 + false + ShrinkCommand.Integration.EventBus + ShrinkCommand.Integration + ShrinkSDK.Command.Integration.EventBus + 0.2.0 + ShrinkCommand and EventBus integration. + true + + + + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.Command.Integration.Network/ShrinkSDK.Command.Integration.Network.csproj b/Godot/Packages/ShrinkSDK.Command.Integration.Network/ShrinkSDK.Command.Integration.Network.csproj new file mode 100644 index 0000000..8274b48 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.Command.Integration.Network/ShrinkSDK.Command.Integration.Network.csproj @@ -0,0 +1,20 @@ + + + netstandard2.1 + false + ShrinkCommand.Integration.Network + ShrinkCommand.Integration + ShrinkSDK.Command.Integration.Network + 0.2.0 + ShrinkCommand and Network RPC integration. + true + + + + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.Command/ShrinkSDK.Command.csproj b/Godot/Packages/ShrinkSDK.Command/ShrinkSDK.Command.csproj new file mode 100644 index 0000000..34600a7 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.Command/ShrinkSDK.Command.csproj @@ -0,0 +1,18 @@ + + + netstandard2.1 + false + ShrinkCommand.Runtime + ShrinkCommand + ShrinkSDK.Command + 0.3.0 + ShrinkSDK command parsing, permissions and execution runtime. + false + + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.Context.AppAdapter/Runtime/ShrinkContextAppHost.cs b/Godot/Packages/ShrinkSDK.Context.AppAdapter/Runtime/ShrinkContextAppHost.cs new file mode 100644 index 0000000..1053589 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.Context.AppAdapter/Runtime/ShrinkContextAppHost.cs @@ -0,0 +1,31 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using ShrinkApp; + +namespace ShrinkContext.AppAdapter; + +public sealed class ShrinkContextAppHost +{ + private readonly ShrinkContextRuntime _runtime = new(); + private readonly ShrinkComponentCatalog _catalog = new(); + private readonly ShrinkContextLoader _loader; + + public ShrinkContextAppHost(ShrinkAppServices? services = null) + { + Services = services ?? new ShrinkAppServices(); + _loader = new ShrinkContextLoader(_runtime, _catalog); + } + + public ShrinkAppServices Services { get; } + public ShrinkContextRuntime Runtime => _runtime; + public ShrinkLoaderTransactionDiagnostic? LastTransaction => _loader.LastTransaction; + + public void Register(string name, Func factory) => _catalog.Register(name, factory); + public void Register(string name) where TComponent : IShrinkComponent, new() => + _catalog.Register(name); + public UniTask ApplyAsync(IReadOnlyList entries) => _loader.ApplyAsync(entries); + public UniTask ShutdownAsync() => _runtime.ShutdownAsync(); +} diff --git a/Godot/Packages/ShrinkSDK.Context.AppAdapter/ShrinkSDK.Context.AppAdapter.csproj b/Godot/Packages/ShrinkSDK.Context.AppAdapter/ShrinkSDK.Context.AppAdapter.csproj new file mode 100644 index 0000000..ff4c6c4 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.Context.AppAdapter/ShrinkSDK.Context.AppAdapter.csproj @@ -0,0 +1,17 @@ + + + netstandard2.1 + false + ShrinkContext.AppAdapter + ShrinkContext.AppAdapter + ShrinkSDK.Context.AppAdapter + 0.2.0 + Engine-neutral ShrinkContext composition host for ShrinkApp. + false + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.Context.Core/ShrinkSDK.Context.Core.csproj b/Godot/Packages/ShrinkSDK.Context.Core/ShrinkSDK.Context.Core.csproj new file mode 100644 index 0000000..6baa9c0 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.Context.Core/ShrinkSDK.Context.Core.csproj @@ -0,0 +1,17 @@ + + + netstandard2.1 + false + ShrinkContext.Core.Runtime + ShrinkContext + ShrinkSDK.Context.Core + 0.2.0 + ShrinkSDK reversible context and fiber runtime. + false + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.DataSaver.Integration.EventBus/ShrinkSDK.DataSaver.Integration.EventBus.csproj b/Godot/Packages/ShrinkSDK.DataSaver.Integration.EventBus/ShrinkSDK.DataSaver.Integration.EventBus.csproj new file mode 100644 index 0000000..7e6de31 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.DataSaver.Integration.EventBus/ShrinkSDK.DataSaver.Integration.EventBus.csproj @@ -0,0 +1,20 @@ + + + netstandard2.1 + false + ShrinkDataSaver.Integration.EventBus + ShrinkDataSaver.Integration + ShrinkSDK.DataSaver.Integration.EventBus + 2.2.0 + ShrinkDataSaver and EventBus integration. + false + disable + + + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.DataSaver/Runtime/ShrinkDataSaverRuntime.cs b/Godot/Packages/ShrinkSDK.DataSaver/Runtime/ShrinkDataSaverRuntime.cs new file mode 100644 index 0000000..8cc5d94 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.DataSaver/Runtime/ShrinkDataSaverRuntime.cs @@ -0,0 +1,43 @@ +#nullable enable + +using System.IO; +using Cysharp.Threading.Tasks; +using ShrinkSDK.Runtime; + +namespace ShrinkDataSaver; + +public sealed class ShrinkDataSaverRuntimeConfig +{ + public string? RootPath { get; set; } + public string SaveFileExtension { get; set; } = ".sav"; + public string SettingsFileName { get; set; } = "settings.json"; + public int CurrentSaveVersion { get; set; } = 1; + public int MaxSlots { get; set; } = 3; + public float SettingsWriteDebounceSeconds { get; set; } = 0.35f; + public bool DontDestroyOnLoadDriver { get; set; } +} + +public static class ShrinkDataSaverRuntime +{ + public static bool IsInitialized { get; private set; } + + public static void Initialize(ShrinkDataSaverRuntimeConfig? config = null) + { + if (IsInitialized) return; + config ??= new ShrinkDataSaverRuntimeConfig(); + var root = string.IsNullOrWhiteSpace(config.RootPath) + ? ShrinkRuntimeServices.Paths.PersistentDataPath + : config.RootPath!; + Directory.CreateDirectory(root); + var storage = new LocalStorageProvider(root); + ShrinkDataSaverPlatform.SettingsWriteDebounceSeconds = config.SettingsWriteDebounceSeconds; + ShrinkDataSaverPlatform.MaxSlots = config.MaxSlots; + ShrinkSettings.Initialize(storage, Path.Combine(root, config.SettingsFileName)); + ShrinkSave.Initialize(storage, Path.Combine(root, "saves"), config.SaveFileExtension, + config.CurrentSaveVersion); + ShrinkSettings.LoadAsync().Forget(); + IsInitialized = true; + ShrinkRuntimeServices.Logger.Log(ShrinkLogLevel.Information, + $"[ShrinkDataSaver] Runtime initialized. Root: {root}"); + } +} diff --git a/Godot/Packages/ShrinkSDK.DataSaver/ShrinkSDK.DataSaver.csproj b/Godot/Packages/ShrinkSDK.DataSaver/ShrinkSDK.DataSaver.csproj new file mode 100644 index 0000000..b4cce88 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.DataSaver/ShrinkSDK.DataSaver.csproj @@ -0,0 +1,29 @@ + + + netstandard2.1 + false + ShrinkDataSaver.Runtime + ShrinkDataSaver + ShrinkSDK.DataSaver + 2.3.0 + ShrinkSDK engine-neutral versioned save and settings runtime. + false + disable + + + + + + + + + + + + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.EventBus/ShrinkSDK.EventBus.csproj b/Godot/Packages/ShrinkSDK.EventBus/ShrinkSDK.EventBus.csproj new file mode 100644 index 0000000..633c90e --- /dev/null +++ b/Godot/Packages/ShrinkSDK.EventBus/ShrinkSDK.EventBus.csproj @@ -0,0 +1,19 @@ + + + netstandard2.1 + false + ShrinkEventBus.Runtime + ShrinkEventBus + ShrinkSDK.EventBus + 2.1.0 + ShrinkSDK typed event bus runtime. + false + + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.Godot/ShrinkGodotHost.cs b/Godot/Packages/ShrinkSDK.Godot/ShrinkGodotHost.cs new file mode 100644 index 0000000..60ffcbd --- /dev/null +++ b/Godot/Packages/ShrinkSDK.Godot/ShrinkGodotHost.cs @@ -0,0 +1,154 @@ +#nullable enable + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Cysharp.Threading.Tasks; +using Godot; +using ShrinkEventBus; +using ShrinkDataSaver; +using ShrinkApp; +using ShrinkNetwork; +using ShrinkSDK.Runtime; + +namespace ShrinkSDK.Godot; + +public partial class ShrinkGodotHost : Node, IShrinkMainThreadDispatcher, + IShrinkLogger, IShrinkClock, IShrinkPathProvider, IShrinkScreenshotProvider, + IShrinkApplicationLifecycle +{ + private readonly ConcurrentQueue _dispatchQueue = new(); + private readonly Dictionary _networkServices = new(); + private int _mainThreadId; + private bool _networkPumpRunning; + private ShrinkAppRuntime? _appRuntime; + + public event Action? Paused; + public event Action? Resumed; + public event Action? Exiting; + + [Export] public int DispatchBudgetPerFrame { get; set; } = 256; + [Export] public int NetworkDispatchBudgetPerFrame { get; set; } = 256; + + public bool IsMainThread => Thread.CurrentThread.ManagedThreadId == _mainThreadId; + public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; + public double UnscaledTimeSeconds => Time.GetTicksMsec() / 1000d; + public string PersistentDataPath => ProjectSettings.GlobalizePath("user://"); + public string TemporaryDataPath => OS.GetCacheDir(); + + public override void _Ready() + { + _mainThreadId = Thread.CurrentThread.ManagedThreadId; + ShrinkRuntimeServices.Configure(this, this, this, this, this, this); + ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(this); + ShrinkDataSaverPlatform.Configure(this, this, this, this); + SetProcess(true); + } + + public override void _Process(double delta) + { + var budget = Math.Max(1, DispatchBudgetPerFrame); + while (budget-- > 0 && _dispatchQueue.TryDequeue(out var action)) + action(); + PumpNetworkAsync().Forget(); + } + + public override void _Notification(int what) + { + switch ((long)what) + { + case NotificationApplicationPaused: + case NotificationApplicationFocusOut: + Paused?.Invoke(); + break; + case NotificationApplicationResumed: + case NotificationApplicationFocusIn: + Resumed?.Invoke(); + break; + case NotificationWMCloseRequest: + case NotificationPredelete: + Exiting?.Invoke(); + break; + } + } + + public async UniTask StartAppAsync(ShrinkAppSettings? settings = null, ShrinkAppServices? services = null) + { + if (_appRuntime?.IsRunning == true) return _appRuntime; + _appRuntime = new ShrinkAppRuntime(this, settings, services); + await _appRuntime.StartAsync(); + return _appRuntime; + } + + public async UniTask ShutdownAppAsync() + { + if (_appRuntime == null) return; + await _appRuntime.ShutdownAsync(); + _appRuntime = null; + } + + public bool TryPost(Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + _dispatchQueue.Enqueue(action); + return true; + } + + public void AddNetworkService(ShrinkNetworkService service) + { + if (service == null) throw new ArgumentNullException(nameof(service)); + if (_networkServices.ContainsKey(service)) return; + var queue = new ShrinkNetworkDispatchQueue(Math.Max(1, NetworkDispatchBudgetPerFrame * 4)); + service.DispatchScheduler = queue; + _networkServices.Add(service, queue); + } + + public bool RemoveNetworkService(ShrinkNetworkService service) + { + if (!_networkServices.Remove(service, out var queue)) return false; + service.DispatchScheduler = ShrinkNetworkDispatchSchedulers.Inline; + queue.Dispose(); + return true; + } + + public static ShrinkCodeGenWovenAttribute RequireWoven(Assembly assembly) + { + if (assembly == null) throw new ArgumentNullException(nameof(assembly)); + return assembly.GetCustomAttribute() ?? + throw new InvalidOperationException( + $"Assembly '{assembly.GetName().Name}' was not woven by ShrinkSDK.CodeGen. Ensure the package is installed and ShrinkCodeGenEnabled is not false."); + } + + public void Log(ShrinkLogLevel level, string message, Exception? exception = null) + { + var text = exception == null ? message : $"{message}{System.Environment.NewLine}{exception}"; + if (level >= ShrinkLogLevel.Error) GD.PushError(text); + else if (level >= ShrinkLogLevel.Warning) GD.PushWarning(text); + else GD.Print(text); + } + + public ValueTask CapturePngAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + var image = GetViewport().GetTexture().GetImage(); + return ValueTask.FromResult(image.SavePngToBuffer()); + } + + private async UniTaskVoid PumpNetworkAsync() + { + if (_networkPumpRunning) return; + _networkPumpRunning = true; + try + { + foreach (var queue in _networkServices.Values) + await queue.PumpAsync(Math.Max(1, NetworkDispatchBudgetPerFrame)); + } + finally + { + _networkPumpRunning = false; + } + } +} diff --git a/Godot/Packages/ShrinkSDK.Godot/ShrinkSDK.Godot.csproj b/Godot/Packages/ShrinkSDK.Godot/ShrinkSDK.Godot.csproj new file mode 100644 index 0000000..61b34a6 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.Godot/ShrinkSDK.Godot.csproj @@ -0,0 +1,20 @@ + + + net8.0 + ShrinkSDK.Godot + ShrinkSDK.Godot + ShrinkSDK.Godot + 0.1.0 + Godot 4.6 host and platform services for ShrinkSDK. + false + + + + + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.ModFramework.Godot/ShrinkGodotModLoader.cs b/Godot/Packages/ShrinkSDK.ModFramework.Godot/ShrinkGodotModLoader.cs new file mode 100644 index 0000000..93d29c3 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.ModFramework.Godot/ShrinkGodotModLoader.cs @@ -0,0 +1,66 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; + +namespace ShrinkModFramework.Godot; + +public sealed class ShrinkGodotModLoader : IDisposable +{ + private sealed record LoadedMod(AssemblyLoadContext LoadContext, WeakReference UnloadReference, + ShrinkModContext Context, IReadOnlyList Instances); + private readonly Dictionary _loaded = new(StringComparer.OrdinalIgnoreCase); + + public IReadOnlyCollection LoadedAssemblyPaths => _loaded.Keys; + + public IReadOnlyList Load(string assemblyPath) + { + assemblyPath = Path.GetFullPath(assemblyPath); + if (_loaded.ContainsKey(assemblyPath)) throw new InvalidOperationException($"Mod assembly is already loaded: {assemblyPath}"); + var loadContext = new AssemblyLoadContext($"ShrinkMod:{Path.GetFileNameWithoutExtension(assemblyPath)}", true); + loadContext.Resolving += (_, name) => ResolveDependency(loadContext, Path.GetDirectoryName(assemblyPath)!, name); + try + { + var assembly = loadContext.LoadFromAssemblyPath(assemblyPath); + var entries = assembly.GetTypes().Where(type => !type.IsAbstract && typeof(IShrinkMod).IsAssignableFrom(type) && + type.GetCustomAttribute() != null).OrderBy(type => type.GetCustomAttribute()!.LoadOrder).ToArray(); + var context = new ShrinkModContext(Path.GetDirectoryName(assemblyPath)!); + var instances = entries.Select(type => (IShrinkMod)(Activator.CreateInstance(type) ?? + throw new InvalidOperationException($"Failed to create mod entry: {type.FullName}"))).ToArray(); + foreach (var mod in instances) mod.OnConstruct(context); + foreach (var mod in instances) mod.OnRegisterContent(context); + foreach (var mod in instances) mod.OnInitialize(context); + foreach (var mod in instances) mod.OnReady(context); + _loaded[assemblyPath] = new LoadedMod(loadContext, new WeakReference(loadContext), context, instances); + return instances; + } + catch { loadContext.Unload(); throw; } + } + + public bool Unload(string assemblyPath) + { + assemblyPath = Path.GetFullPath(assemblyPath); + if (!_loaded.Remove(assemblyPath, out var loaded)) return false; + for (var index = loaded.Instances.Count - 1; index >= 0; index--) + if (loaded.Instances[index] is IShrinkModUnload unload) unload.OnUnload(loaded.Context); + loaded.LoadContext.Unload(); + return true; + } + + public void Dispose() + { + foreach (var path in _loaded.Keys.ToArray()) Unload(path); + } + + private static Assembly? ResolveDependency(AssemblyLoadContext context, string directory, AssemblyName name) + { + var shared = AssemblyLoadContext.Default.Assemblies.FirstOrDefault(assembly => assembly.GetName().Name == name.Name); + if (shared != null) return shared; + var path = Path.Combine(directory, name.Name + ".dll"); + return File.Exists(path) ? context.LoadFromAssemblyPath(path) : null; + } +} diff --git a/Godot/Packages/ShrinkSDK.ModFramework.Godot/ShrinkSDK.ModFramework.Godot.csproj b/Godot/Packages/ShrinkSDK.ModFramework.Godot/ShrinkSDK.ModFramework.Godot.csproj new file mode 100644 index 0000000..7863adb --- /dev/null +++ b/Godot/Packages/ShrinkSDK.ModFramework.Godot/ShrinkSDK.ModFramework.Godot.csproj @@ -0,0 +1,15 @@ + + + net8.0 + ShrinkModFramework.Godot + ShrinkModFramework.Godot + ShrinkSDK.ModFramework.Godot + 0.1.0 + Collectible AssemblyLoadContext host for ShrinkSDK mods in Godot. + false + + + + + + diff --git a/Godot/Packages/ShrinkSDK.ModFramework/Runtime/ShrinkModContext.cs b/Godot/Packages/ShrinkSDK.ModFramework/Runtime/ShrinkModContext.cs new file mode 100644 index 0000000..e38807e --- /dev/null +++ b/Godot/Packages/ShrinkSDK.ModFramework/Runtime/ShrinkModContext.cs @@ -0,0 +1,25 @@ +#nullable enable + +using System; +using System.Collections.Generic; + +namespace ShrinkModFramework; + +public sealed class ShrinkModContext +{ + private readonly Dictionary _services = new(); + public ShrinkModContext(string modDirectory) => ModDirectory = modDirectory ?? throw new ArgumentNullException(nameof(modDirectory)); + public string ModDirectory { get; } + public void RegisterService(T service) where T : class => _services[typeof(T)] = service ?? throw new ArgumentNullException(nameof(service)); + public bool TryGetService(out T? service) where T : class + { + service = _services.TryGetValue(typeof(T), out var value) ? value as T : null; + return service != null; + } + public T GetRequiredService() where T : class => TryGetService(out var value) ? value! : throw new InvalidOperationException($"Service is not registered: {typeof(T).FullName}"); +} + +public interface IShrinkModUnload +{ + void OnUnload(ShrinkModContext context); +} diff --git a/Godot/Packages/ShrinkSDK.ModFramework/ShrinkSDK.ModFramework.csproj b/Godot/Packages/ShrinkSDK.ModFramework/ShrinkSDK.ModFramework.csproj new file mode 100644 index 0000000..baa6894 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.ModFramework/ShrinkSDK.ModFramework.csproj @@ -0,0 +1,21 @@ + + + netstandard2.1 + false + ShrinkModFramework.Runtime + ShrinkModFramework + ShrinkSDK.ModFramework + 0.3.0 + Engine-neutral ShrinkSDK mod contracts and lifecycle. + disable + false + + + + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.Network.Integration.EventBus/ShrinkSDK.Network.Integration.EventBus.csproj b/Godot/Packages/ShrinkSDK.Network.Integration.EventBus/ShrinkSDK.Network.Integration.EventBus.csproj new file mode 100644 index 0000000..225c06b --- /dev/null +++ b/Godot/Packages/ShrinkSDK.Network.Integration.EventBus/ShrinkSDK.Network.Integration.EventBus.csproj @@ -0,0 +1,20 @@ + + + netstandard2.1 + false + ShrinkNetwork.Integration.EventBus + ShrinkNetwork.Integration + ShrinkSDK.Network.Integration.EventBus + 0.2.0 + ShrinkNetwork and EventBus generated bridge. + true + + + + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.Network/ShrinkSDK.Network.csproj b/Godot/Packages/ShrinkSDK.Network/ShrinkSDK.Network.csproj new file mode 100644 index 0000000..fd08cae --- /dev/null +++ b/Godot/Packages/ShrinkSDK.Network/ShrinkSDK.Network.csproj @@ -0,0 +1,22 @@ + + + netstandard2.1 + false + ShrinkNetwork.Runtime + ShrinkNetwork + ShrinkSDK.Network + 0.3.0 + ShrinkSDK messaging, RPC, permission and transport runtime. + true + false + + + + + + + + + + + diff --git a/Godot/Packages/ShrinkSDK.Runtime.Abstractions/ShrinkSDK.Runtime.Abstractions.csproj b/Godot/Packages/ShrinkSDK.Runtime.Abstractions/ShrinkSDK.Runtime.Abstractions.csproj new file mode 100644 index 0000000..47f22ff --- /dev/null +++ b/Godot/Packages/ShrinkSDK.Runtime.Abstractions/ShrinkSDK.Runtime.Abstractions.csproj @@ -0,0 +1,13 @@ + + + netstandard2.1 + ShrinkRuntime.Abstractions + ShrinkSDK.Runtime + ShrinkSDK.Runtime.Abstractions + 0.1.0 + Engine-neutral ShrinkSDK runtime contracts. + + + + + diff --git a/Godot/Packages/ShrinkSDK.Tutorial.Godot/ShrinkGodotTutorialOverlay.cs b/Godot/Packages/ShrinkSDK.Tutorial.Godot/ShrinkGodotTutorialOverlay.cs new file mode 100644 index 0000000..be5aa04 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.Tutorial.Godot/ShrinkGodotTutorialOverlay.cs @@ -0,0 +1,89 @@ +#nullable enable + +using Godot; + +namespace ShrinkTutorial.Godot; + +public partial class ShrinkGodotTutorialOverlay : Control +{ + private readonly Label _title = new(); + private readonly Label _body = new(); + private ShrinkTutorialRunner? _runner; + private Control? _target; + + public override void _Ready() + { + SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect); + MouseFilter = MouseFilterEnum.Stop; + var panel = new PanelContainer { CustomMinimumSize = new Vector2(320, 100) }; + panel.Position = new Vector2(24, 24); + var text = new VBoxContainer(); + text.AddChild(_title); + text.AddChild(_body); + panel.AddChild(text); + AddChild(panel); + } + + public void Bind(ShrinkTutorialRunner runner) + { + if (_runner != null) _runner.StepChanged -= ShowStep; + _runner = runner; + _runner.StepChanged += ShowStep; + if (_runner.CurrentStep != null) ShowStep(_runner.CurrentStep); + } + + public override void _ExitTree() + { + if (_runner != null) _runner.StepChanged -= ShowStep; + _runner = null; + _target = null; + } + + public override void _Process(double delta) + { + if (_runner?.CurrentStep is not { } step) return; + _target = ResolveTarget(step); + QueueRedraw(); + } + + public override void _Draw() + { + var viewport = GetViewportRect(); + if (_target == null) + { + DrawRect(viewport, new Color(0, 0, 0, 0.72f)); + return; + } + var hole = _target.GetGlobalRect().Grow(8); + var shade = new Color(0, 0, 0, 0.72f); + DrawRect(new Rect2(viewport.Position, new Vector2(viewport.Size.X, hole.Position.Y)), shade); + DrawRect(new Rect2(new Vector2(0, hole.End.Y), new Vector2(viewport.Size.X, viewport.Size.Y - hole.End.Y)), shade); + DrawRect(new Rect2(new Vector2(0, hole.Position.Y), new Vector2(hole.Position.X, hole.Size.Y)), shade); + DrawRect(new Rect2(new Vector2(hole.End.X, hole.Position.Y), new Vector2(viewport.Size.X - hole.End.X, hole.Size.Y)), shade); + } + + public override void _GuiInput(InputEvent @event) + { + if (@event is not InputEventMouseButton { Pressed: true }) return; + var step = _runner?.CurrentStep; + if (step == null) return; + if (step.CompleteCondition == ShrinkTutorialCompleteCondition.AnyClick || + step.CompleteCondition == ShrinkTutorialCompleteCondition.ClickTarget && _target?.GetGlobalRect().HasPoint(GetGlobalMousePosition()) == true) + _runner!.CompleteStep(); + AcceptEvent(); + } + + private void ShowStep(ShrinkTutorialStep step) + { + _title.Text = step.Title; + _body.Text = step.Body; + Visible = true; + } + + private Control? ResolveTarget(ShrinkTutorialStep step) + { + if (step.TargetMode == ShrinkTutorialTargetMode.Path) return GetTree().Root.GetNodeOrNull(step.Target); + if (step.TargetMode == ShrinkTutorialTargetMode.AnchorId) return GetTree().GetFirstNodeInGroup("shrink_tutorial_" + step.Target) as Control; + return null; + } +} diff --git a/Godot/Packages/ShrinkSDK.Tutorial.Godot/ShrinkSDK.Tutorial.Godot.csproj b/Godot/Packages/ShrinkSDK.Tutorial.Godot/ShrinkSDK.Tutorial.Godot.csproj new file mode 100644 index 0000000..f021c73 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.Tutorial.Godot/ShrinkSDK.Tutorial.Godot.csproj @@ -0,0 +1,15 @@ + + + net8.0 + ShrinkTutorial.Godot + ShrinkTutorial.Godot + ShrinkSDK.Tutorial.Godot + 0.1.0 + Godot Control overlay and Resources for ShrinkSDK Tutorial. + false + + + + + + diff --git a/Godot/Packages/ShrinkSDK.Tutorial/Runtime/ShrinkTutorialCore.cs b/Godot/Packages/ShrinkSDK.Tutorial/Runtime/ShrinkTutorialCore.cs new file mode 100644 index 0000000..886c88f --- /dev/null +++ b/Godot/Packages/ShrinkSDK.Tutorial/Runtime/ShrinkTutorialCore.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; + +namespace ShrinkTutorial; + +[Serializable] +public sealed class ShrinkTutorialStep +{ + public string StepId = string.Empty; + public string Title = string.Empty; + public string Body = string.Empty; + public string Target = string.Empty; + public ShrinkTutorialTargetMode TargetMode; + public ShrinkTutorialCompleteCondition CompleteCondition = ShrinkTutorialCompleteCondition.ClickTarget; + public ShrinkTutorialDialogAnchor DialogAnchor = ShrinkTutorialDialogAnchor.Auto; + public ShrinkTutorialMaskShape MaskShape = ShrinkTutorialMaskShape.Rect; + public string CustomEventName = string.Empty; + public bool BlockInputOutsideTarget = true; +} + +[Serializable] +public sealed class ShrinkTutorialData +{ + public string TutorialId = string.Empty; + public List Steps = new(); +} + +public sealed class ShrinkTutorialRunner +{ + private readonly IShrinkTutorialStorage _storage; + private ShrinkTutorialProgress _progress; + public ShrinkTutorialRunner(IShrinkTutorialStorage storage) + { + _storage = storage ?? throw new ArgumentNullException(nameof(storage)); + _progress = storage.Load() ?? new ShrinkTutorialProgress(); + } + public ShrinkTutorialData Current { get; private set; } + public int StepIndex { get; private set; } = -1; + public ShrinkTutorialStep CurrentStep => Current != null && StepIndex >= 0 && StepIndex < Current.Steps.Count ? Current.Steps[StepIndex] : null; + public event Action StepChanged; + public event Action Completed; + + public bool Start(ShrinkTutorialData tutorial) + { + if (tutorial == null || tutorial.Steps.Count == 0 || _progress.completedTutorials.Contains(tutorial.TutorialId)) return false; + Current = tutorial; + StepIndex = 0; + StepChanged?.Invoke(CurrentStep); + return true; + } + + public bool CompleteStep(string customEventName = null) + { + var step = CurrentStep; + if (step == null) return false; + if (step.CompleteCondition == ShrinkTutorialCompleteCondition.CustomEvent && + !string.Equals(step.CustomEventName, customEventName, StringComparison.Ordinal)) return false; + StepIndex++; + if (StepIndex < Current.Steps.Count) { StepChanged?.Invoke(CurrentStep); return true; } + _progress.completedTutorials.Add(Current.TutorialId); + _storage.Save(_progress); + var id = Current.TutorialId; + Current = null; + StepIndex = -1; + Completed?.Invoke(id); + return true; + } + + public void Reset() + { + Current = null; + StepIndex = -1; + _storage.Reset(); + _progress = new ShrinkTutorialProgress(); + } +} diff --git a/Godot/Packages/ShrinkSDK.Tutorial/ShrinkSDK.Tutorial.csproj b/Godot/Packages/ShrinkSDK.Tutorial/ShrinkSDK.Tutorial.csproj new file mode 100644 index 0000000..d2bce51 --- /dev/null +++ b/Godot/Packages/ShrinkSDK.Tutorial/ShrinkSDK.Tutorial.csproj @@ -0,0 +1,19 @@ + + + netstandard2.1 + false + ShrinkTutorial.Runtime + ShrinkTutorial + ShrinkSDK.Tutorial + 0.2.0 + Engine-neutral ShrinkSDK tutorial state and step runtime. + disable + + + + + + + + + diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/Main.tscn b/Godot/Samples/ShrinkSDK.Godot.Sample/Main.tscn new file mode 100644 index 0000000..3d6a470 --- /dev/null +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/Main.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource path="res://SampleRoot.cs" type="Script" id="1"] + +[node name="SampleRoot" type="Node"] +script = ExtResource("1") diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs b/Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs new file mode 100644 index 0000000..56d33de --- /dev/null +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs @@ -0,0 +1,124 @@ +#nullable enable + +using System; +using System.Linq; +using System.Collections.Generic; +using Godot; +using ShrinkApp; +using ShrinkApp.Starter.Basic; +using ShrinkCommand; +using Cysharp.Threading.Tasks; +using ShrinkDataSaver; +using ShrinkEventBus; +using ShrinkNetwork; +using ShrinkSDK.Godot; +using ShrinkTutorial; + +public partial class SampleRoot : ShrinkGodotHost +{ + public override void _Ready() + { + base._Ready(); + RunSmokeAsync().Forget(); + } + + private async UniTaskVoid RunSmokeAsync() + { + var marker = RequireWoven(GetType().Assembly); + using var binding = EventBus.Attach(new GodotSubscriber()); + EventBus.Post(new GodotSmokeEvent()); + var assembly = GetType().Assembly; + var registries = assembly.GetCustomAttributes(false).Select(value => value.GetType().Name).ToHashSet(); + var savedValue = 42; + ShrinkDataSaverRuntime.Initialize(new ShrinkDataSaverRuntimeConfig + { + RootPath = PersistentDataPath, + CurrentSaveVersion = 1 + }); + ShrinkSave.RegisterModule("godot-smoke", () => savedValue, value => savedValue = value); + await ShrinkSave.SaveSlotAsync(0, new SaveOptions { SlotName = "Godot Smoke" }); + savedValue = 0; + await ShrinkSave.LoadSlotAsync(0); + var app = await StartAppAsync(); + var composition = ShrinkBasicComposition.CreateHost(app.Context.Services); + var entries = ShrinkBasicComposition.CreateEntries(app.Context.Services); + await composition.ApplyAsync(entries); + await composition.ApplyAsync(entries); + var activeFibers = composition.Runtime.Fibers.Count(fiber => fiber.State == ShrinkContext.ShrinkFiberState.Active); + var tutorialStorage = new MemoryTutorialStorage(); + var tutorial = new ShrinkTutorialRunner(tutorialStorage); + var tutorialStarted = tutorial.Start(new ShrinkTutorialData + { + TutorialId = "godot-smoke", + Steps = { new ShrinkTutorialStep { StepId = "one", CompleteCondition = ShrinkTutorialCompleteCondition.AnyClick } } + }); + var tutorialCompleted = tutorial.CompleteStep(); + + if (GodotSubscriber.Calls != 1 || GodotStaticSubscriber.Calls != 1 || savedValue != 42 || + !app.IsRunning || !GodotInstaller.Initialized || + activeFibers != 3 || !tutorialStarted || !tutorialCompleted || !tutorialStorage.Saved || + !registries.Contains(nameof(ShrinkCommandStaticRegistryAttribute)) || + !registries.Contains(nameof(ShrinkNetworkMessageRegistryAttribute))) + { + GD.PushError("ShrinkSDK Godot smoke failed."); + GetTree().Quit(1); + return; + } + await composition.ShutdownAsync(); + var smokePath = System.IO.Path.Combine(OS.GetUserDataDir(), "shrink_smoke_pass.txt"); + System.IO.File.WriteAllText(smokePath, $"CodeGen={marker.WeaverVersion};DataSaver={savedValue}"); + if (!System.IO.File.Exists(smokePath)) + throw new System.IO.IOException($"Failed to create smoke sentinel: {smokePath}"); + GD.Print($"SHRINK_GODOT_SMOKE_PASS CodeGen={marker.WeaverVersion} DataSaver={savedValue}"); + GetTree().Quit(0); + } +} + +public sealed class MemoryTutorialStorage : IShrinkTutorialStorage +{ + private ShrinkTutorialProgress _progress = new(); + public bool Saved { get; private set; } + public ShrinkTutorialProgress Load() => _progress; + public void Save(ShrinkTutorialProgress progress) { _progress = progress; Saved = true; } + public void Reset() { _progress = new ShrinkTutorialProgress(); Saved = false; } +} + +public readonly struct GodotSmokeEvent : IShrinkEvent { } + +[ShrinkEventSubscriber] +public sealed class GodotSubscriber +{ + public static int Calls; + [ShrinkSubscribe] private void Handle(GodotSmokeEvent value) => Calls++; +} + +[ShrinkEventSubscriber] +public static class GodotStaticSubscriber +{ + public static int Calls; + [ShrinkSubscribe] private static void Handle(GodotSmokeEvent value) => Calls++; +} + +[ShrinkCommandSubscriber] +public static class GodotCommands +{ + [ShrinkCommand("godot/smoke")] public static void Smoke() { } +} + +[ShrinkNetworkMessage(42001, "godot/smoke")] +public sealed class GodotSmokeMessage : IShrinkNetworkMessage { } + +[ShrinkAppModuleInstaller] +public sealed class GodotInstaller : IShrinkAppModuleInstaller +{ + public static bool Initialized; + public string ModuleId => "godot-smoke"; + public int Order => 0; + public IReadOnlyList DependsOn => Array.Empty(); + public void RegisterServices(ShrinkAppContext context) => context.Services.Register(this); + public UniTask InitializeAsync(ShrinkAppContext context) + { + Initialized = true; + return UniTask.CompletedTask; + } +} diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs.uid b/Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs.uid new file mode 100644 index 0000000..0c62012 --- /dev/null +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/SampleRoot.cs.uid @@ -0,0 +1 @@ +uid://j565ln7hmg6i diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/ShrinkSDK.Godot.Sample.csproj b/Godot/Samples/ShrinkSDK.Godot.Sample/ShrinkSDK.Godot.Sample.csproj new file mode 100644 index 0000000..0dc47e7 --- /dev/null +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/ShrinkSDK.Godot.Sample.csproj @@ -0,0 +1,20 @@ + + + net8.0 + true + true + <_ShrinkCodeGenTaskAssembly>$(MSBuildThisFileDirectory)..\..\CodeGen\ShrinkSDK.CodeGen.Task\bin\$(Configuration)\net8.0\ShrinkSDK.CodeGen.Task.dll + + + + + + + + + + + + + + diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/ShrinkSDK.Godot.Sample.sln b/Godot/Samples/ShrinkSDK.Godot.Sample/ShrinkSDK.Godot.Sample.sln new file mode 100644 index 0000000..1bfb9e0 --- /dev/null +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/ShrinkSDK.Godot.Sample.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShrinkSDK.Godot.Sample", "ShrinkSDK.Godot.Sample.csproj", "{8ED6C49D-909C-4A5A-B80C-3537D1349377}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + ExportDebug|Any CPU = ExportDebug|Any CPU + ExportRelease|Any CPU = ExportRelease|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8ED6C49D-909C-4A5A-B80C-3537D1349377}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8ED6C49D-909C-4A5A-B80C-3537D1349377}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8ED6C49D-909C-4A5A-B80C-3537D1349377}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU + {8ED6C49D-909C-4A5A-B80C-3537D1349377}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU + {8ED6C49D-909C-4A5A-B80C-3537D1349377}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU + {8ED6C49D-909C-4A5A-B80C-3537D1349377}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU + {8ED6C49D-909C-4A5A-B80C-3537D1349377}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8ED6C49D-909C-4A5A-B80C-3537D1349377}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/Tutorial.tscn b/Godot/Samples/ShrinkSDK.Godot.Sample/Tutorial.tscn new file mode 100644 index 0000000..44fd905 --- /dev/null +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/Tutorial.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://TutorialDemo.cs" id="1"] + +[node name="TutorialDemo" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/TutorialDemo.cs b/Godot/Samples/ShrinkSDK.Godot.Sample/TutorialDemo.cs new file mode 100644 index 0000000..e780211 --- /dev/null +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/TutorialDemo.cs @@ -0,0 +1,49 @@ +#nullable enable + +using Godot; +using ShrinkTutorial; +using ShrinkTutorial.Godot; + +public partial class TutorialDemo : Control +{ + public override void _Ready() + { + var target = new Button + { + Text = "Tutorial Target", + Position = new Vector2(480, 260), + CustomMinimumSize = new Vector2(220, 72) + }; + target.AddToGroup("shrink_tutorial_primary"); + AddChild(target); + + var overlay = new ShrinkGodotTutorialOverlay(); + AddChild(overlay); + var runner = new ShrinkTutorialRunner(new MemoryTutorialStorage()); + runner.Start(new ShrinkTutorialData + { + TutorialId = "visual-smoke", + Steps = + { + new ShrinkTutorialStep + { + StepId = "target", + Title = "Godot Tutorial", + Body = "Target hole, dialog and input blocker smoke test", + TargetMode = ShrinkTutorialTargetMode.AnchorId, + Target = "primary", + CompleteCondition = ShrinkTutorialCompleteCondition.ClickTarget + } + } + }); + overlay.Bind(runner); + GetTree().CreateTimer(2).Timeout += QuitCleanly; + } + + private void QuitCleanly() + { + var tree = GetTree(); + tree.UnloadCurrentScene(); + tree.CreateTimer(0.1).Timeout += () => tree.Quit(0); + } +} diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/TutorialDemo.cs.uid b/Godot/Samples/ShrinkSDK.Godot.Sample/TutorialDemo.cs.uid new file mode 100644 index 0000000..4b82347 --- /dev/null +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/TutorialDemo.cs.uid @@ -0,0 +1 @@ +uid://bhjfn5prdv2pw diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/export_presets.cfg b/Godot/Samples/ShrinkSDK.Godot.Sample/export_presets.cfg new file mode 100644 index 0000000..c3a6b9c --- /dev/null +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/export_presets.cfg @@ -0,0 +1,52 @@ +[preset.0] + +name="Windows Desktop" +platform="Windows Desktop" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="../../Exports/Windows/ShrinkSDKGodot.exe" +script_export_mode=2 + +[preset.0.options] + +binary_format/architecture="x86_64" +debug/export_console_wrapper=1 + +[preset.1] + +name="Linux" +platform="Linux" +runnable=false +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="../../Exports/Linux/ShrinkSDKGodot.x86_64" +script_export_mode=2 + +[preset.1.options] + +binary_format/architecture="x86_64" + +[preset.2] + +name="macOS" +platform="macOS" +runnable=false +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="../../Exports/macOS/ShrinkSDKGodot.zip" +script_export_mode=2 + +[preset.2.options] + +application/bundle_identifier="work.crash.shrinksdk.sample" +binary_format/architecture="universal" diff --git a/Godot/Samples/ShrinkSDK.Godot.Sample/project.godot b/Godot/Samples/ShrinkSDK.Godot.Sample/project.godot new file mode 100644 index 0000000..fa314e9 --- /dev/null +++ b/Godot/Samples/ShrinkSDK.Godot.Sample/project.godot @@ -0,0 +1,30 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="ShrinkSDK Godot Sample" +run/main_scene="res://Main.tscn" +config/features=PackedStringArray("4.6", "C#") + +[display] + +window/size/viewport_width=960 +window/size/viewport_height=540 + +[dotnet] + +project/assembly_name="ShrinkSDK.Godot.Sample" + +[rendering] + +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" +textures/vram_compression/import_etc2_astc=true diff --git a/Godot/ShrinkSDK.Godot.slnx b/Godot/ShrinkSDK.Godot.slnx new file mode 100644 index 0000000..d6698d4 --- /dev/null +++ b/Godot/ShrinkSDK.Godot.slnx @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Godot/Tests/CodeGenFixture/CodeGenFixture.csproj b/Godot/Tests/CodeGenFixture/CodeGenFixture.csproj new file mode 100644 index 0000000..2fb62d1 --- /dev/null +++ b/Godot/Tests/CodeGenFixture/CodeGenFixture.csproj @@ -0,0 +1,20 @@ + + + Exe + net8.0 + ShrinkSDK.CodeGenFixture + true + true + $(FixtureStrongNameKeyFile) + <_ShrinkCodeGenTaskAssembly>$(MSBuildThisFileDirectory)..\..\CodeGen\ShrinkSDK.CodeGen.Task\bin\$(Configuration)\net8.0\ShrinkSDK.CodeGen.Task.dll + + + + + + + + + + + diff --git a/Godot/Tests/CodeGenFixture/Program.cs b/Godot/Tests/CodeGenFixture/Program.cs new file mode 100644 index 0000000..c02debc --- /dev/null +++ b/Godot/Tests/CodeGenFixture/Program.cs @@ -0,0 +1,79 @@ +#nullable enable + +using System; +using System.Linq; +using ShrinkCommand; +using ShrinkEventBus; +using ShrinkNetwork; +using ShrinkNetwork.Integration; +using ShrinkSDK.Runtime; + +var assembly = typeof(Program).Assembly; +var marker = assembly.GetCustomAttributes(typeof(ShrinkCodeGenWovenAttribute), false) + .Cast() + .SingleOrDefault() ?? throw new InvalidOperationException("CodeGen marker is missing."); +if (marker.WeaverVersion != "0.1.0" || !Guid.TryParse(marker.InputMvid, out _)) + throw new InvalidOperationException("CodeGen marker is invalid."); + +if (!typeof(InstanceSubscriber).GetInterfaces().Contains(typeof(IShrinkGeneratedSubscriber))) + throw new InvalidOperationException("Generated subscriber interface is missing."); + +using var binding = EventBus.Attach(new InstanceSubscriber()); +EventBus.Post(new FixtureEvent()); +if (InstanceSubscriber.Calls != 1 || StaticSubscriber.Calls != 1) + throw new InvalidOperationException($"Generated EventBus bindings failed: instance={InstanceSubscriber.Calls}, static={StaticSubscriber.Calls}."); + +if (assembly.GetCustomAttributes(typeof(ShrinkCommandStaticRegistryAttribute), false).Length != 1) + throw new InvalidOperationException("Command registry metadata is missing."); +if (assembly.GetCustomAttributes(typeof(ShrinkNetworkMessageRegistryAttribute), false).Length != 1) + throw new InvalidOperationException("Network message registry metadata is missing."); +if (assembly.GetCustomAttributes(typeof(ShrinkNetworkStaticSubscriberRegistryAttribute), false).Length != 1) + throw new InvalidOperationException("Network subscriber registry metadata is missing."); +var bridgeRegistrations = (System.Collections.IEnumerable)(typeof(ShrinkNetworkEventRegistry) + .GetMethod("Snapshot", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)! + .Invoke(null, null) ?? throw new InvalidOperationException("Network EventBus registry snapshot failed.")); +if (!bridgeRegistrations.Cast().Any(item => item.GetType().GetProperty("EventType")?.GetValue(item) as Type == typeof(FixtureNetworkEvent))) + throw new InvalidOperationException("Generated Network/EventBus binding is missing."); + +Console.WriteLine($"PASS CodeGen {marker.WeaverVersion}: EventBus + Command + Network + Network/EventBus registries"); + +public readonly struct FixtureEvent : IShrinkEvent { } + +[ShrinkEventSubscriber] +public sealed class InstanceSubscriber +{ + public static int Calls; + + [ShrinkSubscribe] + private void OnEvent(FixtureEvent value) => Calls++; +} + +[ShrinkEventSubscriber] +public static class StaticSubscriber +{ + public static int Calls; + + [ShrinkSubscribe] + private static void OnEvent(FixtureEvent value) => Calls++; +} + +[ShrinkCommandSubscriber] +public static class FixtureCommands +{ + [ShrinkCommand("fixture/ping")] + public static void Ping() { } +} + +[ShrinkNetworkMessage(41001, "fixture/ping")] +public sealed class FixtureMessage : IShrinkNetworkMessage { } + +[ShrinkNetworkEvent] +[ShrinkNetworkMessage(41002, "fixture/event")] +public sealed class FixtureNetworkEvent : IShrinkEvent, IShrinkNetworkMessage { } + +[ShrinkNetworkSubscriber] +public static class FixtureNetworkHandlers +{ + [ShrinkNetworkSubscribe] + public static void Handle(FixtureMessage message) { } +} diff --git a/Godot/Tests/CodeGenValidation/CodeGenValidation.csproj b/Godot/Tests/CodeGenValidation/CodeGenValidation.csproj new file mode 100644 index 0000000..f9fd7d9 --- /dev/null +++ b/Godot/Tests/CodeGenValidation/CodeGenValidation.csproj @@ -0,0 +1,12 @@ + + + Exe + net8.0 + ShrinkSDK.CodeGenValidation + false + + + + + + diff --git a/Godot/Tests/CodeGenValidation/Program.cs b/Godot/Tests/CodeGenValidation/Program.cs new file mode 100644 index 0000000..af46736 --- /dev/null +++ b/Godot/Tests/CodeGenValidation/Program.cs @@ -0,0 +1,163 @@ +#nullable enable + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Mono.Cecil; +using ShrinkSDK.CodeGen; + +if (args.Length == 2 && args[0] == "--marker") +{ + using var exported = AssemblyDefinition.ReadAssembly(args[1]); + var marker = exported.CustomAttributes.SingleOrDefault(attribute => + attribute.AttributeType.FullName == "ShrinkSDK.Runtime.ShrinkCodeGenWovenAttribute"); + Require(marker != null, "exported assembly has no ShrinkSDK CodeGen marker"); + Require(exported.MainModule.Types.Any(type => type.FullName == + "ShrinkEventBus.Generated.ShrinkGeneratedStaticBindings"), + "exported assembly has no static EventBus bootstrap"); + Console.WriteLine($"PASS exported assembly woven by {marker!.ConstructorArguments[0].Value}"); + return; +} + +if ((args.Length == 3 || args.Length == 4) && args[0] == "--resign") +{ + var signedAssembly = Path.GetFullPath(args[1]); + var keepOutput = args.Length == 4; + var output = keepOutput + ? Path.GetFullPath(args[3]) + : Path.Combine(Path.GetTempPath(), "ShrinkSDK.CodeGen.Resigned." + Guid.NewGuid().ToString("N") + ".dll"); + var outputPdb = Path.ChangeExtension(output, ".pdb"); + try + { + var result = ShrinkAssemblyWeaver.Weave(signedAssembly, Path.ChangeExtension(signedAssembly, ".pdb"), + BuildReferences(Path.GetDirectoryName(signedAssembly)!), output, outputPdb, + ShrinkCodeGenPlatform.EngineNeutral, args[2]); + Require(result.Succeeded && result.Changed, + "signed assembly re-weave failed: " + string.Join(" | ", result.Diagnostics.Select(item => item.Message))); + using var resigned = AssemblyDefinition.ReadAssembly(output); + Require(resigned.Name.HasPublicKey, "re-signed output has no public key"); + Console.WriteLine("PASS signed assembly woven and re-signed with explicit key"); + } + finally + { + if (!keepOutput) + { + if (File.Exists(output)) File.Delete(output); + if (File.Exists(outputPdb)) File.Delete(outputPdb); + } + } + return; +} + +if (args.Length != 1 || !File.Exists(args[0])) + throw new ArgumentException("Pass the unwoven CodeGenFixture assembly path."); + +var sourceAssembly = Path.GetFullPath(args[0]); +var sourceDirectory = Path.GetDirectoryName(sourceAssembly)!; +var sourcePdb = Path.ChangeExtension(sourceAssembly, ".pdb"); +var references = BuildReferences(sourceDirectory); +var root = Path.Combine(Path.GetTempPath(), "ShrinkSDK.CodeGen.Validation", Guid.NewGuid().ToString("N")); +Directory.CreateDirectory(root); +try +{ + var woven = Path.Combine(root, "fixture.woven.dll"); + var wovenPdb = Path.Combine(root, "fixture.woven.pdb"); + var first = ShrinkAssemblyWeaver.Weave(sourceAssembly, sourcePdb, references, woven, wovenPdb); + Require(first.Succeeded && first.Changed, + "single weave did not change the assembly: " + string.Join(" | ", first.Diagnostics.Select(item => item.Message))); + Require(first.InstanceSubscribers == 1 && first.StaticSubscribers == 1 && first.RegistryEntries == 5, + $"unexpected generated counts: {first.InstanceSubscribers}/{first.StaticSubscribers}/{first.RegistryEntries}"); + VerifyStructure(woven); + + var second = ShrinkAssemblyWeaver.Weave(woven, wovenPdb, references.Append(woven), + Path.Combine(root, "twice.dll"), Path.Combine(root, "twice.pdb")); + Require(second.Succeeded && !second.Changed, "repeated weave was not idempotent"); + + var corruptPdb = Path.Combine(root, "corrupt.pdb"); + File.WriteAllText(corruptPdb, "not a portable pdb"); + var corrupt = ShrinkAssemblyWeaver.Weave(sourceAssembly, corruptPdb, references, + Path.Combine(root, "corrupt.dll"), Path.Combine(root, "corrupt.out.pdb")); + Require(!corrupt.Succeeded && !File.Exists(Path.Combine(root, "corrupt.dll")), + "damaged PDB was not rejected atomically"); + + var missing = ShrinkAssemblyWeaver.Weave(sourceAssembly, null, Array.Empty(), + Path.Combine(root, "missing.dll"), null); + Require(!missing.Succeeded, "missing dependencies were not rejected"); + + var signedInput = Path.Combine(root, "signed.dll"); + using (var assembly = AssemblyDefinition.ReadAssembly(sourceAssembly)) + { + assembly.Name.PublicKey = Enumerable.Range(0, 160).Select(value => (byte)(value + 1)).ToArray(); + assembly.Name.Attributes |= AssemblyAttributes.PublicKey; + assembly.Write(signedInput); + } + var signed = ShrinkAssemblyWeaver.Weave(signedInput, null, references, + Path.Combine(root, "signed.out.dll"), null); + Require(!signed.Succeeded && signed.Diagnostics.Any(item => item.Message.Contains("Signed assemblies")), + "signed assembly was not rejected"); + + var parallel = Enumerable.Range(0, 4).Select(index => Task.Run(() => + { + var output = Path.Combine(root, $"parallel-{index}.dll"); + var outputPdb = Path.Combine(root, $"parallel-{index}.pdb"); + return ShrinkAssemblyWeaver.Weave(sourceAssembly, sourcePdb, references, output, outputPdb); + })).ToArray(); + await Task.WhenAll(parallel); + Require(parallel.All(task => task.Result.Succeeded && task.Result.Changed), + "parallel weaving of independent assemblies failed"); + + Console.WriteLine("PASS Cecil structure + single/repeat/parallel weave + corrupt PDB/signed/missing dependency guards"); +} +finally +{ + Directory.Delete(root, true); +} + +static void VerifyStructure(string path) +{ + using var assembly = AssemblyDefinition.ReadAssembly(path); + var module = assembly.MainModule; + var instance = module.Types.Single(type => type.Name == "InstanceSubscriber"); + Require(instance.Interfaces.Any(item => item.InterfaceType.FullName == "ShrinkEventBus.IShrinkGeneratedSubscriber"), + "generated subscriber interface is missing"); + Require(instance.Methods.Any(method => method.Name == "ShrinkEventBus.IShrinkGeneratedSubscriber.AttachGenerated"), + "AttachGenerated is missing"); + Require(module.Types.Any(type => type.FullName == "ShrinkEventBus.Generated.ShrinkGeneratedStaticBindings"), + "static binding bootstrap is missing"); + Require(module.Types.Any(type => type.FullName == + "ShrinkNetwork.Integration.Generated.ShrinkGeneratedNetworkEventBindings"), + "Network/EventBus generated bootstrap is missing"); + var moduleInitializer = module.Types.Single(type => type.Name == "").Methods.Single(method => method.Name == ".cctor"); + Require(moduleInitializer.Body.Instructions.Any(instruction => instruction.Operand is MethodReference method && method.Name == "Register"), + "module initializer does not call static registration"); + var attributes = assembly.CustomAttributes.Select(attribute => attribute.AttributeType.FullName).ToArray(); + foreach (var expected in new[] + { + "ShrinkSDK.Runtime.ShrinkCodeGenWovenAttribute", + "ShrinkCommand.ShrinkCommandStaticRegistryAttribute", + "ShrinkNetwork.ShrinkNetworkMessageRegistryAttribute", + "ShrinkNetwork.ShrinkNetworkStaticSubscriberRegistryAttribute" + }) + Require(attributes.Contains(expected), $"assembly registry is missing: {expected}"); +} + +static void Require(bool condition, string message) +{ + if (!condition) throw new InvalidOperationException(message); +} + +static string[] BuildReferences(string sourceDirectory) +{ + var referencePackRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".nuget", "packages", "microsoft.netcore.app.ref"); + var referencePack = Directory.GetDirectories(referencePackRoot) + .Select(path => Path.Combine(path, "ref", "net8.0")) + .Where(Directory.Exists) + .OrderByDescending(path => path, StringComparer.OrdinalIgnoreCase) + .First(); + return Directory.GetFiles(sourceDirectory, "*.dll") + .Concat(Directory.GetFiles(referencePack, "*.dll")) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); +} diff --git a/Godot/Tests/InstallerFixture/InstallerFixture.csproj b/Godot/Tests/InstallerFixture/InstallerFixture.csproj new file mode 100644 index 0000000..2831408 --- /dev/null +++ b/Godot/Tests/InstallerFixture/InstallerFixture.csproj @@ -0,0 +1,10 @@ + + + Exe + net8.0 + ShrinkSDK.InstallerFixture + + + + + diff --git a/Godot/Tests/InstallerFixture/Program.cs b/Godot/Tests/InstallerFixture/Program.cs new file mode 100644 index 0000000..39ba886 --- /dev/null +++ b/Godot/Tests/InstallerFixture/Program.cs @@ -0,0 +1,65 @@ +#nullable enable + +using System; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using ShrinkSDK.Godot.Editor; +using ShrinkSDK.Installer; + +var root = Path.Combine(Path.GetTempPath(), "ShrinkSDK.InstallerFixture", Guid.NewGuid().ToString("N")); +Directory.CreateDirectory(root); +try +{ + var project = Path.Combine(root, "Fixture.csproj"); + File.WriteAllText(project, """ + + net8.0keep + + + 3.4.5 + + + """); + var config = Path.Combine(root, "NuGet.Config"); + File.WriteAllText(config, """ + + """); + + ShrinkProjectPackageEditor.SetPackageReference(project, "ShrinkSDK.EventBus", "2.1.0"); + ShrinkProjectPackageEditor.SetPackageReference(project, "ShrinkSDK.EventBus", "2.1.0"); + var installed = XDocument.Load(project); + Require(installed.Descendants("PackageReference").Count(element => + (string?)element.Attribute("Include") == "ShrinkSDK.EventBus") == 1, "package reference was duplicated"); + Require(installed.ToString().Contains("Existing.Package") && installed.ToString().Contains("FixtureValue"), + "unrelated project content was changed"); + var installedVersions = ShrinkProjectPackageEditor.ReadPackageReferences(project); + Require(installedVersions["ShrinkSDK.EventBus"] == "2.1.0", "installed version was not read correctly"); + Require(installedVersions["Child.Version.Package"] == "3.4.5", "child Version element was not read correctly"); + Require(ShrinkPackageVersion.Classify("2.2.0", "2.1.0") == ShrinkPackageVersionRelation.Newer, + "newer installed version was classified as outdated"); + Require(ShrinkPackageVersion.Classify("2.1.0-preview.10", "2.1.0-preview.2") == + ShrinkPackageVersionRelation.Newer, "prerelease versions were not ordered semantically"); + + ShrinkProjectPackageEditor.SetPackageReference(project, "ShrinkSDK.EventBus", null); + Require(!XDocument.Load(project).Descendants("PackageReference").Any(element => + (string?)element.Attribute("Include") == "ShrinkSDK.EventBus"), "package reference was not removed"); + + ShrinkProjectPackageEditor.EnsureShrinkFeed(config); + ShrinkProjectPackageEditor.EnsureShrinkFeed(config); + var sources = XDocument.Load(config).Descendants("add").ToArray(); + Require(sources.Count(element => (string?)element.Attribute("key") == "ShrinkSDK") == 1, + "ShrinkSDK feed was duplicated"); + Require(sources.Any(element => (string?)element.Attribute("key") == "Existing"), + "existing NuGet source was removed"); + Console.WriteLine("PASS Godot Installer atomic package/feed install-update-remove merge"); +} +finally +{ + Directory.Delete(root, true); +} + +static void Require(bool condition, string message) +{ + if (!condition) throw new InvalidOperationException(message); +} diff --git a/Godot/Tests/InvalidCodeGenFixture/DuplicateContracts.cs b/Godot/Tests/InvalidCodeGenFixture/DuplicateContracts.cs new file mode 100644 index 0000000..6bf6e6b --- /dev/null +++ b/Godot/Tests/InvalidCodeGenFixture/DuplicateContracts.cs @@ -0,0 +1,7 @@ +using ShrinkNetwork; + +[ShrinkNetworkMessage(49999, "duplicate")] +public sealed class DuplicateMessageA : IShrinkNetworkMessage { } + +[ShrinkNetworkMessage(49999, "duplicate")] +public sealed class DuplicateMessageB : IShrinkNetworkMessage { } diff --git a/Godot/Tests/InvalidCodeGenFixture/InvalidCodeGenFixture.csproj b/Godot/Tests/InvalidCodeGenFixture/InvalidCodeGenFixture.csproj new file mode 100644 index 0000000..ad7888f --- /dev/null +++ b/Godot/Tests/InvalidCodeGenFixture/InvalidCodeGenFixture.csproj @@ -0,0 +1,15 @@ + + + net8.0 + ShrinkSDK.InvalidCodeGenFixture + true + false + <_ShrinkCodeGenTaskAssembly>$(MSBuildThisFileDirectory)..\..\CodeGen\ShrinkSDK.CodeGen.Task\bin\$(Configuration)\net8.0\ShrinkSDK.CodeGen.Task.dll + + + + + + + + diff --git a/Godot/Tests/ModFixture/FixtureMod.cs b/Godot/Tests/ModFixture/FixtureMod.cs new file mode 100644 index 0000000..6a38ebf --- /dev/null +++ b/Godot/Tests/ModFixture/FixtureMod.cs @@ -0,0 +1,27 @@ +using ShrinkCommand; +using ShrinkEventBus; +using ShrinkModFramework; + +[ShrinkMod("fixture.mod", "Fixture Mod", "1.0.0")] +public sealed class FixtureMod : ShrinkModBase, IShrinkModUnload +{ + public bool Ready { get; private set; } + public int UnloadCalls { get; private set; } + public override void OnReady(ShrinkModContext context) => Ready = true; + public void OnUnload(ShrinkModContext context) => UnloadCalls++; +} + +public readonly struct ModFixtureEvent : IShrinkEvent { } + +[ShrinkEventSubscriber] +public static class ModFixtureEvents +{ + public static int Calls { get; private set; } + [ShrinkSubscribe] private static void Handle(ModFixtureEvent value) => Calls++; +} + +[ShrinkCommandSubscriber] +public static class ModFixtureCommands +{ + [ShrinkCommand("mod-fixture/ping")] public static void Ping() { } +} diff --git a/Godot/Tests/ModFixture/ModFixture.csproj b/Godot/Tests/ModFixture/ModFixture.csproj new file mode 100644 index 0000000..866771b --- /dev/null +++ b/Godot/Tests/ModFixture/ModFixture.csproj @@ -0,0 +1,15 @@ + + + netstandard2.1 + ShrinkSDK.ModFixture + true + <_ShrinkCodeGenTaskAssembly>$(MSBuildThisFileDirectory)..\..\CodeGen\ShrinkSDK.CodeGen.Task\bin\$(Configuration)\net8.0\ShrinkSDK.CodeGen.Task.dll + + + + + + + + + diff --git a/Godot/Tests/ModHostFixture/ModHostFixture.csproj b/Godot/Tests/ModHostFixture/ModHostFixture.csproj new file mode 100644 index 0000000..adf1ac4 --- /dev/null +++ b/Godot/Tests/ModHostFixture/ModHostFixture.csproj @@ -0,0 +1,13 @@ + + + Exe + net8.0 + ShrinkSDK.ModHostFixture + false + + + + + + + diff --git a/Godot/Tests/ModHostFixture/Program.cs b/Godot/Tests/ModHostFixture/Program.cs new file mode 100644 index 0000000..0c46b05 --- /dev/null +++ b/Godot/Tests/ModHostFixture/Program.cs @@ -0,0 +1,37 @@ +#nullable enable + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using ShrinkCommand; +using ShrinkEventBus; +using ShrinkModFramework.Godot; + +if (args.Length != 1 || !File.Exists(args[0])) + throw new ArgumentException("Pass the woven mod assembly path."); + +using var loader = new ShrinkGodotModLoader(); +var instances = loader.Load(args[0]); +if (instances.Count != 1) throw new InvalidOperationException("Mod entry was not discovered."); +var mod = instances[0]; +var type = mod.GetType(); +if (type.GetProperty("Ready")?.GetValue(mod) is not true) + throw new InvalidOperationException("Mod lifecycle did not reach OnReady."); +var assembly = type.Assembly; +if (assembly.GetCustomAttributes(typeof(ShrinkCommandStaticRegistryAttribute), false).Length != 1) + throw new InvalidOperationException("Mod command registry metadata is missing."); +var eventType = assembly.GetType("ModFixtureEvent") ?? throw new InvalidOperationException("Mod event is missing."); +var post = typeof(EventBus).GetMethods(BindingFlags.Public | BindingFlags.Static) + .Single(method => method.Name == "Post" && method.IsGenericMethodDefinition && method.GetParameters().Length == 1) + .MakeGenericMethod(eventType); +post.Invoke(null, new[] { Activator.CreateInstance(eventType) }); +var eventSubscriber = assembly.GetType("ModFixtureEvents") ?? throw new InvalidOperationException("Mod static subscriber is missing."); +if ((int?)eventSubscriber.GetProperty("Calls", BindingFlags.Public | BindingFlags.Static)?.GetValue(null) != 1) + throw new InvalidOperationException("Mod static EventBus binding did not run."); +if (!loader.Unload(args[0])) throw new InvalidOperationException("Mod unload failed."); +if ((int?)type.GetProperty("UnloadCalls")?.GetValue(mod) != 1) + throw new InvalidOperationException("Mod OnUnload was not called."); +if (loader.LoadedAssemblyPaths.Count != 0) + throw new InvalidOperationException("Mod remained registered after unload."); +Console.WriteLine("PASS external netstandard2.1 mod CodeGen + Godot AssemblyLoadContext lifecycle"); diff --git a/Godot/Tests/NuGetConsumer/NuGetConsumer.csproj b/Godot/Tests/NuGetConsumer/NuGetConsumer.csproj new file mode 100644 index 0000000..b58691f --- /dev/null +++ b/Godot/Tests/NuGetConsumer/NuGetConsumer.csproj @@ -0,0 +1,12 @@ + + + Exe + net8.0 + ShrinkSDK.NuGetConsumer + + + + + + + diff --git a/Godot/Tests/NuGetConsumer/Program.cs b/Godot/Tests/NuGetConsumer/Program.cs new file mode 100644 index 0000000..a6ee298 --- /dev/null +++ b/Godot/Tests/NuGetConsumer/Program.cs @@ -0,0 +1,46 @@ +#nullable enable + +using System; +using System.Linq; +using ShrinkCommand; +using ShrinkEventBus; +using ShrinkNetwork; +using ShrinkSDK.Runtime; + +var assembly = typeof(Program).Assembly; +var marker = assembly.GetCustomAttributes(typeof(ShrinkCodeGenWovenAttribute), false) + .Cast().SingleOrDefault() + ?? throw new InvalidOperationException("buildTransitive CodeGen did not run."); +using var binding = EventBus.Attach(new NuGetSubscriber()); +EventBus.Post(new NuGetEvent()); +if (NuGetSubscriber.Calls != 1 || NuGetStaticSubscriber.Calls != 1) + throw new InvalidOperationException("NuGet EventBus weaving failed."); +if (assembly.GetCustomAttributes(typeof(ShrinkCommandStaticRegistryAttribute), false).Length != 1 || + assembly.GetCustomAttributes(typeof(ShrinkNetworkMessageRegistryAttribute), false).Length != 1) + throw new InvalidOperationException("NuGet registries are missing."); +Console.WriteLine($"PASS NuGet buildTransitive CodeGen {marker.WeaverVersion}"); + +public readonly struct NuGetEvent : IShrinkEvent { } + +[ShrinkEventSubscriber] +public sealed class NuGetSubscriber +{ + public static int Calls; + [ShrinkSubscribe] private void Handle(NuGetEvent value) => Calls++; +} + +[ShrinkEventSubscriber] +public static class NuGetStaticSubscriber +{ + public static int Calls; + [ShrinkSubscribe] private static void Handle(NuGetEvent value) => Calls++; +} + +[ShrinkCommandSubscriber] +public static class NuGetCommands +{ + [ShrinkCommand("nuget/smoke")] public static void Smoke() { } +} + +[ShrinkNetworkMessage(43001, "nuget/smoke")] +public sealed class NuGetMessage : IShrinkNetworkMessage { } diff --git a/Godot/Tests/UnityAdapterCompile/UnityAdapterCompile.csproj b/Godot/Tests/UnityAdapterCompile/UnityAdapterCompile.csproj new file mode 100644 index 0000000..0b32159 --- /dev/null +++ b/Godot/Tests/UnityAdapterCompile/UnityAdapterCompile.csproj @@ -0,0 +1,23 @@ + + + net472 + 9.0 + enable + false + ShrinkSDK.UnityAdapterCompile + + + + + + + + D:\UnityEditor\2022.3.62f3\Editor\Data\Managed\Unity.CompilationPipeline.Common.dll + false + + + ..\..\..\Library\PackageCache\com.unity.nuget.mono-cecil@1.11.4\Mono.Cecil.dll + false + + +