feat(phase-b): cross-scene state — GameSession + play runner
Persistent global store carried across scenes (the engine's flat global bank), the substrate for cross-scene flow and state seeding. GameSession seeds a fresh VM from session state, runs, merges back; VM untouched so trace/selftest parity holds. Age.Cli play <SCENE...> [0xADDR=VAL] runs a sequence carrying state. Tests (engine 15/15): state persists A->B; seed visible to scene; SC0000 via session byte-identical to single run; seeding form flag 0xa57=1 changes behavior. Demonstrated: play SC0000 0xa57=1 -> 186->229 lines (Lily's form-gated dialogue executes); SC0000->SC0030 carries 76 globals. Operationalizes the state- divergence finding. Godot selftest OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
43
engine/Age.Engine/Vm/GameSession.cs
Normal file
43
engine/Age.Engine/Vm/GameSession.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
|
||||
namespace Age.Engine.Vm;
|
||||
|
||||
/// <summary>
|
||||
/// A persistent global store carried across scenes. Every scene the game runs shares one flat global
|
||||
/// bank (the engine's model); running scenes in isolation with empty state is why our headless VM
|
||||
/// diverges from the real game (the bg/sprite geometry drift, the state-gated EMPTY scenes, Lily's
|
||||
/// form-gated voices are all state divergence — see docs/phase-a-slice-plan.md A2b-Geometry).
|
||||
///
|
||||
/// <para>Seam rule: references only <c>Model</c> + <c>Hosting</c> (never <c>Sys4</c>). The
|
||||
/// <see cref="VirtualMachine"/> is unchanged — its globals are pre-seeded before <c>Run</c> and merged
|
||||
/// back after, so trace/selftest parity is untouched. Local frames are per-call and correctly do NOT
|
||||
/// persist (they live in the VM, not here).</para>
|
||||
/// </summary>
|
||||
public sealed class GameSession
|
||||
{
|
||||
public Dictionary<int, long> Globals { get; } = new();
|
||||
public Dictionary<int, string> GlobalStrings { get; } = new();
|
||||
|
||||
public void Seed(int addr, long value) => Globals[addr] = value;
|
||||
public void SeedString(int addr, string value) => GlobalStrings[addr] = value;
|
||||
|
||||
/// <summary>Run one scene: seed a fresh VM from session state, execute, merge final state back.</summary>
|
||||
public SceneResult RunScene(Script script, OpcodeTable table, IHost host, VmOptions? options = null)
|
||||
{
|
||||
var vm = new VirtualMachine(script, table, host, options);
|
||||
foreach (var kv in Globals) vm.Globals[kv.Key] = kv.Value;
|
||||
foreach (var kv in GlobalStrings) vm.GlobalStrings[kv.Key] = kv.Value;
|
||||
|
||||
vm.Run();
|
||||
|
||||
// Globals are one flat space; last write wins — the engine's single global bank.
|
||||
foreach (var kv in vm.Globals) Globals[kv.Key] = kv.Value;
|
||||
foreach (var kv in vm.GlobalStrings) GlobalStrings[kv.Key] = kv.Value;
|
||||
|
||||
return new SceneResult(vm.Emitted.ToList(), vm.HaltReason, vm.Steps);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The observable result of running one scene into a <see cref="GameSession"/>.</summary>
|
||||
public sealed record SceneResult(IReadOnlyList<(int Offset, string Text)> Emitted, string? Halt, long Steps);
|
||||
Reference in New Issue
Block a user