diff --git a/engine/Age.Engine.Tests/Sys4ScriptProviderTests.cs b/engine/Age.Engine.Tests/Sys4ScriptProviderTests.cs new file mode 100644 index 0000000..b419f76 --- /dev/null +++ b/engine/Age.Engine.Tests/Sys4ScriptProviderTests.cs @@ -0,0 +1,20 @@ +using Age.Engine.Sys4; +using Xunit; + +public class Sys4ScriptProviderTests +{ + [Fact] + public void ResolvesKnownIdsToTheirScripts() + { + var table = OpcodeTableJson.Load(Paths.OpcodesJson); + var provider = Sys4ScriptProvider.Load(table); + + var additem = provider.GetById(0x1ab); // ADDITEM.BIN + var mes = provider.GetById(0x2ae7); // MES.BIN + Assert.NotNull(additem); + Assert.NotNull(mes); + Assert.True(additem!.Instructions.Count > 0); + Assert.Same(additem, provider.GetById(0x1ab)); // cached: same instance + Assert.Null(provider.GetById(long.MaxValue)); // unknown id + } +} diff --git a/engine/Age.Engine/Hosting/IScriptProvider.cs b/engine/Age.Engine/Hosting/IScriptProvider.cs new file mode 100644 index 0000000..bb75aaa --- /dev/null +++ b/engine/Age.Engine/Hosting/IScriptProvider.cs @@ -0,0 +1,10 @@ +using Age.Engine.Model; +namespace Age.Engine.Hosting; + +/// Resolves a call-script id (a raw SYS4INI file index) to a loaded . +/// Implemented in Sys4; injected into the VM so the Vm layer never references Sys4. +public interface IScriptProvider +{ + /// The script for this id, or null if the id maps to no known script. + Script? GetById(long id); +} diff --git a/engine/Age.Engine/Sys4/Paths.cs b/engine/Age.Engine/Sys4/Paths.cs index 2533566..fda219b 100644 --- a/engine/Age.Engine/Sys4/Paths.cs +++ b/engine/Age.Engine/Sys4/Paths.cs @@ -10,6 +10,7 @@ public static class Paths public static string OpcodesJson => Path.Combine(Build, "opcodes.json"); public static string AssetSectionsJson => Path.Combine(Build, "asset-sections.json"); public static string AssetIndexJson => Path.Combine(Build, "asset-index.json"); + public static string CallscriptNamesJson => Path.Combine(Build, "callscript-names.json"); public static string Textures => Path.Combine(Build, "textures"); private static string FindRepo() diff --git a/engine/Age.Engine/Sys4/Sys4ScriptProvider.cs b/engine/Age.Engine/Sys4/Sys4ScriptProvider.cs new file mode 100644 index 0000000..9ef9990 --- /dev/null +++ b/engine/Age.Engine/Sys4/Sys4ScriptProvider.cs @@ -0,0 +1,39 @@ +using System.Text.Json; +using Age.Engine.Hosting; +using Age.Engine.Model; +namespace Age.Engine.Sys4; + +/// Resolves call-script ids (raw SYS4INI file indices) to loaded scripts, using +/// build/callscript-names.json (id→name) + Paths.Scripts() (name→path). Cached per id. +/// The native resolver prefers a loose override before the archive; Paths.Scripts() already +/// shadows extracted/DATA1 with root overrides, so that behavior is preserved. +public sealed class Sys4ScriptProvider : IScriptProvider +{ + private readonly OpcodeTable _table; + private readonly IReadOnlyDictionary _idToName; + private readonly Dictionary _byName; // NAME(UPPER) -> path + private readonly Dictionary _cache = new(); + + public Sys4ScriptProvider(OpcodeTable table, IReadOnlyDictionary idToName, + Dictionary byName) + { _table = table; _idToName = idToName; _byName = byName; } + + public static Sys4ScriptProvider Load(OpcodeTable table) + { + var raw = JsonSerializer.Deserialize>( + File.ReadAllText(Paths.CallscriptNamesJson)) ?? new(); + var idToName = raw.ToDictionary(kv => long.Parse(kv.Key), kv => kv.Value); + return new Sys4ScriptProvider(table, idToName, Paths.Scripts()); + } + + public Script? GetById(long id) + { + if (_cache.TryGetValue(id, out var cached)) return cached; + Script? s = null; + if (_idToName.TryGetValue(id, out var name) && + _byName.TryGetValue(name.ToUpperInvariant(), out var path)) + s = Sys4Loader.Load(path, _table); + _cache[id] = s; + return s; + } +}