Merge: A2b graphics geometry (0x208 keystone + blit compositor)

Opening event-CG sequence now renders correctly. 0x208 get-texture-size
implemented as a real VM op; faithful 800x600 immediate-mode blit compositor;
gfx + --shot diagnostics. Post-opening background/sprite anchor drift is
characterized and deferred to the anchor-record subsystem (next chunk).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-06 23:06:39 -04:00
14 changed files with 312 additions and 48 deletions

View File

@@ -81,6 +81,11 @@
- **grounding:** source=inference, confidence=med
- **evidence:** confirm via frida
### 0x208 `get-texture-size` (get-texture-size, argc 3)
- **summary:** 0x208 (slot)(out_w)(out_h) — writes the loaded texture's width/height into two output globals; keystone for bytecode-computed sprite/bg geometry (SC0000 label_12649)
- **grounding:** source=inference, confidence=med
- **evidence:** SC0000 label_12649: set-texture(resId,slot) then 0x208(slot)->w,h feeds w/2 horizontal-center + foot-anchor subtraction into draw-texture dst; stubbing yields 0x0 sizes / off-center draws
### 0x217 `gfx-geom?` (u004211E0, argc 4)
- **summary:** 4 global-ints; part of a 0x217/0x218/0x21a geometry chain
- **grounding:** source=inference, confidence=low
@@ -840,10 +845,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x208 `u00420BF0` (u00420BF0, argc 3)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x20a `u00420CE0` (u00420CE0, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=low

View File

@@ -245,6 +245,40 @@ of the above. **Watch-items:** BGM looping is whole-file for now (Eushully OGGs
`LOOPLENGTH` Vorbis comments — refine later); `play-sound-effect`(0xb4, argc 2) left stubbed (arg roles
unconfirmed).
### A2b-Geometry — `0x208` keystone + blit compositor (2026-07-06)
Spec/plan: `docs/superpowers/{specs,plans}/2026-07-06-a2b-graphics-geometry*.md`. **Shipped & verified:**
the CG-load subroutine (`SC0000.asm` `label_12649`) computes all sprite/background geometry **in
bytecode** (`add`/`sub`/`div`/`lookup-array`); the only missing native primitive was **`0x208 =
get-texture-size(slot) → (out_w, out_h)`**. Implemented as a real VM op (`IHost.GetTextureSize`, writes
the two output globals); non-Godot hosts return `(0,0)` so trace/selftest parity holds (engine 11/11,
`--selftest` byte-identical). Replaced the TextureRect-per-slot approximation with a faithful **800×600
immediate-mode blit compositor** (`Main.BlitSlot`: `_screen.BlitRect(src rect → dst)` in execution order,
one displayed `TextureRect`; source dims read from the pre-converted BMP header on the VM thread via
`BmpHeader.ReadDims`, so the bytecode's geometry math sees real sizes synchronously). New diagnostics:
`Age.Cli gfx <SCENE>` (headless numeric oracle — dumps per-draw resolved file + computed geometry) and
`godot … -- --shot <png> [--shot-page N]` (page-gated screenshot capture). **The opening event-CG sequence
renders correctly** — full-screen CG at `(0,0)` with dialogue over it (verified by screenshot, SC0000
pages 1/3).
**Slot-0 seed (bug found & fixed via the gfx oracle + user eyeball):** slot 0 is the **primary/screen
surface** (800×600), normally created by engine-boot init the single-scene harness skips. Cold, `0x208`
measured `0×0`, and the anchor-preserve math (`base' = center (w_new/2, h_new)`) then wrote a corrupted
`(400,600)` into the **persistent base globals** — so the first CG was grey and CG2 inherited the
corruption. Fix: seed `_slotDims[0] = (800,600)` (and record `create-texture(w,h)` dims) so the first CG's
anchor stays an identity. This is the faithful stand-in for the skipped boot-time primary-surface creation.
**DEFERRED (next chunk) — the sprite/background anchor-record subsystem.** Everything blits through slot 0
as an immediate-mode canvas; the anchor-preserve base globals **accumulate drift** across textures of
*different* sizes (same-size 800×600 CGs stay put; the first `BG*` 800×500 / `AE*` 800×800 / sprite starts
a drift that accumulates — `BG030A→(300,500)`, next→`(450,100)`, →`(800,350)`…, marching bottom-right).
The real engine doesn't drift because it stores each element's geometry in a **per-object record** via
`0x217/0x218/0x21a` (currently no-op) and restores it (the `0x12683` if-branch reading the `0x3239` record
table). Implementing that store/restore (+ the record layout, likely Frida-confirmed) is the fix for
backgrounds **and** sprites together. Fades/alpha (`AE*`, `0x202/0x203`) and green chromakey remain
deferred as before (the compositor is built to accept alpha later). Also out: true multi-surface (dest
handle is collapsed onto the screen). The full-screen opening path is unaffected by any of these.
---
## Risks / open questions for A0

View File

@@ -42,6 +42,28 @@ if (args[0] == "audio")
return 0;
}
if (args[0] == "gfx")
{
// gfx <SCENE.BIN> [0xADDR=VAL ...] — run the scene and dump executed texture ops in order with
// resolved file + computed geometry (set-texture / get-texture-size / draw-texture). Diagnostic only.
var sceneName = args[1];
var sceneKey = Path.GetFileNameWithoutExtension(sceneName).ToUpperInvariant();
var res = ResourceMap.Load();
var host = new GfxTraceHost(res, sceneKey);
var vm = new VirtualMachine(Sys4Loader.Load(Paths.Scripts()[sceneName.ToUpperInvariant()], table), table, host);
foreach (var s in args.Skip(2))
{
var kv = s.Split('=');
int k = kv[0].StartsWith("0x") ? Convert.ToInt32(kv[0], 16) : int.Parse(kv[0]);
long v = kv[1].StartsWith("0x") ? Convert.ToInt64(kv[1], 16) : long.Parse(kv[1]);
vm.Globals[k] = v;
}
vm.Run();
Console.WriteLine($"{sceneName}: {host.Events.Count} texture ops (halt: {vm.HaltReason})");
foreach (var line in host.Events) Console.WriteLine(" " + line);
return 0;
}
if (args[0] == "trace")
{
var scene = new Regex(@"^S[CP]\d{4}\.BIN$");
@@ -84,4 +106,53 @@ sealed class AudioTraceHost : IHost
public void CreateTexture(int slot, int width, int height) { }
public void SetTexture(long resourceId, int slot) { }
public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY) { }
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
}
sealed class GfxTraceHost : IHost
{
private readonly ResourceMap _res;
private readonly string _scene;
private readonly Dictionary<int, string?> _slotBmp = new(); // slot -> resolved BMP path (or null)
// slot -> dims. Slot 0 is the primary/screen surface (800x600), normally created at engine boot which
// the single-scene harness skips; seed it so the first CG's anchor math stays correct (not 0x0).
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
public List<string> Events { get; } = new();
public GfxTraceHost(ResourceMap res, string scene) { _res = res; _scene = scene; }
public (int Width, int Height) GetTextureSize(int slot)
{
var d = _slotDims.TryGetValue(slot, out var v) ? v : (0, 0);
Events.Add($"get-tex-size slot={slot} -> {d.Item1}x{d.Item2}");
return d;
}
public void SetTexture(long resId, int slot)
{
var e = _res.Resolve(_scene, resId);
var bmp = e != null ? ResourceMap.TexturePath(e) : null;
_slotBmp[slot] = bmp;
_slotDims[slot] = BmpHeader.ReadDims(bmp);
Events.Add($"set-texture slot={slot} res=0x{resId:x} -> {(e?.Name ?? "<unresolved>")}"
+ (bmp == null ? " [NO BMP]" : ""));
}
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy)
{
_slotBmp.TryGetValue(slot, out var bmp);
Events.Add($"draw-texture slot={slot} src=({sx},{sy} {w}x{h}) dst=({dx},{dy}) "
+ $"file={(bmp != null ? System.IO.Path.GetFileName(bmp) : "<none>")}");
}
public void CreateTexture(int slot, int width, int height)
{
_slotDims[slot] = (width, height);
Events.Add($"create-texture slot={slot} {width}x{height}");
}
public void ShowText(int offset, string text) { }
public void CallScript(long id) { }
public void OnStub(int opcode) { }
public void WaitForInput() { }
public void PlayBgm(long id) { }
public void PlayVoice(long id) { }
}

View File

@@ -0,0 +1,33 @@
using System.IO;
using Age.Engine.Sys4;
using Xunit;
public class BmpHeaderTests
{
[Fact]
public void ReadDimsReadsWidthAndHeightFromBmpHeader()
{
// Minimal 54-byte BMP header (BITMAPFILEHEADER 14 + BITMAPINFOHEADER 40); width=4, height=3.
var b = new byte[54];
b[0] = (byte)'B'; b[1] = (byte)'M';
System.BitConverter.GetBytes(40).CopyTo(b, 14); // header size
System.BitConverter.GetBytes(4).CopyTo(b, 18); // width
System.BitConverter.GetBytes(3).CopyTo(b, 22); // height
var tmp = Path.Combine(Path.GetTempPath(), "agehdr_test.bmp");
File.WriteAllBytes(tmp, b);
try
{
var (w, h) = BmpHeader.ReadDims(tmp);
Assert.Equal(4, w);
Assert.Equal(3, h);
}
finally { File.Delete(tmp); }
}
[Fact]
public void ReadDimsReturnsZeroForMissingFile()
{
var (w, h) = BmpHeader.ReadDims(Path.Combine(Path.GetTempPath(), "does_not_exist_agehdr.bmp"));
Assert.Equal((0, 0), (w, h));
}
}

View File

@@ -0,0 +1,46 @@
using System.Collections.Generic;
using Age.Engine.Hosting;
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
using Xunit;
public class TextureGeometryTests
{
private sealed class FakeSizeHost : IHost
{
public void ShowText(int o, string t) { }
public void CallScript(long id) { }
public void OnStub(int op) { }
public void WaitForInput() { }
public void CreateTexture(int slot, int w, int h) { }
public void SetTexture(long resId, int slot) { }
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy) { }
public void PlayBgm(long id) { }
public void PlayVoice(long id) { }
public (int Width, int Height) GetTextureSize(int slot) => (0x140, 0xC8);
}
[Fact]
public void GetTextureSizeWritesHostDimsIntoOutputGlobals()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
// 0x208 (global-int 50)(global-int 60)(global-int 61): slot=50, out_w=G[60], out_h=G[61]
const int T_GINT = 3;
var ins = new Instruction(0, 0x208, new[]
{
new Operand(T_GINT, 50), new Operand(T_GINT, 60), new Operand(T_GINT, 61),
});
var script = new Script
{
Header = new ScriptHeader(0, 0, 0, 0, 0, 0),
Instructions = new[] { ins },
IndexByOffset = new Dictionary<int, int> { { 0, 0 } },
Strings = new Dictionary<int, string>(),
};
var vm = new VirtualMachine(script, table, new FakeSizeHost());
vm.Run();
Assert.Equal(0x140, vm.Globals[60]);
Assert.Equal(0xC8, vm.Globals[61]);
}
}

View File

@@ -18,6 +18,7 @@ public class TextureOpsTests
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 srcX, int srcY, int w, int h, int dstX, int dstY) => Draws.Add((slot, w, h));
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
public void PlayBgm(long id) { }
public void PlayVoice(long id) { }
}

View File

@@ -17,6 +17,7 @@ public class WaitForInputTests
public void CreateTexture(int slot, int w, int h) { }
public void SetTexture(long resId, int slot) { }
public void DrawTexture(int slot, int srcX, int srcY, int w, int h, int dstX, int dstY) { }
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
public void PlayBgm(long id) { }
public void PlayVoice(long id) { }
}

View File

@@ -11,6 +11,7 @@ public sealed class CaptureHost : IHost
public void CreateTexture(int slot, int width, int height) { }
public void SetTexture(long resourceId, int slot) { }
public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY) { }
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
public void PlayBgm(long id) { }
public void PlayVoice(long id) { }
}

View File

@@ -8,6 +8,7 @@ public interface IHost
void CreateTexture(int slot, int width, int height);
void SetTexture(long resourceId, int slot);
void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY);
(int Width, int Height) GetTextureSize(int slot);
void PlayBgm(long id);
void PlayVoice(long id);
}

View File

@@ -0,0 +1,25 @@
namespace Age.Engine.Sys4;
/// <summary>
/// Reads pixel dimensions from a BMP file header (BITMAPINFOHEADER: width at byte 18, height at byte 22,
/// both little-endian int32; height may be negative for top-down bitmaps). Used to give the VM the
/// texture size that opcode 0x208 (get-texture-size) needs, without decoding pixels. Our textures are
/// pre-converted BMPs (tools/convert_agf.py).
/// </summary>
public static class BmpHeader
{
public static (int Width, int Height) ReadDims(string? path)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return (0, 0);
try
{
var b = new byte[26];
using var fs = File.OpenRead(path);
if (fs.Read(b, 0, 26) < 26 || b[0] != (byte)'B' || b[1] != (byte)'M') return (0, 0);
int w = System.BitConverter.ToInt32(b, 18);
int h = System.BitConverter.ToInt32(b, 22);
return (System.Math.Abs(w), System.Math.Abs(h));
}
catch { return (0, 0); }
}
}

View File

@@ -175,6 +175,12 @@ public sealed class VirtualMachine
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;
case "get-texture-size": // 0x208 (slot) (out_w) (out_h)
{
var (gw, gh) = _host.GetTextureSize((int)Read(a[0]));
Write(a[1], gw); Write(a[2], gh);
return pc + 1;
}
case "play-bgm": _host.PlayBgm(Read(a[0])); return pc + 1;
case "play-voice": _host.PlayVoice(Read(a[0])); return pc + 1;
default:

View File

@@ -8,7 +8,10 @@ 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 Dictionary<int, string?> _slotBmp = new(); // slot -> pre-converted BMP path
// slot -> dims. Slot 0 is the primary/screen surface (800x600), normally created at engine boot which
// the single-scene harness skips; seed it so the first CG's anchor math stays correct (not 0x0).
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
private readonly SemaphoreSlim _gate = new(0, 1);
public volatile bool IsWaiting;
public readonly List<(int Offset, string Text)> Captured = new();
@@ -24,8 +27,11 @@ public sealed class GodotAdvHost : IHost
_main.CallDeferred("AppendLine", text);
}
public volatile int Pages; // VM-thread page counter (incremented before IsWaiting so shot-gating can't race)
public void WaitForInput()
{
Pages++;
_main.CallDeferred("PageBreak");
IsWaiting = true;
_gate.Wait();
@@ -40,18 +46,25 @@ public sealed class GodotAdvHost : IHost
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 CreateTexture(int slot, int width, int height) { _slotBmp[slot] = null; _slotDims[slot] = (width, height); }
public void SetTexture(long resourceId, int slot)
{
var asset = _res.Resolve(_scene, resourceId);
_slotBmp[slot] = asset != null ? ResourceMap.TexturePath(asset) : null;
var bmp = asset != null ? ResourceMap.TexturePath(asset) : null;
_slotBmp[slot] = bmp;
_slotDims[slot] = BmpHeader.ReadDims(bmp); // synchronous: dims from the header, no Godot Image
}
// Dims are read from the BMP header on the VM thread so the bytecode's geometry math (which calls this
// synchronously right after set-texture) sees the real size. Pixels are blitted later on the main thread.
public (int Width, int Height) GetTextureSize(int slot)
=> _slotDims.TryGetValue(slot, out var d) ? (d.W, d.H) : (0, 0);
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);
_main.CallDeferred("BlitSlot", bmp, srcX, srcY, width, height, dstX, dstY);
}
// ---- audio ops (OGG plays natively in Godot) ----

View File

@@ -7,8 +7,9 @@ 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 TextureRect _screenView = null!; // shows the composited screen backbuffer
private Image _screen = null!; // 800x600 immediate-mode canvas
private ImageTexture _screenTex = null!;
private Label _text = null!;
private Label _status = null!;
private AudioStreamPlayer _bgm = null!; // looping background music
@@ -18,14 +19,26 @@ public partial class Main : Godot.Control
private volatile bool _done;
private bool _ended;
private bool _selftest;
private string? _shotPath; // --shot <png>: capture a page then quit (dev tool)
private int _shotPage = 1; // --shot-page <n>: which page to capture (default 1)
private volatile int _pageCount;
private int _shotSettle;
private bool _shotDone;
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);
// Screen backbuffer: one 800x600 canvas that draw-texture blits into, shown behind the dialogue.
_screen = Image.CreateEmpty(800, 600, false, Image.Format.Rgba8);
_screenTex = ImageTexture.CreateFromImage(_screen);
_screenView = new TextureRect
{
Texture = _screenTex,
ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize,
StretchMode = TextureRect.StretchModeEnum.Scale,
MouseFilter = MouseFilterEnum.Ignore,
};
_screenView.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
AddChild(_screenView); // added first -> draws behind the text/status labels
_text = new Label { AutowrapMode = TextServer.AutowrapMode.WordSmart };
_text.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
@@ -57,7 +70,13 @@ public partial class Main : Godot.Control
AddChild(_bgm);
AddChild(_voice);
_selftest = System.Array.IndexOf(OS.GetCmdlineUserArgs(), "--selftest") >= 0;
var userArgs = OS.GetCmdlineUserArgs();
_selftest = System.Array.IndexOf(userArgs, "--selftest") >= 0;
for (int i = 0; i < userArgs.Length; i++)
{
if (userArgs[i] == "--shot" && i + 1 < userArgs.Length) _shotPath = userArgs[i + 1];
if (userArgs[i] == "--shot-page" && i + 1 < userArgs.Length) int.TryParse(userArgs[i + 1], out _shotPage);
}
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var script = Sys4Loader.Load(Paths.Scripts()["SC0000.BIN"], table);
@@ -67,10 +86,26 @@ public partial class Main : Godot.Control
if (_selftest)
_ = Task.Run(async () => { while (!_done) { if (_host.IsWaiting) _host.SignalInput(); await Task.Delay(1); } });
// --shot: auto-advance up to (but not past) the target page, then _Process captures + quits.
if (_shotPath != null)
_ = Task.Run(async () => { while (!_done) { if (_host.IsWaiting && _host.Pages < _shotPage) _host.SignalInput(); await Task.Delay(1); } });
}
public override void _Process(double delta)
{
// --shot: once the target page is composed and parked at wait-for-input, settle a few frames then grab it.
if (_shotPath != null && !_shotDone && (_host.Pages >= _shotPage && _host.IsWaiting || _done))
{
if (++_shotSettle >= 3)
{
_shotDone = true;
var img = GetViewport().GetTexture().GetImage();
img.SavePng(_shotPath);
GD.Print($"SHOT saved page {_pageCount} -> {_shotPath}");
GetTree().Quit(0);
}
return;
}
if (_done && !_ended)
{
_ended = true;
@@ -92,28 +127,24 @@ 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)
// Blit a source BMP (src rect) onto the screen backbuffer at (dstX,dstY), then refresh the display
// texture. Execution order == paint order, so later draws (sprites) land over earlier ones (bg).
public void BlitSlot(string bmpPath, int srcX, int srcY, int w, int h, int dstX, int dstY)
{
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;
var src = new Image();
if (src.LoadBmpFromBuffer(System.IO.File.ReadAllBytes(bmpPath)) != Error.Ok)
{ GD.Print($"BMP load failed {bmpPath}"); return; }
if (src.GetFormat() != Image.Format.Rgba8) src.Convert(Image.Format.Rgba8);
// Clamp the source rect to the image; a zero/negative size falls back to the full image.
int sw = w > 0 ? w : src.GetWidth();
int sh = h > 0 ? h : src.GetHeight();
sw = System.Math.Min(sw, src.GetWidth() - srcX);
sh = System.Math.Min(sh, src.GetHeight() - srcY);
if (sw <= 0 || sh <= 0) return;
_screen.BlitRect(src, new Rect2I(srcX, srcY, sw, sh), new Vector2I(dstX, dstY));
_screenTex.Update(_screen);
}
// Load an OGG off disk and play it. BGM loops; voice plays once, cutting off any prior line.
@@ -136,7 +167,7 @@ public partial class Main : Godot.Control
}
public void AppendLine(string text) => _text.Text += text + "\n";
public void PageBreak() => _status.Text = "▼ click / Enter";
public void PageBreak() { _pageCount++; _status.Text = "▼ click / Enter"; }
public void ClearPage() { _text.Text = ""; _status.Text = ""; }
public void ShowEnd() => _status.Text = "— end —";

View File

@@ -4978,33 +4978,33 @@ observed_types = ["imm", "l-int"]
[[opcode]]
op = 0x208
label = "u00420BF0"
label = "get-texture-size"
argc = 3
abi_source = "kelebek+decode-validated"
[opcode.semantics]
name = "u00420BF0"
category = "unknown"
summary = ""
name = "get-texture-size"
category = "draw"
summary = "0x208 (slot)(out_w)(out_h) — writes the loaded texture's width/height into two output globals; keystone for bytecode-computed sprite/bg geometry (SC0000 label_12649)"
noop_headless = false
source = "kelebek"
confidence = "low"
source = "inference"
confidence = "med"
depends_on = []
evidence = ""
evidence = "SC0000 label_12649: set-texture(resId,slot) then 0x208(slot)->w,h feeds w/2 horizontal-center + foot-anchor subtraction into draw-texture dst; stubbing yields 0x0 sizes / off-center draws"
[[opcode.semantics.args]]
i = 1
role = ""
role = "slot"
observed_types = ["imm", "g-int", "l-int"]
[[opcode.semantics.args]]
i = 2
role = ""
role = "out_width"
observed_types = ["g-int", "l-int"]
[[opcode.semantics.args]]
i = 3
role = ""
role = "out_height"
observed_types = ["g-int", "l-int"]
[[opcode]]