diff --git a/.gitea/workflows/publish-nuget.yml b/.gitea/workflows/publish-nuget.yml
new file mode 100644
index 0000000..06dc7b7
--- /dev/null
+++ b/.gitea/workflows/publish-nuget.yml
@@ -0,0 +1,52 @@
+name: Publish NuGet packages
+
+on:
+ push:
+ tags:
+ - 'v*'
+ workflow_dispatch:
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ env:
+ NUGET_AUTH_TOKEN: ${{ secrets.SHRINKSDK_PACKAGE_TOKEN }}
+ steps:
+ - name: Checkout tagged source
+ uses: actions/checkout@v4
+
+ - name: Set up .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Validate tag and pack projects
+ env:
+ GITEA_REF: ${{ gitea.ref }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ tag="${GITEA_REF#refs/tags/}"
+ version="$(node -p "require('./package.json').version")"
+ test "$tag" = "v$version"
+ mkdir -p packages
+ mapfile -d '' projects < <(find DotNet~ Godot~ -type f -name '*.csproj' -print0 2>/dev/null || true)
+ test "${#projects[@]}" -gt 0
+ for project in "${projects[@]}"; do
+ dotnet restore "$project" --configfile NuGet.Config
+ dotnet pack "$project" --configuration Release --no-restore --output "$PWD/packages" --include-symbols --include-source
+ done
+ find packages -maxdepth 1 -name '*.nupkg' -type f | grep -q .
+
+ - name: Publish packages
+ shell: bash
+ run: |
+ set -euo pipefail
+ : "${NUGET_AUTH_TOKEN:?SHRINKSDK_PACKAGE_TOKEN is required}"
+ dotnet nuget push 'packages/*.nupkg' --api-key "$NUGET_AUTH_TOKEN" --source https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json --skip-duplicate
+ if compgen -G 'packages/*.snupkg' > /dev/null; then
+ dotnet nuget push 'packages/*.snupkg' --api-key "$NUGET_AUTH_TOKEN" --source https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json --skip-duplicate
+ fi
diff --git a/.gitignore b/.gitignore
index 246935d..c3b8764 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,11 @@
/Tools~/**/[Oo]bj/
*.user
*.DotSettings.user
+/DotNet~/**/[Bb]in/
+/DotNet~/**/[Oo]bj/
+/Godot~/**/[Bb]in/
+/Godot~/**/[Oo]bj/
+/artifacts/
+/packages/
+!DotNet~/**/*.csproj
+!Godot~/**/*.csproj
diff --git a/.npmignore b/.npmignore
index 700f03d..6a59c77 100644
--- a/.npmignore
+++ b/.npmignore
@@ -6,3 +6,7 @@ Tools~/
*.sln
*.user
*.DotSettings.user
+DotNet~/
+Godot~/
+NuGet.Config
+Directory.Build.props
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..43b188b
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,13 @@
+
+
+ latest
+ enable
+ true
+ true
+ ShrinkSDK
+ ShrinkSDK
+ https://git.crash.work/ShrinkSDK
+ true
+ snupkg
+
+
diff --git a/Editor/ShrinkSdkPackageCatalog.cs b/Editor/ShrinkSdkPackageCatalog.cs
index 02dc959..17cfd30 100644
--- a/Editor/ShrinkSdkPackageCatalog.cs
+++ b/Editor/ShrinkSdkPackageCatalog.cs
@@ -73,7 +73,7 @@ namespace ShrinkSDK.Installer
"com.cneicy.shrink-context-app-adapter", "0.2.0", ShrinkSdkPackageLayer.Context, true);
internal static readonly ShrinkSdkPackageEntry ContextEventBusAdapter = Package(
"Context 事件适配", "把事件订阅纳入 Context 的安装与自动撤回。",
- "com.cneicy.shrink-context-eventbus-adapter", "0.1.1", ShrinkSdkPackageLayer.Context, true);
+ "com.cneicy.shrink-context-eventbus-adapter", "0.2.0", ShrinkSdkPackageLayer.Context, true);
internal static readonly ShrinkSdkPackageEntry AppCore = Package(
"应用宿主", "统一启动、服务门面与应用生命周期。",
@@ -127,7 +127,7 @@ namespace ShrinkSDK.Installer
"com.cneicy.shrink-network-integration-eventbus", "0.2.0", ShrinkSdkPackageLayer.Integration, true);
internal static readonly ShrinkSdkPackageEntry SharedCodeGen = Package(
"共享代码生成", "各功能包使用的编译期注册表基础设施。",
- "com.cneicy.shrink-shared-codegen", "0.1.0", ShrinkSdkPackageLayer.Tooling);
+ "com.cneicy.shrink-shared-codegen", "0.1.1", ShrinkSdkPackageLayer.Tooling);
internal static readonly ShrinkSdkPackageEntry RuntimeAbstractions = Package(
"运行时抽象", "Unity 与其他 .NET 宿主共用的平台服务合同。",
"com.cneicy.shrink-runtime-abstractions", "0.1.0", ShrinkSdkPackageLayer.Tooling);
diff --git a/Godot~/ShrinkSDK.Godot.Installer.csproj b/Godot~/ShrinkSDK.Godot.Installer.csproj
new file mode 100644
index 0000000..69ba4fe
--- /dev/null
+++ b/Godot~/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~/addons/shrinksdk/ShrinkGodotInstallerPlugin.cs b/Godot~/addons/shrinksdk/ShrinkGodotInstallerPlugin.cs
new file mode 100644
index 0000000..ec1cc0a
--- /dev/null
+++ b/Godot~/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~/addons/shrinksdk/ShrinkProjectPackageEditor.cs b/Godot~/addons/shrinksdk/ShrinkProjectPackageEditor.cs
new file mode 100644
index 0000000..7397f76
--- /dev/null
+++ b/Godot~/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~/addons/shrinksdk/plugin.cfg b/Godot~/addons/shrinksdk/plugin.cfg
new file mode 100644
index 0000000..1715616
--- /dev/null
+++ b/Godot~/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/NuGet.Config b/NuGet.Config
new file mode 100644
index 0000000..43e134b
--- /dev/null
+++ b/NuGet.Config
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 2581da7..bf61f5f 100644
--- a/README.md
+++ b/README.md
@@ -19,3 +19,13 @@ https://git.crash.work/ShrinkSDK/Installer.git#main
```
正式项目应在对应版本发布后将 `main` 换成固定 tag。安装完成后等待 Unity 编译,再打开 `ShrinkSDK/包管理`,选择单个模块或推荐组合。
+
+## Godot 4.6 C#
+
+Godot Installer 源码和 NuGet 打包工程位于 `Godot~`。安装 `ShrinkSDK.Godot.Installer 0.1.1` 后,将包内 `contentFiles/any/any/addons/shrinksdk` 复制到 Godot 项目的 `addons/shrinksdk`,再到 `Project > Project Settings > Plugins` 启用 **ShrinkSDK Installer**。
+
+Godot 面板直接修改项目 `.csproj` 的 `PackageReference`,安全合并项目级 `NuGet.Config`,并可执行 `dotnet restore/build`、显示 CodeGen 织入状态。已安装版本高于目录版本时不会执行降级。
+
+```powershell
+dotnet pack .\Godot~\ShrinkSDK.Godot.Installer.csproj -c Release --output .\artifacts --include-symbols --include-source
+```