feat(a2b): first-pass texture render — full-screen event-CG layer
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>
This commit is contained in:
@@ -81,8 +81,15 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
|
||||
branchy non-opening scenes — use the C# VM to trace those). Runtime note for future work: the game is
|
||||
**packed** (main VM logic in a per-run heap `r-x` region) and streams archives through a heap block-cache
|
||||
via `ReadFile` (not mmap); the stable AGF decoder is `AGE.EXE+0x74f1f`.
|
||||
3. **Wire the backend** (already designed — A2b-background plan Tasks 3–5): `ResourceMap` resolver +
|
||||
Godot `TextureRect` compositing; render only resolved full-screen slots. Mechanical once (1)+(2) land.
|
||||
3. **Wire the backend.** **✅ FIRST-PASS RENDER LANDED (2026-07-06).** `Age.Engine/Sys4/ResourceMap.cs`
|
||||
(Resolve + BMP path) + `GodotAdvHost` texture ops → `TextureRect` compositing behind the dialogue;
|
||||
`IHost.DrawTexture` extended with dst x/y; 800×600 window; `convert_agf.py --scene` pre-converts a
|
||||
scene's manifest AGFs → BMP. The full-screen **event-CG layer renders end-to-end** from the executed
|
||||
bytecode. **Limitations (next chunk = graphics geometry/blend):** sprites + `BG*` (routed through the
|
||||
CG-load subroutine) have garbage geometry because native graphics ops are stubbed (`0x208`
|
||||
get-texture-size + the sprite position/animation chain); fades (`AE*`) draw opaque (no alpha); slot
|
||||
model approximates the game's immediate-mode blit-onto-slot-0 canvas. See `docs/phase-a-slice-plan.md`
|
||||
(A2b section) for the full write-up + the graphics-subsystem plan.
|
||||
4. **Audio** (parallel, same shape): resolve `play-voice`/`play-bgm` `id → OGG` via SYS4INI + a
|
||||
Frida audio capture (hook `DATA3.ALF` reads or the audio-play fn); play via Godot. Reuses the
|
||||
`tools/frida/` framework.
|
||||
@@ -98,9 +105,11 @@ rendering what the executed bytecode + the map produce (never a hardcoded image)
|
||||
|
||||
## Status
|
||||
|
||||
A2b-background: **machinery landed**; **steps 1 & 2 SOLVED (static, general).** Step 1 =
|
||||
`build/asset-index.json`. Step 2 = **`resId → files[section_base(scene) + resId]`** via SYS4INI
|
||||
per-scene sections (`tools/resolve_asset.py` + `build/asset-sections.json`) — no runtime capture, works
|
||||
across all archives/types and for audio too. Remaining for the render (step 3): wire a `ResourceMap`
|
||||
(scene → section_base; resId → asset via the index) + Godot `TextureRect` compositing (A2b plan Tasks 3–5,
|
||||
now purely mechanical). Audio (step 4) uses the *same* resolver (`play-bgm/play-voice id → files[base+id]`).
|
||||
A2b-background: **steps 1–3 landed.** Step 1 = `build/asset-index.json`. Step 2 = **`resId →
|
||||
files[section_base(scene) + resId]`** via SYS4INI per-scene sections (`tools/resolve_asset.py` +
|
||||
`build/asset-sections.json`) — no runtime capture, all archives/types + audio. Step 3 = **first-pass
|
||||
render** (ResourceMap + GodotAdvHost texture ops → TextureRect compositing): the full-screen event-CG
|
||||
layer renders end-to-end from the bytecode. Remaining (next chunk): the **graphics geometry/blend
|
||||
subsystem** — native geometry ops (`0x208` + sprite position/animation) so sprites/`BG*` position, plus
|
||||
alpha/blend for fades + chromakey. See `docs/phase-a-slice-plan.md` (A2b). Audio (step 4) uses the *same*
|
||||
resolver (`play-bgm/play-voice id → files[base+id]`).
|
||||
|
||||
@@ -181,16 +181,34 @@ Toolchain: `godot --headless --path godot --import` → `dotnet build godot/Hime
|
||||
**Next = A2b:** background via `AGF2BMP2AGF.exe`, `play-voice`/`play-bgm`, choices → VM globals,
|
||||
just-enough `call-script`/state (unlocks richer scenes).
|
||||
|
||||
### A2b-Background — machinery landed; render blocked on asset resolution (2026-07-06)
|
||||
Engine-driven texture ops shipped: `create/set/draw-texture` (0x1f8/0x1f9/0x1fb) promoted from VM stubs
|
||||
to typed `IHost` methods (CaptureHost no-ops → trace parity kept; engine tests 8/8). Tools: `convert_agf.py`
|
||||
(AGF→BMP stills) + `tools/frida/` capture harness (Frida 17.15.3 installed). **Blocked:** rendering the bg
|
||||
needs `resId → asset file` resolution, which proved opaque — CGINIT isn't a filename map, SYS4INI (S4IC422)
|
||||
needs format RE, Frida file-I/O offsets are noisy (memory-mapping), and the opening mixes movies (MVB/OP =
|
||||
MPEG) with stills so eyeball-curation stalled too. **Asset resolution promoted to a dedicated foundational
|
||||
RE effort** (graphics + audio; not machine-verifiable — Frida ground truth + human eye/ear are the oracle):
|
||||
see `docs/asset-resolution-re.md`. The backend render (ResourceMap + Godot compositing) stays designed in
|
||||
`docs/superpowers/plans/2026-07-06-a2b-background.md` Tasks 3–5, mechanical once resolution lands.
|
||||
### A2b-Background — FIRST-PASS RENDER LANDED (2026-07-06)
|
||||
Resolution solved (`docs/asset-resolution-re.md`: `resId → files[section_base(scene)+resId]`) and wired
|
||||
into a live render. **Shipped:** `Age.Engine/Sys4/ResourceMap.cs` (loads `build/asset-index.json` +
|
||||
`build/asset-sections.json`; `Resolve(scene,resId) → AssetEntry`; `TexturePath` → pre-converted BMP);
|
||||
`GodotAdvHost` implements `create/set/draw-texture` (slot → `TextureRect` composited behind the dialogue
|
||||
in a `_stage` layer); `IHost.DrawTexture` + VM dispatch extended to pass the destination x/y (draw-texture
|
||||
args 7/8); `project.godot` window = 800×600; `convert_agf.py` searches all archives + `--scene` batch.
|
||||
Engine 8/8, C# `--selftest` still byte-matches the vm0 trace (VM behaviour unchanged). **Works end-to-end:**
|
||||
the VM executes `set-texture(resId)` → ResourceMap resolves across archives → BMP loads → composite; the
|
||||
full-screen **event-CG layer (`EV052*`) renders correctly** as the opening plays.
|
||||
|
||||
**Known first-pass limitations (all one subsystem = graphics geometry/blend, the next chunk):**
|
||||
1. **Only the full-screen layer is correct.** Sprites/effects and `BG*` backgrounds routed through the
|
||||
CG-load subroutine (`label_12649`) derive width/height/position from native ops we still **stub** —
|
||||
`0x208` (get-texture-size) + the sprite position/registration/animation chain — so their `dst/size`
|
||||
are garbage (backgrounds land off-center, e.g. `BG030A dst=(300,300)`; sizes come out `0x0`). Only the
|
||||
*immediate* full-screen draws (`(0,0) 800×600`) render right.
|
||||
2. **No alpha/blend.** `AE*` full-screen fade/flash effects draw **opaque and instant** (a static grey/white
|
||||
sheet over the CG) instead of alpha-animating. No chromakey either (sprites would show green boxes —
|
||||
moot until they position).
|
||||
3. **Slot model is an approximation.** We use one `TextureRect` per slot, replace-on-draw; the game
|
||||
actually **blits onto slot 0 as an immediate-mode canvas** (everything composites into slot 0).
|
||||
4. AGF is **pre-converted to BMP offline** (`convert_agf.py --scene`); a runtime C# AGF decoder is deferred.
|
||||
|
||||
**Next chunk — graphics-geometry/blend subsystem:** implement `0x208` (host returns the slot's real image
|
||||
dims) + the sprite position/registration ops so geometry is correct; add alpha/additive blend for fades +
|
||||
green chromakey; likely move to a proper canvas/blit compositor. Fixes sprites, background placement, and
|
||||
fades together. (Superseded: the id-specific plan in `docs/superpowers/plans/2026-07-06-a2b-background.md`.)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited *
|
||||
| `parse_sys4ini.py` | Parse `SYS4INI.BIN` (S4IC422, LZSS-compressed) into the authoritative asset index — name ↔ archive ↔ offset ↔ size for all DATA*.ALF (the `resId→file` answer key). | `parse_sys4ini.py [--check]` (`--check` validates vs `extracted/` + `.ALF` sizes) | `姫狩り…/SYS4INI.BIN` → `build/asset-index.json` |
|
||||
| `resolve_asset.py` | ★ **The static asset resolver.** SYS4INI is sectioned (one per scene: `SCxxxx.BIN` + its cross-archive manifest; `file_number` = index within section). Resolves `resId → files[section_base(scene) + resId]` for graphics AND audio, no capture. | `resolve_asset.py --build` · `resolve_asset.py <SCENE> [resId]` | `build/asset-index.json` → `build/asset-sections.json`; resolves any (scene, resId) |
|
||||
| `resolve_frida_reads.py` | Rescue noisy Frida archive-read offsets → asset names via the index (per-archive range search; drops 0x20000 paging reads); recovers the per-scene asset load order. | `resolve_frida_reads.py [reads.log] [-o out.json]` | `build/frida-reads.log` + `build/asset-index.json` → `build/frida-asset-loads.json` |
|
||||
| `convert_agf.py` | Convert named AGF stills to BMP via `AGF2BMP2AGF.exe`. | `convert_agf.py EV001AA.AGF …` | `extracted/DATA2|DATA5/*.AGF` → `build/textures/*.BMP` |
|
||||
| `convert_agf.py` | Convert AGF stills to BMP via `AGF2BMP2AGF.exe` (searches all `extracted/DATA*`). `--scene` batch-converts a scene's whole SYS4INI manifest — feeds the Godot render. | `convert_agf.py EV052CA.AGF …` · `convert_agf.py --scene SC0000` | `extracted/DATA*/*.AGF` → `build/textures/*.BMP` |
|
||||
|
||||
## Runtime capture (Frida)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ public class TextureOpsTests
|
||||
public void WaitForInput() { }
|
||||
public void CreateTexture(int slot, int w, int h) => Creates++;
|
||||
public void SetTexture(long resId, int slot) => Sets.Add((resId, slot));
|
||||
public void DrawTexture(int slot, int x, int y, int w, int h) => Draws.Add((slot, w, h));
|
||||
public void DrawTexture(int slot, int srcX, int srcY, int w, int h, int dstX, int dstY) => Draws.Add((slot, w, h));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -16,7 +16,7 @@ public class WaitForInputTests
|
||||
public void WaitForInput() => Waits++;
|
||||
public void CreateTexture(int slot, int w, int h) { }
|
||||
public void SetTexture(long resId, int slot) { }
|
||||
public void DrawTexture(int slot, int x, int y, int w, int h) { }
|
||||
public void DrawTexture(int slot, int srcX, int srcY, int w, int h, int dstX, int dstY) { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -10,5 +10,5 @@ public sealed class CaptureHost : IHost
|
||||
public void WaitForInput() { }
|
||||
public void CreateTexture(int slot, int width, int height) { }
|
||||
public void SetTexture(long resourceId, int slot) { }
|
||||
public void DrawTexture(int slot, int x, int y, int width, int height) { }
|
||||
public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY) { }
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@ public interface IHost
|
||||
void WaitForInput();
|
||||
void CreateTexture(int slot, int width, int height);
|
||||
void SetTexture(long resourceId, int slot);
|
||||
void DrawTexture(int slot, int x, int y, int width, int height);
|
||||
void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ public static class Paths
|
||||
public static string GameDir => Path.Combine(Workspace, "姫狩りダンジョンマイスター");
|
||||
public static string Build => Path.Combine(Repo, "build");
|
||||
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 Textures => Path.Combine(Build, "textures");
|
||||
|
||||
private static string FindRepo()
|
||||
{
|
||||
|
||||
63
engine/Age.Engine/Sys4/ResourceMap.cs
Normal file
63
engine/Age.Engine/Sys4/ResourceMap.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -172,8 +172,9 @@ public sealed class VirtualMachine
|
||||
_host.CreateTexture((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2])); return pc + 1;
|
||||
case "set-texture":
|
||||
_host.SetTexture(Read(a[0]), (int)Read(a[1])); return pc + 1;
|
||||
case "draw-texture":
|
||||
_host.DrawTexture((int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]), (int)Read(a[4]), (int)Read(a[5])); return pc + 1;
|
||||
case "draw-texture": // (handle, slot, srcX, srcY, w, h, dstX, dstY)
|
||||
_host.DrawTexture((int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]), (int)Read(a[4]),
|
||||
(int)Read(a[5]), (int)Read(a[6]), (int)Read(a[7])); return pc + 1;
|
||||
default:
|
||||
_host.OnStub(op); return pc + 1;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Sys4;
|
||||
|
||||
public sealed class GodotAdvHost : IHost
|
||||
{
|
||||
private readonly Main _main;
|
||||
private readonly ResourceMap _res;
|
||||
private readonly string _scene; // e.g. "SC0000" — for section_base
|
||||
private readonly Dictionary<int, string?> _slotBmp = new(); // slot -> pre-converted BMP path
|
||||
private readonly SemaphoreSlim _gate = new(0, 1);
|
||||
public volatile bool IsWaiting;
|
||||
public readonly List<(int Offset, string Text)> Captured = new();
|
||||
|
||||
public GodotAdvHost(Main main) => _main = main;
|
||||
public GodotAdvHost(Main main, ResourceMap res, string scene)
|
||||
{
|
||||
_main = main; _res = res; _scene = scene;
|
||||
}
|
||||
|
||||
public void ShowText(int offset, string text)
|
||||
{
|
||||
@@ -31,4 +38,19 @@ public sealed class GodotAdvHost : IHost
|
||||
|
||||
public void CallScript(long id) { }
|
||||
public void OnStub(int opcode) { }
|
||||
|
||||
// ---- texture ops (run on the VM thread; marshal Godot node work to the main thread) ----
|
||||
public void CreateTexture(int slot, int width, int height) => _slotBmp[slot] = null;
|
||||
|
||||
public void SetTexture(long resourceId, int slot)
|
||||
{
|
||||
var asset = _res.Resolve(_scene, resourceId);
|
||||
_slotBmp[slot] = asset != null ? ResourceMap.TexturePath(asset) : null;
|
||||
}
|
||||
|
||||
public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY)
|
||||
{
|
||||
if (_slotBmp.TryGetValue(slot, out var bmp) && bmp != null)
|
||||
_main.CallDeferred("DrawSlot", slot, bmp, dstX, dstY, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
1
godot/GodotAdvHost.cs.uid
Normal file
1
godot/GodotAdvHost.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b6bhdmtc2387h
|
||||
@@ -7,6 +7,8 @@ using Age.Engine.Vm;
|
||||
|
||||
public partial class Main : Godot.Control
|
||||
{
|
||||
private Control _stage = null!; // texture layer (behind the text)
|
||||
private readonly Dictionary<int, TextureRect> _slots = new();
|
||||
private Label _text = null!;
|
||||
private Label _status = null!;
|
||||
private VirtualMachine _vm = null!;
|
||||
@@ -17,6 +19,12 @@ public partial class Main : Godot.Control
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// texture stage, added first so it draws BEHIND the dialogue text
|
||||
_stage = new Control();
|
||||
_stage.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
_stage.MouseFilter = MouseFilterEnum.Ignore;
|
||||
AddChild(_stage);
|
||||
|
||||
_text = new Label { AutowrapMode = TextServer.AutowrapMode.WordSmart };
|
||||
_text.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
_text.OffsetLeft = 40; _text.OffsetTop = 40; _text.OffsetRight = -40; _text.OffsetBottom = -80;
|
||||
@@ -46,7 +54,7 @@ public partial class Main : Godot.Control
|
||||
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var script = Sys4Loader.Load(Paths.Scripts()["SC0000.BIN"], table);
|
||||
_host = new GodotAdvHost(this);
|
||||
_host = new GodotAdvHost(this, ResourceMap.Load(), "SC0000");
|
||||
_vm = new VirtualMachine(script, table, _host);
|
||||
_ = Task.Run(() => { _vm.Run(); _done = true; });
|
||||
|
||||
@@ -77,6 +85,30 @@ public partial class Main : Godot.Control
|
||||
public override void _ExitTree() { _host?.SignalInput(); }
|
||||
|
||||
// ---- UI methods invoked on the main thread via CallDeferred ----
|
||||
// Composite a resolved texture into a slot at (x,y) sized (w,h). One TextureRect per slot,
|
||||
// layered in draw order (backgrounds are drawn before sprites, so they sit behind).
|
||||
public void DrawSlot(int slot, string bmpPath, int x, int y, int w, int h)
|
||||
{
|
||||
var img = new Image();
|
||||
var err = img.LoadBmpFromBuffer(System.IO.File.ReadAllBytes(bmpPath));
|
||||
if (err != Error.Ok) { GD.Print($"BMP load failed {bmpPath}: {err}"); return; }
|
||||
if (!_slots.TryGetValue(slot, out var tr))
|
||||
{
|
||||
tr = new TextureRect
|
||||
{
|
||||
ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize,
|
||||
StretchMode = TextureRect.StretchModeEnum.Scale,
|
||||
MouseFilter = MouseFilterEnum.Ignore,
|
||||
};
|
||||
_stage.AddChild(tr);
|
||||
_slots[slot] = tr;
|
||||
}
|
||||
tr.Texture = ImageTexture.CreateFromImage(img);
|
||||
tr.Position = new Vector2(x, y);
|
||||
tr.Size = new Vector2(w > 0 ? w : img.GetWidth(), h > 0 ? h : img.GetHeight());
|
||||
tr.Visible = true;
|
||||
}
|
||||
|
||||
public void AppendLine(string text) => _text.Text += text + "\n";
|
||||
public void PageBreak() => _status.Text = "▼ click / Enter";
|
||||
public void ClearPage() { _text.Text = ""; _status.Text = ""; }
|
||||
|
||||
@@ -4,5 +4,9 @@ config_version=5
|
||||
config/name="Himegari (age-reimpl A2a)"
|
||||
run/main_scene="res://Main.tscn"
|
||||
|
||||
[display]
|
||||
window/size/viewport_width=800
|
||||
window/size/viewport_height=600
|
||||
|
||||
[dotnet]
|
||||
project/assembly_name="Himegari"
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert named AGF files (in extracted/DATA2 or DATA5) to BMP via AGF2BMP2AGF.exe,
|
||||
into build/textures/. Run: py -3.11 -X utf8 tools/convert_agf.py EV001AA.AGF ..."""
|
||||
import os, sys, shutil, subprocess
|
||||
"""Convert AGF images to BMP via AGF2BMP2AGF.exe, into build/textures/.
|
||||
|
||||
py -3.11 -X utf8 tools/convert_agf.py EV052CA.AGF BG030A.AGF ... # named assets
|
||||
py -3.11 -X utf8 tools/convert_agf.py --scene SC0000 # all .AGF in the scene's
|
||||
# SYS4INI section manifest
|
||||
|
||||
Searches every extracted/DATA* dir (DATA1 holds BG/CS/CB/CA/AE/EM sprites, DATA2 the EV CGs).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import paths
|
||||
|
||||
EXE = paths.EXTRACTED / "DATA1" / "AGF2BMP2AGF.exe" # tool lives in extracted/DATA1
|
||||
SRC_DIRS = [paths.EXTRACTED / "DATA2", paths.EXTRACTED / "DATA5"]
|
||||
EXE = paths.EXTRACTED / "DATA1" / "AGF2BMP2AGF.exe" # tool lives in extracted/DATA1
|
||||
SRC_DIRS = [paths.EXTRACTED / d for d in ("DATA1", "DATA2", "DATA3", "DATA4", "DATA5")]
|
||||
OUT = paths.BUILD / "textures"
|
||||
|
||||
|
||||
def find(name):
|
||||
for d in SRC_DIRS:
|
||||
p = d / name
|
||||
@@ -16,18 +28,44 @@ def find(name):
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def scene_agfs(scene):
|
||||
"""The .AGF asset names in a scene's SYS4INI section manifest."""
|
||||
secs = json.loads((paths.BUILD / "asset-sections.json").read_text(encoding="utf-8"))
|
||||
files = json.loads((paths.BUILD / "asset-index.json").read_text(encoding="utf-8"))["files"]
|
||||
base = secs["scene_base"][scene.upper().removesuffix(".BIN")]
|
||||
end = next(s["end"] for s in secs["sections"] if s["start"] == base)
|
||||
return [files[p]["name"] for p in range(base, end + 1)
|
||||
if files[p]["name"].upper().endswith(".AGF")]
|
||||
|
||||
|
||||
def convert(name):
|
||||
src = find(name)
|
||||
if not src:
|
||||
print(f"NOT FOUND: {name}"); return False
|
||||
out_bmp = OUT / (os.path.splitext(name)[0] + ".BMP")
|
||||
if out_bmp.exists():
|
||||
return True
|
||||
tmp = OUT / name
|
||||
shutil.copy(src, tmp)
|
||||
r = subprocess.run([str(EXE), name], cwd=str(OUT), capture_output=True, text=True)
|
||||
tmp.unlink(missing_ok=True)
|
||||
if not out_bmp.exists():
|
||||
print(f"FAILED: {name} ({r.stdout.strip()[:60]})"); return False
|
||||
return True
|
||||
|
||||
|
||||
def main(argv):
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
for name in argv:
|
||||
src = find(name)
|
||||
if not src:
|
||||
print(f"NOT FOUND: {name}"); continue
|
||||
tmp = OUT / name
|
||||
shutil.copy(src, tmp)
|
||||
subprocess.run([str(EXE), name], cwd=str(OUT), check=True)
|
||||
tmp.unlink(missing_ok=True) # drop the copied .AGF, keep the .BMP
|
||||
print(f"converted {name} -> {OUT / (os.path.splitext(name)[0] + '.BMP')}")
|
||||
if argv and argv[0] == "--scene":
|
||||
names = scene_agfs(argv[1])
|
||||
print(f"{argv[1]}: {len(names)} .AGF assets in manifest")
|
||||
else:
|
||||
names = argv
|
||||
ok = sum(convert(n) for n in names)
|
||||
print(f"converted {ok}/{len(names)} -> {OUT.relative_to(paths.REPO)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
|
||||
Reference in New Issue
Block a user