#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 ShrinkApp; using ShrinkDataSaver; using ShrinkEventBus; using ShrinkNetwork; using ShrinkSDK.Runtime; namespace ShrinkSDK.Godot; public sealed class ShrinkGodotRuntime : IShrinkMainThreadDispatcher, IShrinkDeferredMainThreadDispatcher, IShrinkLogger, IShrinkClock, IShrinkPathProvider, IShrinkScreenshotProvider, IShrinkApplicationLifecycle { private readonly ConcurrentQueue _dispatchQueue = new(); private readonly Dictionary _networkServices = new(); private readonly object _dispatchGate = new(); private KeyValuePair[] _networkSnapshot = []; private readonly SemaphoreSlim _lifecycleGate = new(1, 1); private Node? _owner; private int _mainThreadId; private volatile bool _initialized; private volatile bool _shutdown; private bool _networkPumpRunning; private ShrinkAppRuntime? _appRuntime; public event Action? Paused; public event Action? Resumed; public event Action? Exiting; public int DispatchBudgetPerFrame { get; set; } = 256; public int NetworkDispatchBudgetPerFrame { get; set; } = 256; public bool IsInitialized => _initialized; public bool IsShutdown => _shutdown; 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 void Initialize(Node owner) { if (owner == null) throw new ArgumentNullException(nameof(owner)); if (_shutdown) throw new InvalidOperationException("ShrinkGodotRuntime has already shut down."); if (_initialized) { if (!ReferenceEquals(_owner, owner)) throw new InvalidOperationException("ShrinkGodotRuntime is already bound to another owner."); return; } _owner = owner; _mainThreadId = Thread.CurrentThread.ManagedThreadId; ShrinkRuntimeServices.Configure(this, this, this, this, this, this); ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(this); ShrinkDataSaverPlatform.Configure(this, this, this, this); _initialized = true; } public void Tick(double delta) { if (!_initialized || _shutdown) return; RequireMainThread(); var budget = Math.Max(1, DispatchBudgetPerFrame); var count = Math.Min(budget, _dispatchQueue.Count); while (count-- > 0 && _dispatchQueue.TryDequeue(out var action)) { try { action(); } catch (Exception exception) { Log(ShrinkLogLevel.Error, "Godot dispatch callback failed.", exception); } } PumpNetworkAsync().Forget(); } public void Notify(int what) { if (_shutdown) return; switch ((long)what) { case MainLoop.NotificationApplicationPaused: case MainLoop.NotificationApplicationFocusOut: Paused?.Invoke(); break; case MainLoop.NotificationApplicationResumed: case MainLoop.NotificationApplicationFocusIn: Resumed?.Invoke(); break; case Node.NotificationWMCloseRequest: case Node.NotificationPredelete: Exiting?.Invoke(); break; } } public async UniTask StartAppAsync(ShrinkAppSettings? settings = null, ShrinkAppServices? services = null) { await _lifecycleGate.WaitAsync(); try { if (!_initialized) throw new InvalidOperationException("Initialize the Godot runtime before starting the app."); if (_shutdown) throw new InvalidOperationException("ShrinkGodotRuntime has already shut down."); if (_appRuntime?.IsRunning == true) return _appRuntime; if (_appRuntime != null) { await _appRuntime.ShutdownAsync(); _appRuntime = null; } _appRuntime = new ShrinkAppRuntime(_owner, settings, services); await _appRuntime.StartAsync(); return _appRuntime; } finally { _lifecycleGate.Release(); } } public async UniTask ShutdownAsync() { await _lifecycleGate.WaitAsync(); try { if (_shutdown) return; var appRuntime = _appRuntime; _appRuntime = null; try { if (appRuntime != null) await appRuntime.ShutdownAsync(); } finally { lock (_dispatchGate) { _shutdown = true; while (_dispatchQueue.TryDequeue(out _)) { } } foreach (var pair in _networkServices) { pair.Key.DispatchScheduler = ShrinkNetworkDispatchSchedulers.Inline; pair.Value.Dispose(); } _networkServices.Clear(); _networkSnapshot = []; } } finally { _lifecycleGate.Release(); } } public async UniTask StopAppAsync() { await _lifecycleGate.WaitAsync(); try { if (_shutdown || _appRuntime == null) return; var appRuntime = _appRuntime; _appRuntime = null; await appRuntime.ShutdownAsync(); } finally { _lifecycleGate.Release(); } } public bool TryPost(Action action) { if (action == null) throw new ArgumentNullException(nameof(action)); lock (_dispatchGate) { if (!_initialized || _shutdown) return false; _dispatchQueue.Enqueue(action); return true; } } public bool TryPostDeferred(Action action) => TryPost(action); public bool AddNetworkService(ShrinkNetworkService service) { if (service == null) throw new ArgumentNullException(nameof(service)); if (!_initialized || _shutdown) return false; RequireMainThread(); if (_networkServices.ContainsKey(service)) return true; var queue = new ShrinkNetworkDispatchQueue(Math.Max(1, NetworkDispatchBudgetPerFrame * 4)); service.DispatchScheduler = queue; _networkServices.Add(service, queue); UpdateNetworkSnapshot(); return true; } public bool RemoveNetworkService(ShrinkNetworkService service) { if (service == null) throw new ArgumentNullException(nameof(service)); RequireMainThread(); if (!_networkServices.Remove(service, out var queue)) return false; service.DispatchScheduler = ShrinkNetworkDispatchSchedulers.Inline; queue.Dispose(); UpdateNetworkSnapshot(); 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(); if (_owner == null || !GodotObject.IsInstanceValid(_owner)) throw new InvalidOperationException("ShrinkGodotRuntime has no valid owner viewport."); var image = _owner.GetViewport().GetTexture().GetImage(); return ValueTask.FromResult(image.SavePngToBuffer()); } private void RequireMainThread() { if (!IsMainThread) throw new InvalidOperationException("Godot lifecycle and network registration require the owner thread."); } private void UpdateNetworkSnapshot() { _networkSnapshot = new KeyValuePair[_networkServices.Count]; ((ICollection>)_networkServices).CopyTo(_networkSnapshot, 0); } private async UniTaskVoid PumpNetworkAsync() { if (_networkPumpRunning || _shutdown) return; _networkPumpRunning = true; try { foreach (var pair in _networkSnapshot) { if (_shutdown) break; if (_networkServices.TryGetValue(pair.Key, out var queue) && ReferenceEquals(queue, pair.Value)) await queue.PumpAsync(Math.Max(1, NetworkDispatchBudgetPerFrame)); } } finally { _networkPumpRunning = false; } } }