#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; } } }