using System.Text.Json; namespace Age.Engine.Sys4; /// One SYS4INI asset entry. public sealed record AssetEntry(string Name, string Archive, long Offset, long Size); /// /// Static asset resolver. SYS4INI's file list is sectioned (one per scene: SCxxxx.BIN + its /// cross-archive asset manifest); file_number is the index within a section. So a bytecode /// resId resolves as files[section_base(scene) + resId] -- unified for graphics and audio. /// See docs/asset-resolution-re.md. Built from build/asset-index.json + build/asset-sections.json. /// public sealed class ResourceMap { private readonly IReadOnlyList _files; private readonly IReadOnlyDictionary _sceneBase; // "SC0000" -> section base index public ResourceMap(IReadOnlyList files, IReadOnlyDictionary sceneBase) { _files = files; _sceneBase = sceneBase; } public static ResourceMap Load(string indexPath, string sectionsPath) { var files = new List(); using (var idx = JsonDocument.Parse(File.ReadAllText(indexPath))) foreach (var f in idx.RootElement.GetProperty("files").EnumerateArray()) files.Add(new AssetEntry( f.GetProperty("name").GetString()!, f.GetProperty("archive").GetString()!, f.GetProperty("offset").GetInt64(), f.GetProperty("size").GetInt64())); var sceneBase = new Dictionary(StringComparer.OrdinalIgnoreCase); using (var sec = JsonDocument.Parse(File.ReadAllText(sectionsPath))) foreach (var p in sec.RootElement.GetProperty("scene_base").EnumerateObject()) sceneBase[p.Name] = p.Value.GetInt32(); return new ResourceMap(files, sceneBase); } public static ResourceMap Load() => Load(Paths.AssetIndexJson, Paths.AssetSectionsJson); /// Resolve a scene-local resId to its asset, or null if out of range / unknown scene. public AssetEntry? Resolve(string scene, long resId) { var key = scene.EndsWith(".BIN", StringComparison.OrdinalIgnoreCase) ? scene[..^4] : scene; if (!_sceneBase.TryGetValue(key, out var b)) return null; long p = b + resId; return p >= 0 && p < _files.Count ? _files[(int)p] : null; } /// Pre-converted BMP path for an AGF asset (see tools/convert_agf.py). public static string? TexturePath(AssetEntry a) { if (!a.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase)) return null; var bmp = Path.Combine(Paths.Textures, Path.GetFileNameWithoutExtension(a.Name) + ".BMP"); return File.Exists(bmp) ? bmp : null; } }