using System.Text.Json; using Age.Engine.Diagnostics; using Age.Engine.Hosting; using Age.Engine.Model; using Age.Engine.Persistence; namespace Age.Engine.Vm; /// /// A persistent store carried across scenes. AGE keeps separate integer, float, string, and pointer /// banks; 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). /// /// Seam rule: references only Model + Hosting (never Sys4). The /// is unchanged — its globals are pre-seeded before Run 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). /// public sealed class GameSession { public Dictionary Globals { get; } = new(); public Dictionary GlobalFloats { get; } = new(); public Dictionary GlobalStrings { get; } = new(); public Dictionary GlobalPointers { get; } = new(); public Dictionary GlobalStringPointers { get; } = new(); /// AGE's selected profile-wide cells plus native shared SAVE.DAT/RT.DAT lifecycle. public SharedProfile SharedProfile { get; } /// Native shared/numbered save directory service used by persistence opcodes. public INativeDatStore? NativeDatStore { get; } /// AGE's profile-lifetime sound:* settings registry, independent of SAVE.DAT. public AudioMixerSettings AudioMixerSettings { get; } /// The live retained ADV backlog shared by every VM run in this session. public AdvTextHistory TextHistory { get; } = new(); public GameSession(SharedProfile? sharedProfile = null, INativeDatStore? nativeDatStore = null, AudioMixerSettings? audioMixerSettings = null) { SharedProfile = sharedProfile ?? new SharedProfile(); NativeDatStore = nativeDatStore; AudioMixerSettings = audioMixerSettings ?? new AudioMixerSettings(); } public void Seed(int addr, long value) => Globals[addr] = value; public void SeedString(int addr, string value) => GlobalStrings[addr] = value; /// Run one scene: seed a fresh VM from session state, execute, merge final state back. public SceneResult RunScene(Script script, OpcodeTable table, IHost host, VmOptions? options = null, IScriptProvider? provider = null, ITraceSink? sink = null) { var vm = new VirtualMachine( script, table, host, options, provider, sink, TextHistory, SharedProfile, NativeDatStore, AudioMixerSettings); foreach (var kv in Globals) vm.Globals[kv.Key] = kv.Value; foreach (var kv in GlobalFloats) vm.GlobalFloats[kv.Key] = kv.Value; foreach (var kv in GlobalStrings) vm.GlobalStrings[kv.Key] = kv.Value; foreach (var kv in GlobalPointers) vm.GlobalPointers[kv.Key] = kv.Value; foreach (var kv in GlobalStringPointers) vm.GlobalStringPointers[kv.Key] = kv.Value; vm.Run(); // Each native bank is shared by the session; last write wins within that bank. foreach (var kv in vm.Globals) Globals[kv.Key] = kv.Value; foreach (var kv in vm.GlobalFloats) GlobalFloats[kv.Key] = kv.Value; foreach (var kv in vm.GlobalStrings) GlobalStrings[kv.Key] = kv.Value; foreach (var kv in vm.GlobalPointers) GlobalPointers[kv.Key] = kv.Value; foreach (var kv in vm.GlobalStringPointers) GlobalStringPointers[kv.Key] = kv.Value; return new SceneResult(vm.Emitted.ToList(), vm.HaltReason, vm.Steps); } /// Serialize the persistent global banks to JSON, keyed by decimal address. /// Lets an expensive booted state be snapshotted and reused. Live is /// intentionally excluded until the unified save/profile architecture defines its lifecycle. public string ToJson() { var snap = new StateSnapshot( Globals.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value), GlobalStrings.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value)); return JsonSerializer.Serialize(snap, new JsonSerializerOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); } /// Rebuild a session from a snapshot. public static GameSession FromJson(string json) { var s = new GameSession(); var snap = JsonSerializer.Deserialize(json) ?? new StateSnapshot(new(), new()); foreach (var kv in snap.Globals) s.Globals[int.Parse(kv.Key)] = kv.Value; foreach (var kv in snap.Strings) s.GlobalStrings[int.Parse(kv.Key)] = kv.Value; return s; } private sealed record StateSnapshot(Dictionary Globals, Dictionary Strings); } /// The observable result of running one scene into a . public sealed record SceneResult(IReadOnlyList<(int Offset, string Text, string Script)> Emitted, string? Halt, long Steps);