#nullable enable using System; using System.IO; using System.Linq; using System.Threading.Tasks; using Cysharp.Threading.Tasks; using Godot; using ShrinkApp; using ShrinkDataSaver; using ShrinkEventBus; using ShrinkSDK.Godot; public partial class WorldDemo : Node { private readonly ShrinkGodotRuntime _runtime = new(); private WorldDemoWorld? _active; public override void _Ready() { ProcessMode = ProcessModeEnum.Always; _runtime.Initialize(this); RunAsync().Forget(); } public override void _Process(double delta) => _runtime.Tick(delta); public override void _Notification(int what) => _runtime.Notify(what); private bool SwitchWorld(bool failRestore = false) { var candidate = new WorldDemoWorld(); try { AddChild(candidate); candidate.Restore(failRestore); } catch (InvalidOperationException) { candidate.Free(); return false; } var old = _active; _active = candidate; old?.Free(); candidate.Commit(); return true; } private static void Check(bool value, string message) { if (!value) throw new InvalidOperationException(message); } private async UniTaskVoid RunAsync() { try { var uninitialized = new ShrinkGodotRuntime(); var rejected = false; try { await uninitialized.StartAppAsync(); } catch (InvalidOperationException) { rejected = true; } Check(rejected, "Start must require initialization."); ShrinkGodotRuntime.RequireWoven(GetType().Assembly); await _runtime.StartAppAsync(new ShrinkAppSettings { DisabledModuleIds = ["shrink.command", "shrink.datasaver", "shrink.network"] }); var root = Path.Combine(_runtime.PersistentDataPath, "world-demo"); var storage = ShrinkGodotStorage.CreateRenameOnly(root); await storage.WriteAsync("probe", new byte[] { 1 }); await storage.WriteAsync("probe", new byte[] { 2 }); await storage.WriteAsync("probe", new byte[] { 3 }); Check((await storage.ReadAsync("probe.bak1"))[0] == 2, "First backup mismatch."); Check((await storage.ReadAsync("probe.bak2"))[0] == 1, "Second backup mismatch."); Check(SwitchWorld(), "First world failed."); var first = _active!; var firstKey = first.Key; Check(first.IsInsideTree() && first.ReadyCount == 1 && first.EffectCount == 0, "Restore must suppress effects and commit must publish ready."); Check(!SwitchWorld(true) && ReferenceEquals(_active, first) && first.IsInsideTree(), "Failed candidate replaced the active world."); Check(EventBus.CaptureNamedBuses().Count(bus => bus.Key == firstKey) == 1, "Candidate failure damaged the original bus."); Check(SwitchWorld(), "Second world failed."); Check(!GodotObject.IsInstanceValid(first) && !EventBus.TryGetBus(firstKey, out _), "Old node or subscription bus survived replacement."); var active = _active!; GetTree().Paused = true; Check(EventBus.PostDeferred(active.Bus, new DemoEffect()).Accepted, "Deferred rejected."); Check(active.EffectCount == 0, "Effect ran synchronously."); for (var frame = 0; frame < 4 && active.EffectCount == 0; frame++) await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); Check(active.EffectCount == 1, "Paused app did not pump."); GetTree().Paused = false; active.Free(); _active = null; await _runtime.ShutdownAsync(); Check(_runtime.IsShutdown && !_runtime.TryPost(() => { }), "Closed runtime accepted dispatch."); Check(!await Task.Run(() => _runtime.TryPostDeferred(() => { })), "Worker posted after shutdown."); GD.Print("SHRINK_GODOT_WORLD_PASS"); GetTree().Quit(0); } catch (Exception exception) { GD.PushError($"SHRINK_GODOT_WORLD_FAIL {exception}"); GetTree().Quit(1); } } }