import { DebugInfo, type MemberLocation, type SourceLocation } from '@gba-kit/debug-info'; import { Gba } from './gba.js'; import type { CpuSnapshot, GbaSnapshot } from './savestate.js'; /** Platform-specific I/O adapter for the scripting engine */ export interface ScriptingHost { writeScreenshot(name: string, rgbaData: Uint8Array, width: number, height: number): Promise; writeMemorySnapshot(name: string, data: Record): Promise; writeSaveState(name: string, snapshot: GbaSnapshot): Promise; readSaveState(path: string): Promise; log(message: string): void; } type ButtonName = 'a' | 'b' | 'select' | 'start' | 'right' | 'left' | 'up' | 'down' | 'r' | 'l'; interface WaitFrames { frames: number; } interface WaitMemory { memory: { /** * A raw address (read as a single byte), or — when debug info is loaded — a * `symbol`/`symbol.field` path, resolved through the DWARF and read at the * field's full width (bitfields decoded). */ address: number | string; equals?: number; lessThan?: number; greaterThan?: number; bitSet?: number; }; timeout?: number; } interface WaitExecution { /** * Wait until this instruction executes — an address, or a symbol name when debug * info is loaded. The counterpart to {@link ScriptingEngine.watchExecution}, which * records the same event rather than waiting for it. */ execution: number | string; timeout?: number; } interface WaitPixel { pixel: { x: number; y: number; r: number; g: number; b: number; }; timeout?: number; } type WaitCondition = WaitFrames | WaitMemory | WaitExecution | WaitPixel; interface MemorySnapshotRegion { name: string; region: 'iwram' | 'ewram' | 'vram' | 'oam' | 'palette' | 'io' | 'sram'; } interface MemorySnapshotRange { name: string; address: number; length: number; } type MemorySnapshotOptions = MemorySnapshotRegion | MemorySnapshotRange; interface AssertMemory { memory: { /** * A raw address (read as a single byte), or — when debug info is loaded — a * `symbol`/`symbol.field` path, resolved through the DWARF and read at the * field's full width (bitfields decoded). */ address: number | string; equals: number; }; } interface AssertRegister { register: { name: string; equals: number; }; } type AssertCondition = AssertMemory | AssertRegister; /** * A recorded write captured by a data watchpoint. For a `dma*` source, `pc` / * `instructionAddress` refer to the instruction that started the DMA. */ export interface WatchHit { /** CPU PC (pipeline-ahead of the instruction). */ pc: number; /** Address of the responsible instruction (pc-2 in Thumb, pc-4 in ARM). */ instructionAddress: number; /** The watched byte that was written. */ address: number; /** Value committed, masked to the access size. */ value: number; /** Access size in bytes (1, 2 or 4). */ size: number; thumb: boolean; source: 'cpu' | 'dma0' | 'dma1' | 'dma2' | 'dma3'; /** * The C `file:line` (+ function) of the writing instruction, when debug info * is loaded (see `loadDebugInfo`). This is the "a memory write names its own * source line" payoff. Undefined when no debug info, or for code with none * (e.g. INCLUDE_ASM stubs, library code). */ location?: SourceLocation; } /** One execution of a watched instruction, recorded by `watchExecution`. */ export interface ExecHit { /** The watched instruction address. */ address: number; /** * The link register as the instruction executed — a fact about the CPU, not * about the watched address. Raw, so a Thumb `bl` leaves bit 0 set and the * address is `lr & ~1`. * * It is the caller's return address only when the watched address was reached * by a `bl`/`blx`; one fallen into from the instruction above, branched to, or * entered by a tail call carries whatever the last unrelated call left behind. * Confirm the instruction ending at `lr & ~1` is a call to * {@link ExecHit.address} before reading it as a caller. */ lr: number; thumb: boolean; /** * {@link ExecHit.lr} resolved through the line table, when debug info covers it. * It inherits `lr`'s caveat, and a plausible `file:line` is what makes that * worth checking. */ callerLocation?: SourceLocation; } export declare class ScriptingEngine { #private; /** CPU interface — set externally since Gba doesn't expose full CPU */ cpuRegisters: Uint32Array | undefined; cpuCpsr: (() => number) | undefined; cpuSerialize: (() => CpuSnapshot) | undefined; cpuDeserialize: ((snapshot: CpuSnapshot) => void) | undefined; constructor(gba: Gba, host: ScriptingHost); /** * Load symbol/DWARF info from a (`-g`-built) ELF image. Enables * `pcToSource`/`symbolToAddress`/etc. and annotates watchpoint hits with the * writing instruction's source line. The `.gba` ROM has no debug info — pass * the sidecar ELF's bytes (its loadable bytes match the ROM, so addresses * line up). */ loadDebugInfo(elfBytes: Uint8Array): void; /** Provide an already-parsed DebugInfo (e.g. shared with a UI). */ setDebugInfo(debugInfo: DebugInfo | null): void; get debugInfo(): DebugInfo | null; get hasDebugInfo(): boolean; /** Map a PC to `{ file, line, func }`, or null (no debug info / not in C). */ pcToSource(pc: number): SourceLocation | null; /** The function containing `pc`, as `{ name, address }`, or null. */ pcToFunction(pc: number): { name: string; address: number; } | null; /** Nearest preceding symbol to `addr` as `{ name, offset }`, or null. */ addressToSymbol(addr: number): { name: string; offset: number; exact: boolean; } | null; /** * How many bytes a named object occupies and where that is known from, or null when * nothing states it — the bound the write guards apply. See * {@link DebugInfo.symbolExtent}. */ symbolExtent(name: string): { size: number; source: 'st_size' | 'dwarf'; } | null; /** Address of a named symbol (function or global), or null. */ symbolToAddress(name: string): number | null; get actionsExecuted(): number; wait(condition: WaitCondition): Promise; press(buttons: ButtonName | ButtonName[], options?: { hold?: number; }): Promise; pressSequence(inputs: [string | null, number][]): Promise; release(button: ButtonName): void; takeScreenshot(options: { name: string; }): Promise; takeMemorySnapshot(options: MemorySnapshotOptions): Promise; getRegisters(): Record; getMemory(address: number, length: number): Uint8Array; /** * Watch a memory range; each write appends a {@link WatchHit} to the returned * handle's `hits` array, recording which code performed it. The core primitive * for finding where a value is written. * * @example * const w = watchMemory({ address: 0x030055C0 }); * await press('right', { hold: 60 }); // take a hit * for (const h of w.hits) console.log(h.source, hex(h.instructionAddress), h.value); * w.stop(); */ watchMemory(options: { /** * A raw address, or — when debug info is loaded — a symbol name, in which case * `length` defaults to the whole object rather than one byte. */ address: number | string; length?: number; /** * Keep a hit only when this returns true — watch a wide region but record only * what matters. A throw is treated as `false` (never aborts the emulation). */ filter?: (hit: WatchHit) => boolean; /** * Cap recorded hits (first `maxHits` kept); guards memory on wide/long watches. * The watchpoint stays active — call `stop()` to remove it. */ maxHits?: number; }): { hits: WatchHit[]; /** * Writes that matched but were not recorded because `maxHits` was reached. A cap * that reports nothing leaves `hits.length === maxHits` meaning either "that is * all of them" or "that is the first few", which are different findings. */ dropped: number; stop: () => void; }; /** * Watch a named global by symbol (requires debug info) — `watchMemory` with a * symbol name, kept for readability at the call site. The length defaults to the * whole object, so a multi-byte global is watched in full; pass `length` to * override. Throws if no debug info is loaded or the symbol is unknown. * * @example * const w = watchSymbol('gPlayerState'); // covers the whole global * await press('a'); for (const h of w.hits) console.log(h.location, h.value); */ watchSymbol(name: string, options?: { length?: number; filter?: (hit: WatchHit) => boolean; maxHits?: number; }): { hits: WatchHit[]; dropped: number; stop: () => void; }; /** * Record every execution of the instruction at `target` — a raw address, or a * symbol name when debug info is loaded. The execution counterpart to * {@link watchMemory}, and the way to answer "does this code ever run". * * Counting is exact: the watchpoint fires from the CPU's own instruction step, * not from a sample. Each hit also carries `lr`, which names the caller only for * an address a `bl` reached — see {@link ExecHit.lr}. * * @example * const w = watchExecution('UpdatePlayer'); * await wait({ frames: 60 }); * w.stop(); * console.log(w.hits.length); // 0 means it really did not run */ watchExecution(target: number | string, options?: { /** Keep a hit only when this returns true. A throw is treated as `false`. */ filter?: (hit: ExecHit) => boolean; /** Cap recorded hits; `dropped` counts the rest, and `count` stays exact. */ maxHits?: number; }): { hits: ExecHit[]; /** Every execution seen, whether recorded or not — unaffected by `maxHits`. */ count: number; /** Executions that matched but were not recorded because `maxHits` was reached. */ dropped: number; stop: () => void; }; /** Remove the data watchpoints created via this engine's `watchMemory`. */ clearWatchpoints(): void; /** * Read a halfword. **Throws** on an odd address, and on one the bus decodes to * nothing. * * The hardware bus answers both: a GBA forces `LDRH` to an even address, so * `read16(0x03000103)` returns the halfword at `0x03000102` — the right answer to a * question you did not ask, and indistinguishable from the one you wanted. That is * the correct emulation and the wrong debugger. To read two bytes at an odd * address — ordinary for a struct member — use {@link readBytes}. */ read16(address: number): number; /** * Read a word. **Throws** on a misaligned address, and on one nothing backs — see * {@link read16}. * * The result is unsigned, like every other read on this surface. The bus assembles a * word with `|`, which is an int32 operator, so a word with bit 31 set comes back * negative there — harmless to the CPU, which stores it into a register, and not * harmless to a reader comparing or formatting it. */ read32(address: number): number; /** * Read 1–4 bytes as an unsigned little-endian integer, at **any** alignment — the * honest way to read a value the hardware's aligned loads cannot address. Assembled * byte by byte, so an odd address means what it says. Throws if any byte of the span * is unbacked. */ readBytes(address: number, size: number): number; /** Write a byte. **Throws** if the target is not writable memory. */ write8(address: number, value: number): void; /** * Write a halfword. **Throws** on an odd address, and if the target is not writable. * * The bus forces the store to an even address, so an odd one does not merely write * the wrong place — it overwrites the halfword *next door*. A read at the wrong * address returns a number you can still sanity-check; a write at the wrong address * silently changes the state under observation. */ write16(address: number, value: number): void; /** Write a word. **Throws** on a misaligned address — see {@link write16}. */ write32(address: number, value: number): void; /** * Write 1–4 bytes little-endian at **any** alignment, byte by byte — the counterpart * to {@link readBytes}, and the way to store a value the hardware's aligned stores * cannot address without disturbing its neighbour. */ writeBytes(address: number, size: number, value: number): void; /** * Read a global/static variable's current value by a `symbol` or * `symbol.field.subfield` path — the read counterpart to {@link watchSymbol}. The * address comes from the symbol table and the byte size (and any bitfield * shift/width) from the variable's DWARF type, so the right number of bytes is read * and a packed bitfield is decoded to its plain value. Throws if no debug info is * loaded or the path can't be resolved. * * @example * readVariable('g_game_vars.score'); // a nested struct field * readVariable('gPlayerFlags.invincible'); // a bitfield, decoded */ readVariable(path: string): number; /** * Write a global/static variable by the same `symbol` or `symbol.field.subfield` path * {@link readVariable} reads — the address and width come from the ELF, and a * bitfield is merged into its container without disturbing the fields beside it. * Throws if the path can't be resolved or the target is read-only. * * @example * writeVariable('g_game_vars.score', 1000); * writeVariable('gPlayerFlags.invincible', 1); // neighbouring bits survive */ writeVariable(path: string, value: number): void; /** * Read a DWARF-described struct member out of a struct instance at `base` — * bitfields decoded, and correct at any alignment. * * `member` is a {@link MemberLocation} from `structMember()` / `variableMember()`, * so the offset, width and bit range all come from the build's own debug info * rather than from a hand-typed constant. Unlike {@link readVariable}, the base is a * plain address, so this reaches an instance the symbol table cannot name: one * behind a pointer, an array element, or anything else placed at run time. * * @example * const f = di.structMember('PlayerState', 'invincible'); * readMember(structBase, f); // the field's value, already shifted and masked */ readMember(base: number, member: MemberLocation): number; /** * Write a DWARF-described struct member, preserving a bitfield's neighbours — the * write counterpart to {@link readMember}, reaching the same run-time instances * {@link writeVariable} cannot name. */ writeMember(base: number, member: MemberLocation, value: number): void; disassemble(address: number, count?: number, mode?: 'thumb' | 'arm'): { address: number; instruction: string; bytes: number; }[]; /** Disassemble a complete function, stopping at return instructions */ disassembleFunction(address: number, mode?: 'thumb' | 'arm'): { address: number; instruction: string; bytes: number; }[]; /** Read a null-terminated string from memory */ readString(address: number, maxLen?: number): string; getPixel(x: number, y: number): { r: number; g: number; b: number; }; getScreenRegion(x: number, y: number, width: number, height: number): Uint8Array; record(options: { name: string; interval?: number; columns?: number; }): { stopRecording: () => Promise; }; /** Parse OAM into structured sprite entries */ readOAM(): { index: number; x: number; y: number; tileId: number; width: number; height: number; palette: number; priority: number; hFlip: boolean; vFlip: boolean; enabled: boolean; mode: number; }[]; /** Read background scroll registers (camera position) */ readBgScroll(layer: number): { x: number; y: number; }; /** Read background tilemap as a grid of tile entries */ readBgTilemap(layer: number): { width: number; height: number; tileSize: number; tiles: { id: number; hFlip: boolean; vFlip: boolean; palette: number; }[]; }; /** Parse DISPCNT to show active display configuration */ readDisplayControl(): { mode: number; bg: [boolean, boolean, boolean, boolean]; obj: boolean; win0: boolean; win1: boolean; objWin: boolean; frameSelect: number; }; /** Fast hash of a screen region for change detection */ hashRegion(x: number, y: number, width: number, height: number): number; /** Register a per-frame callback fired during wait/press/pressSequence */ onFrame(callback: ((frame: number) => void) | null): void; searchMemory(options: { value: number; size?: 8 | 16 | 32; region?: 'iwram' | 'ewram' | 'both'; }): number[]; filterMemory(addresses: number[], options: { value: number; size?: 8 | 16 | 32; }): number[]; saveState(options: { name: string; }): Promise; loadState(path: string): Promise; assert(condition: AssertCondition): void; } export {}; //# sourceMappingURL=scripting.d.ts.map