Wire asset resolution into a live Godot render. VM executes set-texture -> ResourceMap resolves (scene,resId) -> files[section_base+resId] across all archives -> pre-converted BMP -> composite behind the dialogue. The full-screen event-CG layer (EV052*) renders end-to-end from the executed bytecode. - Age.Engine/Sys4/ResourceMap.cs: Resolve + BMP TexturePath; Paths: asset JSONs - GodotAdvHost: create/set/draw-texture -> TextureRect in a _stage layer - IHost.DrawTexture + VM dispatch extended with dst x/y (draw-texture args 7/8) - project.godot 800x600; convert_agf.py all-archive + --scene batch - engine 8/8, C# --selftest still byte-matches vm0 trace (VM behaviour unchanged) Known limitations (next chunk = graphics geometry/blend subsystem): - sprites + BG* via the CG-load subroutine get garbage dst/size — native ops stubbed (0x208 get-texture-size + sprite position/anim chain) - AE* fades draw opaque/instant (no alpha); no chromakey - slot model approximates the game's immediate-mode blit-onto-slot-0 canvas - AGF pre-converted to BMP offline (runtime decoder deferred) See docs/phase-a-slice-plan.md (A2b section). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
64 lines
2.8 KiB
C#
64 lines
2.8 KiB
C#
using System.Text.Json;
|
|
|
|
namespace Age.Engine.Sys4;
|
|
|
|
/// <summary>One SYS4INI asset entry.</summary>
|
|
public sealed record AssetEntry(string Name, string Archive, long Offset, long Size);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public sealed class ResourceMap
|
|
{
|
|
private readonly IReadOnlyList<AssetEntry> _files;
|
|
private readonly IReadOnlyDictionary<string, int> _sceneBase; // "SC0000" -> section base index
|
|
|
|
public ResourceMap(IReadOnlyList<AssetEntry> files, IReadOnlyDictionary<string, int> sceneBase)
|
|
{
|
|
_files = files;
|
|
_sceneBase = sceneBase;
|
|
}
|
|
|
|
public static ResourceMap Load(string indexPath, string sectionsPath)
|
|
{
|
|
var files = new List<AssetEntry>();
|
|
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<string, int>(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);
|
|
|
|
/// <summary>Resolve a scene-local resId to its asset, or null if out of range / unknown scene.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Pre-converted BMP path for an AGF asset (see tools/convert_agf.py).</summary>
|
|
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;
|
|
}
|
|
}
|