From 500488ac0a9a1763337ce4b51d8fe18ac1f6257c Mon Sep 17 00:00:00 2001 From: cneicy Date: Sat, 5 Sep 2026 03:18:14 +0800 Subject: [PATCH] feat: add Godot C# host package --- .gitea/workflows/publish-nuget.yml | 44 +++++++++ .gitignore | 3 + NuGet.Config | 8 ++ README.md | 9 ++ ShrinkGodotHost.cs | 154 +++++++++++++++++++++++++++++ ShrinkSDK.Godot.csproj | 20 ++++ 6 files changed, 238 insertions(+) create mode 100644 .gitea/workflows/publish-nuget.yml create mode 100644 .gitignore create mode 100644 NuGet.Config create mode 100644 README.md create mode 100644 ShrinkGodotHost.cs create mode 100644 ShrinkSDK.Godot.csproj diff --git a/.gitea/workflows/publish-nuget.yml b/.gitea/workflows/publish-nuget.yml new file mode 100644 index 0000000..0003158 --- /dev/null +++ b/.gitea/workflows/publish-nuget.yml @@ -0,0 +1,44 @@ +name: Publish NuGet package + +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 + shell: bash + run: | + set -euo pipefail + tag="${GITEA_REF#refs/tags/}" + version="$(dotnet msbuild ShrinkSDK.Godot.csproj -getProperty:Version -nologo)" + test "$tag" = "v$version" + dotnet restore ShrinkSDK.Godot.csproj --configfile NuGet.Config + dotnet pack ShrinkSDK.Godot.csproj --configuration Release --no-restore --output packages --include-symbols --include-source + + - name: Publish package + 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 new file mode 100644 index 0000000..b88af1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +**/bin/ +**/obj/ +artifacts/ 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 new file mode 100644 index 0000000..e1192e8 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# ShrinkSDK.Godot + +Godot 4.6 C# 宿主与平台服务适配包。 + +```powershell +dotnet add package ShrinkSDK.Godot --version 0.1.0 +``` + +主场景根节点继承 `ShrinkGodotHost`,在 `_Ready` 中先调用 `base._Ready()`,再调用 `StartAppAsync()`。CodeGen 由依赖模块的 `buildTransitive` 自动启用。 diff --git a/ShrinkGodotHost.cs b/ShrinkGodotHost.cs new file mode 100644 index 0000000..60ffcbd --- /dev/null +++ b/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/ShrinkSDK.Godot.csproj b/ShrinkSDK.Godot.csproj new file mode 100644 index 0000000..e78124a --- /dev/null +++ b/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 + + + + + + + + + + +