Files
Workspace/Godot/Samples/ShrinkSDK.Godot.Sample/WorldDemo.cs
T

109 lines
4.2 KiB
C#

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