109 lines
4.2 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|