@gba-kit/gba-browser 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/emulator.d.ts +71 -0
- package/dist/emulator.d.ts.map +1 -0
- package/dist/emulator.js +377 -0
- package/dist/emulator.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/savestate-db.d.ts +29 -0
- package/dist/savestate-db.d.ts.map +1 -0
- package/dist/savestate-db.js +118 -0
- package/dist/savestate-db.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bruno Macabeus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @gba-kit/gba-browser
|
|
2
|
+
|
|
3
|
+
Framework-agnostic browser runtime for GBA emulation.
|
|
4
|
+
|
|
5
|
+
Provides `EmulatorBridge`, a class that connects the GBA emulator to a browser canvas with keyboard input, save states, breakpoints, disassembly, and IndexedDB-backed save state persistence.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @gba-kit/gba-browser
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### EmulatorBridge
|
|
16
|
+
|
|
17
|
+
The core class that wraps `@gba-kit/gba-emulator` for browser use:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { EmulatorBridge } from '@gba-kit/gba-browser';
|
|
21
|
+
|
|
22
|
+
const emulator = new EmulatorBridge();
|
|
23
|
+
|
|
24
|
+
// Attach a canvas for rendering
|
|
25
|
+
emulator.attachCanvas(document.querySelector('canvas')!);
|
|
26
|
+
|
|
27
|
+
// Load a ROM and start emulation
|
|
28
|
+
emulator.loadRom(romArrayBuffer);
|
|
29
|
+
emulator.run();
|
|
30
|
+
|
|
31
|
+
// Pause / step
|
|
32
|
+
emulator.pause();
|
|
33
|
+
emulator.stepInstruction();
|
|
34
|
+
|
|
35
|
+
// Keyboard input (wire to your own event listeners)
|
|
36
|
+
window.addEventListener('keydown', (e) => emulator.handleKeyDown(e));
|
|
37
|
+
window.addEventListener('keyup', (e) => emulator.handleKeyUp(e));
|
|
38
|
+
|
|
39
|
+
// Breakpoints
|
|
40
|
+
emulator.addBreakpoint(0x08001234);
|
|
41
|
+
|
|
42
|
+
// Save/load states
|
|
43
|
+
const { snapshot, thumbnail } = await emulator.saveState();
|
|
44
|
+
emulator.loadState(snapshot);
|
|
45
|
+
|
|
46
|
+
// Disassembly
|
|
47
|
+
const instructions = emulator.disassembleAt(0x08000000, 20);
|
|
48
|
+
|
|
49
|
+
// Memory access
|
|
50
|
+
const data = emulator.readMemory(0x03000000, 256);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Save State Persistence
|
|
54
|
+
|
|
55
|
+
IndexedDB-backed storage for save states with thumbnail previews:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { computeRomHash, deleteState, listByRom, loadState, saveState } from '@gba-kit/gba-browser';
|
|
59
|
+
|
|
60
|
+
const hash = await computeRomHash(romArrayBuffer);
|
|
61
|
+
|
|
62
|
+
const id = await saveState(hash, snapshot, thumbnailBlob, 'My Save');
|
|
63
|
+
|
|
64
|
+
const saves = await listByRom(hash);
|
|
65
|
+
|
|
66
|
+
const record = await loadState(id);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Exports
|
|
70
|
+
|
|
71
|
+
| Export | Description |
|
|
72
|
+
| ------------------ | ---------------------------------------------------------------------------- |
|
|
73
|
+
| `EmulatorBridge` | Browser GBA emulator wrapper with canvas, keyboard, breakpoints, save states |
|
|
74
|
+
| `computeRomHash()` | SHA-256 hash of ROM header for identity |
|
|
75
|
+
| `saveState()` | Save snapshot + thumbnail to IndexedDB |
|
|
76
|
+
| `loadState()` | Load a save state record by ID |
|
|
77
|
+
| `deleteState()` | Delete a save state by ID |
|
|
78
|
+
| `renameState()` | Rename a save state |
|
|
79
|
+
| `listByRom()` | List save state metadata for a ROM |
|
|
80
|
+
|
|
81
|
+
Types: `EmulatorState`, `EmulatorCallbacks`, `Breakpoint`, `SaveStateRecord`, `SaveStateMeta`
|
|
82
|
+
|
|
83
|
+
## See Also
|
|
84
|
+
|
|
85
|
+
- [`@gba-kit/gba-react`](../gba-react) — React hooks that wrap this package
|
|
86
|
+
- [`@gba-kit/gba-node`](../gba-node) — Headless Node.js runtime for scripted emulation
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ArmCpu } from '@gba-kit/arm-emulator/arm-cpu';
|
|
2
|
+
import { Gba } from '@gba-kit/gba-emulator';
|
|
3
|
+
import type { GbaSnapshot } from '@gba-kit/gba-emulator/savestate';
|
|
4
|
+
export type EmulatorState = 'idle' | 'running' | 'paused';
|
|
5
|
+
export interface Breakpoint {
|
|
6
|
+
address: number;
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface EmulatorCallbacks {
|
|
10
|
+
onStateChange: (state: EmulatorState) => void;
|
|
11
|
+
onFrame: () => void;
|
|
12
|
+
onBreakpoint: (address: number) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare class EmulatorBridge {
|
|
15
|
+
#private;
|
|
16
|
+
constructor();
|
|
17
|
+
get gba(): Gba;
|
|
18
|
+
get cpu(): ArmCpu;
|
|
19
|
+
get state(): EmulatorState;
|
|
20
|
+
/** Attach callbacks for state updates */
|
|
21
|
+
setCallbacks(callbacks: EmulatorCallbacks): void;
|
|
22
|
+
/** Attach a canvas element for rendering. Immediately shows the last frame. */
|
|
23
|
+
attachCanvas(canvas: HTMLCanvasElement): void;
|
|
24
|
+
/** Detach the current canvas (call when a view unmounts) */
|
|
25
|
+
detachCanvas(): void;
|
|
26
|
+
/** Load a ROM from an ArrayBuffer */
|
|
27
|
+
loadRom(data: ArrayBuffer): void;
|
|
28
|
+
/** Start/resume emulation */
|
|
29
|
+
run(): void;
|
|
30
|
+
/** Pause emulation */
|
|
31
|
+
pause(): void;
|
|
32
|
+
/** Stop emulation and reset */
|
|
33
|
+
stop(): void;
|
|
34
|
+
/** Execute a single CPU instruction */
|
|
35
|
+
stepInstruction(): void;
|
|
36
|
+
/** Run a single emulation frame and render to the attached canvas. */
|
|
37
|
+
runOneFrame(): void;
|
|
38
|
+
/** Step over: run until PC advances past the current instruction */
|
|
39
|
+
stepOver(): void;
|
|
40
|
+
/** Run until a specific address is reached */
|
|
41
|
+
runToAddress(address: number): void;
|
|
42
|
+
addBreakpoint(address: number): void;
|
|
43
|
+
removeBreakpoint(address: number): void;
|
|
44
|
+
toggleBreakpoint(address: number): void;
|
|
45
|
+
getBreakpoints(): Breakpoint[];
|
|
46
|
+
/** Disassemble instructions around the given address */
|
|
47
|
+
disassembleAt(address: number, count: number): Array<{
|
|
48
|
+
address: number;
|
|
49
|
+
mnemonic: string;
|
|
50
|
+
isThumb: boolean;
|
|
51
|
+
}>;
|
|
52
|
+
handleKeyDown(e: KeyboardEvent): void;
|
|
53
|
+
handleKeyUp(e: KeyboardEvent): void;
|
|
54
|
+
/** Whether audio output is currently enabled */
|
|
55
|
+
get audioEnabled(): boolean;
|
|
56
|
+
/** Enable audio output. Creates an AudioContext on first call (requires user gesture). */
|
|
57
|
+
enableAudio(): void;
|
|
58
|
+
/** Disable audio output (mute). The AudioContext is kept alive for quick re-enable. */
|
|
59
|
+
disableAudio(): void;
|
|
60
|
+
/** Toggle audio on/off. Returns the new state. */
|
|
61
|
+
toggleAudio(): boolean;
|
|
62
|
+
readMemory(address: number, size: number): Uint8Array;
|
|
63
|
+
/** Create a snapshot of the current emulator state plus a thumbnail. */
|
|
64
|
+
saveState(): Promise<{
|
|
65
|
+
snapshot: GbaSnapshot;
|
|
66
|
+
thumbnail: Blob;
|
|
67
|
+
}>;
|
|
68
|
+
/** Load a snapshot, restoring the full emulator state. */
|
|
69
|
+
loadState(snapshot: GbaSnapshot): void;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=emulator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emulator.d.ts","sourceRoot":"","sources":["../src/emulator.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAEvD,OAAO,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAC;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAgBnE,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE1D,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC9C,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,qBAAa,cAAc;;;IAoBzB,IAAI,GAAG,IAAI,GAAG,CAEb;IAED,IAAI,GAAG,IAAI,MAAM,CAEhB;IAED,IAAI,KAAK,IAAI,aAAa,CAEzB;IAED,yCAAyC;IACzC,YAAY,CAAC,SAAS,EAAE,iBAAiB,GAAG,IAAI;IAIhD,+EAA+E;IAC/E,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI;IAQ7C,4DAA4D;IAC5D,YAAY,IAAI,IAAI;IAIpB,qCAAqC;IACrC,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAuChC,6BAA6B;IAC7B,GAAG,IAAI,IAAI;IAaX,sBAAsB;IACtB,KAAK,IAAI,IAAI;IAQb,+BAA+B;IAC/B,IAAI,IAAI,IAAI;IAMZ,uCAAuC;IACvC,eAAe,IAAI,IAAI;IAWvB,sEAAsE;IACtE,WAAW,IAAI,IAAI;IAMnB,oEAAoE;IACpE,QAAQ,IAAI,IAAI;IA2BhB,8CAA8C;IAC9C,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAoBnC,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKpC,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKvC,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAQvC,cAAc,IAAI,UAAU,EAAE;IAM9B,wDAAwD;IACxD,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAsB7G,aAAa,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI;IAQrC,WAAW,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI;IAUnC,gDAAgD;IAChD,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED,0FAA0F;IAC1F,WAAW,IAAI,IAAI;IA4BnB,uFAAuF;IACvF,YAAY,IAAI,IAAI;IAQpB,kDAAkD;IAClD,WAAW,IAAI,OAAO;IAWtB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU;IAUrD,wEAAwE;IAClE,SAAS,IAAI,OAAO,CAAC;QAAE,QAAQ,EAAE,WAAW,CAAC;QAAC,SAAS,EAAE,IAAI,CAAA;KAAE,CAAC;IAkCtE,0DAA0D;IAC1D,SAAS,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI;CA6EvC"}
|
package/dist/emulator.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { disassembleArm, disassembleThumb } from '@gba-kit/arm-emulator/disassembler';
|
|
2
|
+
import { Gba } from '@gba-kit/gba-emulator';
|
|
3
|
+
/** Keyboard mapping: key → GBA button bit */
|
|
4
|
+
const KEY_MAP = {
|
|
5
|
+
ArrowRight: 4,
|
|
6
|
+
ArrowLeft: 5,
|
|
7
|
+
ArrowUp: 6,
|
|
8
|
+
ArrowDown: 7,
|
|
9
|
+
z: 0, // A
|
|
10
|
+
x: 1, // B
|
|
11
|
+
Backspace: 2, // Select
|
|
12
|
+
Enter: 3, // Start
|
|
13
|
+
a: 8, // R
|
|
14
|
+
s: 9, // L
|
|
15
|
+
};
|
|
16
|
+
export class EmulatorBridge {
|
|
17
|
+
#gba = new Gba();
|
|
18
|
+
#ctx = null;
|
|
19
|
+
#state = 'idle';
|
|
20
|
+
#animFrameId = 0;
|
|
21
|
+
#callbacks = null;
|
|
22
|
+
#breakpoints = new Map();
|
|
23
|
+
#hitBreakpoint = false;
|
|
24
|
+
/** Persistent framebuffer image, always up-to-date with the last rendered frame.
|
|
25
|
+
* Decoupled from any DOM canvas so mode switches never lose the current frame. */
|
|
26
|
+
#framebufferImage = new ImageData(240, 160);
|
|
27
|
+
// Audio output
|
|
28
|
+
#audioCtx = null;
|
|
29
|
+
#audioNode = null;
|
|
30
|
+
#audioEnabled = false;
|
|
31
|
+
constructor() { }
|
|
32
|
+
get gba() {
|
|
33
|
+
return this.#gba;
|
|
34
|
+
}
|
|
35
|
+
get cpu() {
|
|
36
|
+
return this.#gba.armCpu;
|
|
37
|
+
}
|
|
38
|
+
get state() {
|
|
39
|
+
return this.#state;
|
|
40
|
+
}
|
|
41
|
+
/** Attach callbacks for state updates */
|
|
42
|
+
setCallbacks(callbacks) {
|
|
43
|
+
this.#callbacks = callbacks;
|
|
44
|
+
}
|
|
45
|
+
/** Attach a canvas element for rendering. Immediately shows the last frame. */
|
|
46
|
+
attachCanvas(canvas) {
|
|
47
|
+
canvas.width = 240;
|
|
48
|
+
canvas.height = 160;
|
|
49
|
+
this.#ctx = canvas.getContext('2d');
|
|
50
|
+
// Immediately blit the persistent framebuffer so the canvas is never blank
|
|
51
|
+
this.#ctx.putImageData(this.#framebufferImage, 0, 0);
|
|
52
|
+
}
|
|
53
|
+
/** Detach the current canvas (call when a view unmounts) */
|
|
54
|
+
detachCanvas() {
|
|
55
|
+
this.#ctx = null;
|
|
56
|
+
}
|
|
57
|
+
/** Load a ROM from an ArrayBuffer */
|
|
58
|
+
loadRom(data) {
|
|
59
|
+
this.stop();
|
|
60
|
+
this.#gba.reset();
|
|
61
|
+
this.#gba.loadRom(new Uint8Array(data));
|
|
62
|
+
const cpu = this.#gba.armCpu;
|
|
63
|
+
// Set up initial CPU state matching post-BIOS boot:
|
|
64
|
+
// The GBA BIOS normally initializes stacks and then jumps to ROM.
|
|
65
|
+
// Since we skip the BIOS, we replicate the post-boot state.
|
|
66
|
+
// Set up IRQ mode stack pointer
|
|
67
|
+
cpu.switchMode(0x12); // MODE_IRQ
|
|
68
|
+
cpu.registers[13] = 0x03007fa0;
|
|
69
|
+
// Set up SVC mode stack pointer
|
|
70
|
+
cpu.switchMode(0x13); // MODE_SVC
|
|
71
|
+
cpu.registers[13] = 0x03007fe0;
|
|
72
|
+
// Switch to System mode (privileged, but uses USR registers)
|
|
73
|
+
cpu.switchMode(0x1f); // MODE_SYS
|
|
74
|
+
// Set USR/SYS stack pointer
|
|
75
|
+
cpu.registers[13] = 0x03007f00;
|
|
76
|
+
// Enable IRQs (clear I bit), keep FIQ disabled, System mode
|
|
77
|
+
// CPSR: 0x1F (SYS mode) with I=0, F=0, T=0 (ARM state initially)
|
|
78
|
+
cpu.cpsr = 0x1f; // SYS mode, IRQs enabled, ARM state
|
|
79
|
+
// PC to ROM entry point
|
|
80
|
+
cpu.registers[15] = 0x08000000;
|
|
81
|
+
// Read the ROM header to determine entry mode
|
|
82
|
+
// GBA ROMs start with an ARM branch instruction at 0x08000000
|
|
83
|
+
// The branch usually jumps to Thumb code, but the entry is always ARM
|
|
84
|
+
this.#setState('paused');
|
|
85
|
+
}
|
|
86
|
+
/** Start/resume emulation */
|
|
87
|
+
run() {
|
|
88
|
+
if (this.#state === 'running') {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.#setState('running');
|
|
92
|
+
this.#hitBreakpoint = false;
|
|
93
|
+
// Install breakpoint hooks if any breakpoints are set
|
|
94
|
+
this.#updateDebugHooks();
|
|
95
|
+
this.#emulationLoop();
|
|
96
|
+
}
|
|
97
|
+
/** Pause emulation */
|
|
98
|
+
pause() {
|
|
99
|
+
if (this.#state !== 'running') {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
cancelAnimationFrame(this.#animFrameId);
|
|
103
|
+
this.#setState('paused');
|
|
104
|
+
}
|
|
105
|
+
/** Stop emulation and reset */
|
|
106
|
+
stop() {
|
|
107
|
+
cancelAnimationFrame(this.#animFrameId);
|
|
108
|
+
this.disableAudio();
|
|
109
|
+
this.#setState('idle');
|
|
110
|
+
}
|
|
111
|
+
/** Execute a single CPU instruction */
|
|
112
|
+
stepInstruction() {
|
|
113
|
+
if (this.#state !== 'paused') {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.#gba.armCpu.step();
|
|
117
|
+
this.#gba.scheduler.tick(1);
|
|
118
|
+
this.#gba.apu.tick(1);
|
|
119
|
+
this.#renderFrame();
|
|
120
|
+
this.#callbacks?.onFrame();
|
|
121
|
+
}
|
|
122
|
+
/** Run a single emulation frame and render to the attached canvas. */
|
|
123
|
+
runOneFrame() {
|
|
124
|
+
this.#gba.runFrame();
|
|
125
|
+
this.#renderFrame();
|
|
126
|
+
this.#callbacks?.onFrame();
|
|
127
|
+
}
|
|
128
|
+
/** Step over: run until PC advances past the current instruction */
|
|
129
|
+
stepOver() {
|
|
130
|
+
if (this.#state !== 'paused') {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const currentPC = this.#gba.armCpu.registers[15];
|
|
134
|
+
const instrSize = this.#gba.armCpu.getT() ? 2 : 4;
|
|
135
|
+
const targetPC = currentPC + instrSize;
|
|
136
|
+
// Run until we reach the next instruction or hit a breakpoint
|
|
137
|
+
let cyclesRun = 0;
|
|
138
|
+
for (let i = 0; i < 100_000; i++) {
|
|
139
|
+
this.#gba.armCpu.step();
|
|
140
|
+
this.#gba.scheduler.tick(1);
|
|
141
|
+
cyclesRun++;
|
|
142
|
+
if (this.#gba.armCpu.registers[15] === targetPC) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
if (this.#breakpoints.has(this.#gba.armCpu.registers[15])) {
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
this.#gba.apu.tick(cyclesRun);
|
|
150
|
+
this.#renderFrame();
|
|
151
|
+
this.#callbacks?.onFrame();
|
|
152
|
+
}
|
|
153
|
+
/** Run until a specific address is reached */
|
|
154
|
+
runToAddress(address) {
|
|
155
|
+
if (this.#state !== 'paused') {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
let cyclesRun = 0;
|
|
159
|
+
for (let i = 0; i < 10_000_000; i++) {
|
|
160
|
+
this.#gba.armCpu.step();
|
|
161
|
+
this.#gba.scheduler.tick(1);
|
|
162
|
+
cyclesRun++;
|
|
163
|
+
if (this.#gba.armCpu.registers[15] === address) {
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
this.#gba.apu.tick(cyclesRun);
|
|
168
|
+
this.#renderFrame();
|
|
169
|
+
this.#callbacks?.onFrame();
|
|
170
|
+
}
|
|
171
|
+
// ─── Breakpoints ──────────────────────────────────────────────────
|
|
172
|
+
addBreakpoint(address) {
|
|
173
|
+
this.#breakpoints.set(address, { address, enabled: true });
|
|
174
|
+
this.#updateDebugHooks();
|
|
175
|
+
}
|
|
176
|
+
removeBreakpoint(address) {
|
|
177
|
+
this.#breakpoints.delete(address);
|
|
178
|
+
this.#updateDebugHooks();
|
|
179
|
+
}
|
|
180
|
+
toggleBreakpoint(address) {
|
|
181
|
+
const bp = this.#breakpoints.get(address);
|
|
182
|
+
if (bp) {
|
|
183
|
+
bp.enabled = !bp.enabled;
|
|
184
|
+
}
|
|
185
|
+
this.#updateDebugHooks();
|
|
186
|
+
}
|
|
187
|
+
getBreakpoints() {
|
|
188
|
+
return Array.from(this.#breakpoints.values());
|
|
189
|
+
}
|
|
190
|
+
// ─── Disassembly ──────────────────────────────────────────────────
|
|
191
|
+
/** Disassemble instructions around the given address */
|
|
192
|
+
disassembleAt(address, count) {
|
|
193
|
+
const result = [];
|
|
194
|
+
const isThumb = this.#gba.armCpu.getT();
|
|
195
|
+
let addr = address;
|
|
196
|
+
for (let i = 0; i < count; i++) {
|
|
197
|
+
if (isThumb) {
|
|
198
|
+
const instr = this.#gba.bus.read16(addr);
|
|
199
|
+
result.push({ address: addr, mnemonic: disassembleThumb(instr, addr), isThumb: true });
|
|
200
|
+
addr += 2;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const instr = this.#gba.bus.read32(addr);
|
|
204
|
+
result.push({ address: addr, mnemonic: disassembleArm(instr, addr), isThumb: false });
|
|
205
|
+
addr += 4;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
// ─── Keyboard Input ───────────────────────────────────────────────
|
|
211
|
+
handleKeyDown(e) {
|
|
212
|
+
const button = KEY_MAP[e.key];
|
|
213
|
+
if (button !== undefined) {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
this.#gba.input.press(button);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
handleKeyUp(e) {
|
|
219
|
+
const button = KEY_MAP[e.key];
|
|
220
|
+
if (button !== undefined) {
|
|
221
|
+
e.preventDefault();
|
|
222
|
+
this.#gba.input.release(button);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ─── Audio ──────────────────────────────────────────────────────────
|
|
226
|
+
/** Whether audio output is currently enabled */
|
|
227
|
+
get audioEnabled() {
|
|
228
|
+
return this.#audioEnabled;
|
|
229
|
+
}
|
|
230
|
+
/** Enable audio output. Creates an AudioContext on first call (requires user gesture). */
|
|
231
|
+
enableAudio() {
|
|
232
|
+
if (this.#audioEnabled) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (!this.#audioCtx) {
|
|
236
|
+
this.#audioCtx = new AudioContext({ sampleRate: 32768 });
|
|
237
|
+
// ScriptProcessorNode: pulls samples from the APU ring buffer
|
|
238
|
+
this.#audioNode = this.#audioCtx.createScriptProcessor(2048, 0, 2);
|
|
239
|
+
this.#audioNode.onaudioprocess = (e) => {
|
|
240
|
+
const left = e.outputBuffer.getChannelData(0);
|
|
241
|
+
const right = e.outputBuffer.getChannelData(1);
|
|
242
|
+
const interleaved = new Float32Array(left.length * 2);
|
|
243
|
+
this.#gba.apu.readSamples(interleaved);
|
|
244
|
+
for (let i = 0; i < left.length; i++) {
|
|
245
|
+
left[i] = interleaved[i * 2];
|
|
246
|
+
right[i] = interleaved[i * 2 + 1];
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
this.#audioNode.connect(this.#audioCtx.destination);
|
|
251
|
+
if (this.#audioCtx.state === 'suspended') {
|
|
252
|
+
this.#audioCtx.resume();
|
|
253
|
+
}
|
|
254
|
+
this.#audioEnabled = true;
|
|
255
|
+
}
|
|
256
|
+
/** Disable audio output (mute). The AudioContext is kept alive for quick re-enable. */
|
|
257
|
+
disableAudio() {
|
|
258
|
+
if (!this.#audioEnabled) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.#audioNode?.disconnect();
|
|
262
|
+
this.#audioEnabled = false;
|
|
263
|
+
}
|
|
264
|
+
/** Toggle audio on/off. Returns the new state. */
|
|
265
|
+
toggleAudio() {
|
|
266
|
+
if (this.#audioEnabled) {
|
|
267
|
+
this.disableAudio();
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
this.enableAudio();
|
|
271
|
+
}
|
|
272
|
+
return this.#audioEnabled;
|
|
273
|
+
}
|
|
274
|
+
// ─── Memory Access ────────────────────────────────────────────────
|
|
275
|
+
readMemory(address, size) {
|
|
276
|
+
const data = new Uint8Array(size);
|
|
277
|
+
for (let i = 0; i < size; i++) {
|
|
278
|
+
data[i] = this.#gba.bus.read8(address + i);
|
|
279
|
+
}
|
|
280
|
+
return data;
|
|
281
|
+
}
|
|
282
|
+
// ─── Save States ────────────────────────────────────────────────
|
|
283
|
+
/** Create a snapshot of the current emulator state plus a thumbnail. */
|
|
284
|
+
async saveState() {
|
|
285
|
+
const wasRunning = this.#state === 'running';
|
|
286
|
+
if (wasRunning) {
|
|
287
|
+
this.pause();
|
|
288
|
+
}
|
|
289
|
+
// Serialize GBA state
|
|
290
|
+
const snapshot = this.#gba.serialize();
|
|
291
|
+
// Serialize CPU into the snapshot
|
|
292
|
+
snapshot.cpu = this.#gba.armCpu.serialize();
|
|
293
|
+
// Generate thumbnail (60x40)
|
|
294
|
+
const thumbCanvas = document.createElement('canvas');
|
|
295
|
+
thumbCanvas.width = 60;
|
|
296
|
+
thumbCanvas.height = 40;
|
|
297
|
+
const thumbCtx = thumbCanvas.getContext('2d');
|
|
298
|
+
// Create a temporary canvas with the full framebuffer to scale from
|
|
299
|
+
const srcCanvas = document.createElement('canvas');
|
|
300
|
+
srcCanvas.width = 240;
|
|
301
|
+
srcCanvas.height = 160;
|
|
302
|
+
const srcCtx = srcCanvas.getContext('2d');
|
|
303
|
+
srcCtx.putImageData(this.#framebufferImage, 0, 0);
|
|
304
|
+
thumbCtx.imageSmoothingEnabled = false;
|
|
305
|
+
thumbCtx.drawImage(srcCanvas, 0, 0, 60, 40);
|
|
306
|
+
const thumbnail = await new Promise((resolve) => {
|
|
307
|
+
thumbCanvas.toBlob((blob) => resolve(blob), 'image/png');
|
|
308
|
+
});
|
|
309
|
+
return { snapshot, thumbnail };
|
|
310
|
+
}
|
|
311
|
+
/** Load a snapshot, restoring the full emulator state. */
|
|
312
|
+
loadState(snapshot) {
|
|
313
|
+
cancelAnimationFrame(this.#animFrameId);
|
|
314
|
+
// Restore GBA subsystems
|
|
315
|
+
this.#gba.deserialize(snapshot);
|
|
316
|
+
// Restore CPU
|
|
317
|
+
this.#gba.armCpu.deserialize(snapshot.cpu);
|
|
318
|
+
// Re-render frame from restored framebuffer
|
|
319
|
+
this.#renderFrame();
|
|
320
|
+
this.#callbacks?.onFrame();
|
|
321
|
+
this.#setState('paused');
|
|
322
|
+
}
|
|
323
|
+
// ─── Internal ─────────────────────────────────────────────────────
|
|
324
|
+
#emulationLoop = () => {
|
|
325
|
+
if (this.#state !== 'running') {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
this.#gba.runFrame();
|
|
329
|
+
this.#renderFrame();
|
|
330
|
+
this.#callbacks?.onFrame();
|
|
331
|
+
if (this.#hitBreakpoint) {
|
|
332
|
+
this.#setState('paused');
|
|
333
|
+
this.#callbacks?.onBreakpoint(this.#gba.armCpu.registers[15]);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
this.#animFrameId = requestAnimationFrame(this.#emulationLoop);
|
|
337
|
+
};
|
|
338
|
+
#renderFrame() {
|
|
339
|
+
const framebuffer = this.#gba.ppu.getFramebuffer();
|
|
340
|
+
const pixels = new Uint8ClampedArray(this.#framebufferImage.data.buffer);
|
|
341
|
+
// Copy ABGR framebuffer to RGBA ImageData (always update the persistent buffer)
|
|
342
|
+
for (let i = 0; i < 240 * 160; i++) {
|
|
343
|
+
const abgr = framebuffer[i];
|
|
344
|
+
const offset = i * 4;
|
|
345
|
+
pixels[offset] = abgr & 0xff; // R
|
|
346
|
+
pixels[offset + 1] = (abgr >> 8) & 0xff; // G
|
|
347
|
+
pixels[offset + 2] = (abgr >> 16) & 0xff; // B
|
|
348
|
+
pixels[offset + 3] = (abgr >> 24) & 0xff; // A
|
|
349
|
+
}
|
|
350
|
+
// Blit to the attached canvas if one exists
|
|
351
|
+
this.#ctx?.putImageData(this.#framebufferImage, 0, 0);
|
|
352
|
+
}
|
|
353
|
+
#setState(state) {
|
|
354
|
+
this.#state = state;
|
|
355
|
+
this.#callbacks?.onStateChange(state);
|
|
356
|
+
}
|
|
357
|
+
#updateDebugHooks() {
|
|
358
|
+
const hasBreakpoints = Array.from(this.#breakpoints.values()).some((bp) => bp.enabled);
|
|
359
|
+
if (hasBreakpoints) {
|
|
360
|
+
const hooks = {
|
|
361
|
+
onInstructionPre: (address) => {
|
|
362
|
+
const bp = this.#breakpoints.get(address);
|
|
363
|
+
if (bp?.enabled) {
|
|
364
|
+
this.#hitBreakpoint = true;
|
|
365
|
+
return 'break';
|
|
366
|
+
}
|
|
367
|
+
return 'continue';
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
this.#gba.armCpu.setDebugHooks(hooks);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
this.#gba.armCpu.setDebugHooks(undefined);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
//# sourceMappingURL=emulator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emulator.js","sourceRoot":"","sources":["../src/emulator.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { EmulatorBridge } from './emulator.js';
|
|
2
|
+
export type { EmulatorState, EmulatorCallbacks, Breakpoint } from './emulator.js';
|
|
3
|
+
export { computeRomHash, saveState, loadState, deleteState, renameState, listByRom } from './savestate-db.js';
|
|
4
|
+
export type { SaveStateRecord, SaveStateMeta } from './savestate-db.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGlF,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9G,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO;AACP,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C,yBAAyB;AACzB,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Save State IndexedDB Storage
|
|
3
|
+
*
|
|
4
|
+
* Stores GBA save states with thumbnail previews in IndexedDB.
|
|
5
|
+
* Uses structured clone for efficient storage of typed arrays.
|
|
6
|
+
*/
|
|
7
|
+
import type { GbaSnapshot } from '@gba-kit/gba-emulator/savestate';
|
|
8
|
+
export interface SaveStateRecord {
|
|
9
|
+
id: number;
|
|
10
|
+
label: string;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
romHash: string;
|
|
13
|
+
thumbnail: Blob;
|
|
14
|
+
snapshot: GbaSnapshot;
|
|
15
|
+
}
|
|
16
|
+
export type SaveStateMeta = Omit<SaveStateRecord, 'snapshot'>;
|
|
17
|
+
/** Compute a ROM hash from the first 192 bytes of the ROM. */
|
|
18
|
+
export declare function computeRomHash(rom: ArrayBuffer): Promise<string>;
|
|
19
|
+
/** Save a snapshot to IndexedDB. Returns the auto-generated ID. */
|
|
20
|
+
export declare function saveState(romHash: string, snapshot: GbaSnapshot, thumbnail: Blob, label?: string): Promise<number>;
|
|
21
|
+
/** Load a full save state record by ID. */
|
|
22
|
+
export declare function loadState(id: number): Promise<SaveStateRecord | undefined>;
|
|
23
|
+
/** Delete a save state by ID. */
|
|
24
|
+
export declare function deleteState(id: number): Promise<void>;
|
|
25
|
+
/** Rename a save state. */
|
|
26
|
+
export declare function renameState(id: number, label: string): Promise<void>;
|
|
27
|
+
/** List save state metadata (no snapshot data) for a specific ROM. */
|
|
28
|
+
export declare function listByRom(romHash: string): Promise<SaveStateMeta[]>;
|
|
29
|
+
//# sourceMappingURL=savestate-db.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"savestate-db.d.ts","sourceRoot":"","sources":["../src/savestate-db.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAEnE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,WAAW,CAAC;CACvB;AAED,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;AA6B9D,8DAA8D;AAC9D,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAMtE;AAED,mEAAmE;AACnE,wBAAsB,SAAS,CAC7B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,WAAW,EACrB,SAAS,EAAE,IAAI,EACf,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAgBjB;AAED,2CAA2C;AAC3C,wBAAsB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAShF;AAED,iCAAiC;AACjC,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS3D;AAED,2BAA2B;AAC3B,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB1E;AAED,sEAAsE;AACtE,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAsBzE"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const DB_NAME = 'gba-kit-savestates';
|
|
2
|
+
const DB_VERSION = 1;
|
|
3
|
+
const STORE_NAME = 'slots';
|
|
4
|
+
let dbPromise = null;
|
|
5
|
+
function openDb() {
|
|
6
|
+
if (dbPromise) {
|
|
7
|
+
return dbPromise;
|
|
8
|
+
}
|
|
9
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
10
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
11
|
+
request.onupgradeneeded = () => {
|
|
12
|
+
const db = request.result;
|
|
13
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
14
|
+
db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
request.onsuccess = () => resolve(request.result);
|
|
18
|
+
request.onerror = () => {
|
|
19
|
+
dbPromise = null;
|
|
20
|
+
reject(request.error);
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
return dbPromise;
|
|
24
|
+
}
|
|
25
|
+
/** Compute a ROM hash from the first 192 bytes of the ROM. */
|
|
26
|
+
export async function computeRomHash(rom) {
|
|
27
|
+
const header = rom.slice(0, 192);
|
|
28
|
+
const hash = await crypto.subtle.digest('SHA-256', header);
|
|
29
|
+
return Array.from(new Uint8Array(hash))
|
|
30
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
31
|
+
.join('');
|
|
32
|
+
}
|
|
33
|
+
/** Save a snapshot to IndexedDB. Returns the auto-generated ID. */
|
|
34
|
+
export async function saveState(romHash, snapshot, thumbnail, label) {
|
|
35
|
+
const db = await openDb();
|
|
36
|
+
const record = {
|
|
37
|
+
label: label ?? `Save #${Date.now()}`,
|
|
38
|
+
timestamp: Date.now(),
|
|
39
|
+
romHash,
|
|
40
|
+
thumbnail,
|
|
41
|
+
snapshot,
|
|
42
|
+
};
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
45
|
+
const store = tx.objectStore(STORE_NAME);
|
|
46
|
+
const request = store.add(record);
|
|
47
|
+
request.onsuccess = () => resolve(request.result);
|
|
48
|
+
request.onerror = () => reject(request.error);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Load a full save state record by ID. */
|
|
52
|
+
export async function loadState(id) {
|
|
53
|
+
const db = await openDb();
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
56
|
+
const store = tx.objectStore(STORE_NAME);
|
|
57
|
+
const request = store.get(id);
|
|
58
|
+
request.onsuccess = () => resolve(request.result);
|
|
59
|
+
request.onerror = () => reject(request.error);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Delete a save state by ID. */
|
|
63
|
+
export async function deleteState(id) {
|
|
64
|
+
const db = await openDb();
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
67
|
+
const store = tx.objectStore(STORE_NAME);
|
|
68
|
+
const request = store.delete(id);
|
|
69
|
+
request.onsuccess = () => resolve();
|
|
70
|
+
request.onerror = () => reject(request.error);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** Rename a save state. */
|
|
74
|
+
export async function renameState(id, label) {
|
|
75
|
+
const db = await openDb();
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
78
|
+
const store = tx.objectStore(STORE_NAME);
|
|
79
|
+
const getReq = store.get(id);
|
|
80
|
+
getReq.onsuccess = () => {
|
|
81
|
+
const record = getReq.result;
|
|
82
|
+
if (!record) {
|
|
83
|
+
reject(new Error(`Save state ${id} not found`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
record.label = label;
|
|
87
|
+
const putReq = store.put(record);
|
|
88
|
+
putReq.onsuccess = () => resolve();
|
|
89
|
+
putReq.onerror = () => reject(putReq.error);
|
|
90
|
+
};
|
|
91
|
+
getReq.onerror = () => reject(getReq.error);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** List save state metadata (no snapshot data) for a specific ROM. */
|
|
95
|
+
export async function listByRom(romHash) {
|
|
96
|
+
const db = await openDb();
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
99
|
+
const store = tx.objectStore(STORE_NAME);
|
|
100
|
+
const request = store.getAll();
|
|
101
|
+
request.onsuccess = () => {
|
|
102
|
+
const all = request.result;
|
|
103
|
+
const filtered = all
|
|
104
|
+
.filter((r) => r.romHash === romHash)
|
|
105
|
+
.map(({ id, label, timestamp, romHash, thumbnail }) => ({
|
|
106
|
+
id,
|
|
107
|
+
label,
|
|
108
|
+
timestamp,
|
|
109
|
+
romHash,
|
|
110
|
+
thumbnail,
|
|
111
|
+
}))
|
|
112
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
113
|
+
resolve(filtered);
|
|
114
|
+
};
|
|
115
|
+
request.onerror = () => reject(request.error);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=savestate-db.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"savestate-db.js","sourceRoot":"","sources":["../src/savestate-db.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gba-kit/gba-browser",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser runtime for GBA emulation",
|
|
5
|
+
"author": "macabeus",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/macabeus/gba-kit",
|
|
19
|
+
"directory": "packages/gba-browser"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/macabeus/gba-kit",
|
|
22
|
+
"bugs": "https://github.com/macabeus/gba-kit/issues",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"gba",
|
|
25
|
+
"gameboy-advance",
|
|
26
|
+
"emulator",
|
|
27
|
+
"arm7tdmi",
|
|
28
|
+
"typescript"
|
|
29
|
+
],
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@gba-kit/arm-emulator": "0.1.0",
|
|
35
|
+
"@gba-kit/gba-emulator": "0.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^6.0.2"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"check-types": "tsc --noEmit",
|
|
43
|
+
"lint": "eslint src/"
|
|
44
|
+
}
|
|
45
|
+
}
|