Skip to content

Commit 08b8511

Browse files
committed
Add types to the public API
Remove `@ts-nocheck` from the exported classes (NES, Controller, Browser, GameGenie) and add real type annotations to the surface that consumers see. The internal emulator modules (CPU, PPU, PAPU, mappers, browser helpers) still have `@ts-nocheck` — references to them from the public classes are typed as `any` so the implementation keeps working unchanged. Public types exported from `src/index.ts`: - `NESOptions`, `EmulatorData`, `ControllerId`, `RomData` - `ButtonKey`, `ControllerState` - `BrowserOptions` - `GameGeniePatch` The class fields, method parameters, and return types on the exported classes themselves are also annotated so consumers get IntelliSense on things like `nes.loadROM(...)`, `controller.buttonDown(...)`, etc. All 577 tests still pass and the webpack build is unchanged.
1 parent 65ee0ed commit 08b8511

5 files changed

Lines changed: 200 additions & 66 deletions

File tree

src/browser/index.ts

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,37 @@
1-
// @ts-nocheck
21
import NES from "../nes.ts";
2+
import type { RomData } from "../nes.ts";
33
import Screen from "./screen.ts";
44
import Speakers from "./speakers.ts";
55
import FrameTimer from "./frame-timer.ts";
66
import KeyboardController from "./keyboard.ts";
77
import GamepadController from "./gamepad.ts";
88

9+
export interface BrowserOptions {
10+
/** The container element to render into. */
11+
container: HTMLElement;
12+
/** ROM data to load immediately. If omitted, call loadROM() then start(). */
13+
romData?: RomData | null;
14+
/** Called when the emulator encounters an error during frame execution. */
15+
onError?: (error: unknown) => void;
16+
/** Called when battery-backed SRAM is written. */
17+
onBatteryRamWrite?: (address: number, value: number) => void;
18+
}
19+
20+
// The browser helper classes (Screen, Speakers, FrameTimer, etc.) still
21+
// have @ts-nocheck for a loose conversion, so their public shapes are not
22+
// accurately typed. We use `any` for their field types here so consumers
23+
// of the Browser class still get precise types on the public API surface.
24+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
25+
type Internal = any;
26+
927
// Debug logging, enabled via localStorage.jsnes_debug = 1
1028
let debugEnabled = false;
1129
try {
1230
debugEnabled = !!localStorage.getItem("jsnes_debug");
1331
} catch {
1432
// localStorage not available
1533
}
16-
function debug(...args) {
34+
function debug(...args: unknown[]): void {
1735
if (debugEnabled) console.log(...args);
1836
}
1937

@@ -31,12 +49,23 @@ function debug(...args) {
3149
* If romData is omitted, call browser.loadROM(data) then browser.start().
3250
*/
3351
export default class Browser {
34-
constructor(options = {}) {
52+
readonly nes: NES;
53+
readonly keyboard: Internal;
54+
readonly gamepad: Internal;
55+
56+
private _options: BrowserOptions;
57+
private _screen: Internal;
58+
private _speakers: Internal;
59+
private _frameTimer: Internal;
60+
private _gamepadPolling: Internal;
61+
private _fpsInterval: ReturnType<typeof setInterval> | undefined;
62+
63+
constructor(options: BrowserOptions) {
3564
this._options = options;
3665

3766
// Create screen (creates <canvas> inside container)
3867
this._screen = new Screen(options.container, {
39-
onMouseDown: (x, y) => {
68+
onMouseDown: (x: number, y: number) => {
4069
this.nes.zapperMove(x, y);
4170
this.nes.zapperFireDown();
4271
},
@@ -117,21 +146,24 @@ export default class Browser {
117146
}
118147
}
119148

120-
start() {
149+
/** Start emulation. Called automatically if romData is provided to constructor. */
150+
start(): void {
121151
this._frameTimer.start();
122152
this._speakers.start();
123153
this._fpsInterval = setInterval(() => {
124154
debug(`FPS: ${this.nes.getFPS()}`);
125155
}, 1000);
126156
}
127157

128-
stop() {
158+
/** Pause emulation. */
159+
stop(): void {
129160
this._frameTimer.stop();
130161
this._speakers.stop();
131162
clearInterval(this._fpsInterval);
132163
}
133164

134-
loadROM(data) {
165+
/** Load a new ROM and start emulation. */
166+
loadROM(data: RomData): void {
135167
this.stop();
136168
this.nes.loadROM(data);
137169
this.start();
@@ -140,18 +172,19 @@ export default class Browser {
140172
/**
141173
* Fill parent element with screen. Call if parent element changes size.
142174
*/
143-
fitInParent() {
175+
fitInParent(): void {
144176
this._screen.fitInParent();
145177
}
146178

147-
screenshot() {
179+
/** Get a screenshot as an HTMLImageElement. */
180+
screenshot(): HTMLImageElement {
148181
return this._screen.screenshot();
149182
}
150183

151184
/**
152185
* Clean up all resources: stop emulation, remove event listeners, remove canvas.
153186
*/
154-
destroy() {
187+
destroy(): void {
155188
this.stop();
156189
document.removeEventListener("keydown", this.keyboard.handleKeyDown);
157190
document.removeEventListener("keyup", this.keyboard.handleKeyUp);
@@ -163,7 +196,10 @@ export default class Browser {
163196
/**
164197
* Load ROM data from a URL via XHR.
165198
*/
166-
static loadROMFromURL(url, callback) {
199+
static loadROMFromURL(
200+
url: string,
201+
callback: (error: Error | null, data?: string) => void,
202+
): XMLHttpRequest {
167203
var req = new XMLHttpRequest();
168204
req.open("GET", url);
169205
req.overrideMimeType("text/plain; charset=x-user-defined");
@@ -175,7 +211,7 @@ export default class Browser {
175211
} else if (this.status === 0) {
176212
// Aborted, ignore
177213
} else {
178-
req.onerror();
214+
req.onerror!(new ProgressEvent("error"));
179215
}
180216
};
181217
req.send();

src/controller.ts

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,45 @@
1-
// @ts-nocheck
21
import { toJSON, fromJSON } from "./utils.ts";
32

3+
export type ButtonKey = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
4+
5+
export interface ControllerState {
6+
state: number[];
7+
baseA: number;
8+
baseB: number;
9+
turboA: boolean;
10+
turboB: boolean;
11+
turboToggle: boolean;
12+
}
13+
414
class Controller {
5-
static BUTTON_A = 0;
6-
static BUTTON_B = 1;
7-
static BUTTON_SELECT = 2;
8-
static BUTTON_START = 3;
9-
static BUTTON_UP = 4;
10-
static BUTTON_DOWN = 5;
11-
static BUTTON_LEFT = 6;
12-
static BUTTON_RIGHT = 7;
15+
static readonly BUTTON_A = 0;
16+
static readonly BUTTON_B = 1;
17+
static readonly BUTTON_SELECT = 2;
18+
static readonly BUTTON_START = 3;
19+
static readonly BUTTON_UP = 4;
20+
static readonly BUTTON_DOWN = 5;
21+
static readonly BUTTON_LEFT = 6;
22+
static readonly BUTTON_RIGHT = 7;
1323
// Turbo buttons rapidly toggle A/B each frame while held, simulating the
1424
// extra buttons on the NES Advantage and dogbone controllers.
15-
static BUTTON_TURBO_A = 8;
16-
static BUTTON_TURBO_B = 9;
25+
static readonly BUTTON_TURBO_A = 8;
26+
static readonly BUTTON_TURBO_B = 9;
1727

18-
static JSON_PROPERTIES = [
28+
static readonly JSON_PROPERTIES = [
1929
"state",
2030
"baseA",
2131
"baseB",
2232
"turboA",
2333
"turboB",
2434
"turboToggle",
25-
];
35+
] as const;
36+
37+
state: number[];
38+
baseA: number;
39+
baseB: number;
40+
turboA: boolean;
41+
turboB: boolean;
42+
turboToggle: boolean;
2643

2744
constructor() {
2845
this.state = new Array(8);
@@ -38,7 +55,7 @@ class Controller {
3855
this.turboToggle = false;
3956
}
4057

41-
buttonDown(key) {
58+
buttonDown(key: ButtonKey): void {
4259
if (key === Controller.BUTTON_TURBO_A) {
4360
this.turboA = true;
4461
} else if (key === Controller.BUTTON_TURBO_B) {
@@ -50,7 +67,7 @@ class Controller {
5067
}
5168
}
5269

53-
buttonUp(key) {
70+
buttonUp(key: ButtonKey): void {
5471
if (key === Controller.BUTTON_TURBO_A) {
5572
this.turboA = false;
5673
this.state[Controller.BUTTON_A] = this.baseA;
@@ -67,7 +84,7 @@ class Controller {
6784
// Called once per frame to toggle turbo button states. Produces a ~30 Hz
6885
// press rate at 60 FPS, matching the fast end of the NES Advantage's
6986
// adjustable turbo range.
70-
clock() {
87+
clock(): void {
7188
if (!this.turboA && !this.turboB) return;
7289
this.turboToggle = !this.turboToggle;
7390
if (this.turboA) {
@@ -78,11 +95,11 @@ class Controller {
7895
}
7996
}
8097

81-
toJSON() {
82-
return toJSON(this);
98+
toJSON(): ControllerState {
99+
return toJSON(this) as ControllerState;
83100
}
84101

85-
fromJSON(s) {
102+
fromJSON(s: ControllerState): void {
86103
fromJSON(this, s);
87104
}
88105
}

src/gamegenie.ts

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,45 @@
1-
// @ts-nocheck
21
const LETTER_VALUES = "APZLGITYEOXUKSVN";
32

4-
function toDigit(letter) {
3+
function toDigit(letter: string): number {
54
return LETTER_VALUES.indexOf(letter);
65
}
76

8-
function toLetter(digit) {
7+
function toLetter(digit: number): string {
98
return LETTER_VALUES[digit];
109
}
1110

12-
function toHex(n, width) {
11+
function toHex(n: number, width: number): string {
1312
const s = n.toString(16);
1413
return "0000".substring(0, width - s.length) + s;
1514
}
1615

16+
export interface GameGeniePatch {
17+
addr: number;
18+
value: number;
19+
wantskey?: boolean;
20+
key?: number;
21+
}
22+
1723
class GameGenie {
24+
patches: GameGeniePatch[];
25+
enabled: boolean;
26+
// Callback invoked when patches or enabled state change, so the CPU
27+
// can swap its loadFromCartridge function pointer. Set by NES after
28+
// construction.
29+
onChange: (() => void) | null;
30+
1831
constructor() {
1932
this.patches = [];
2033
this.enabled = true;
21-
// Callback invoked when patches or enabled state change, so the CPU
22-
// can swap its loadFromCartridge function pointer. Set by NES after
23-
// construction.
2434
this.onChange = null;
2535
}
2636

27-
setEnabled(enabled) {
37+
setEnabled(enabled: boolean): void {
2838
this.enabled = enabled;
2939
if (this.onChange) this.onChange();
3040
}
3141

32-
addCode(code) {
42+
addCode(code: string): void {
3343
const patch = this.decode(code);
3444
if (!patch) {
3545
throw new Error(`Invalid Game Genie code: ${code}`);
@@ -38,12 +48,12 @@ class GameGenie {
3848
if (this.onChange) this.onChange();
3949
}
4050

41-
addPatch(addr, value, key) {
51+
addPatch(addr: number, value: number, key?: number): void {
4252
this.patches.push({ addr, value, key });
4353
if (this.onChange) this.onChange();
4454
}
4555

46-
removeAllCodes() {
56+
removeAllCodes(): void {
4757
this.patches = [];
4858
if (this.onChange) this.onChange();
4959
}
@@ -52,7 +62,7 @@ class GameGenie {
5262
// Game Genie works by intercepting ROM reads and substituting values.
5363
// The address is masked to 15 bits because Game Genie ignores the
5464
// highest bit (ROM is mirrored in $8000-$FFFF).
55-
applyCodes(addr, value) {
65+
applyCodes(addr: number, value: number): number {
5666
if (!this.enabled) return value;
5767

5868
for (let i = 0; i < this.patches.length; ++i) {
@@ -68,7 +78,7 @@ class GameGenie {
6878
return value;
6979
}
7080

71-
decode(code) {
81+
decode(code: string): GameGeniePatch | null {
7282
if (code.includes(":")) return this.decodeHex(code);
7383

7484
const digits = code.toUpperCase().split("").map(toDigit);
@@ -83,7 +93,7 @@ class GameGenie {
8393
((digits[2] & 7) << 4) +
8494
(digits[3] & 8) +
8595
(digits[4] & 7);
86-
let key;
96+
let key: number | undefined;
8797

8898
if (digits.length === 8) {
8999
value += digits[7] & 8;
@@ -101,7 +111,12 @@ class GameGenie {
101111
return { value, addr, wantskey, key };
102112
}
103113

104-
encodeHex(addr, value, key, wantskey) {
114+
encodeHex(
115+
addr: number,
116+
value: number,
117+
key?: number,
118+
wantskey?: boolean,
119+
): string {
105120
let s = toHex(addr, 4) + ":" + toHex(value, 2);
106121

107122
if (key !== undefined || wantskey) {
@@ -115,7 +130,7 @@ class GameGenie {
115130
return s;
116131
}
117132

118-
decodeHex(s) {
133+
decodeHex(s: string): GameGeniePatch | null {
119134
const match = s.match(/([0-9a-fA-F]+):([0-9a-fA-F]+)(\?[0-9a-fA-F]*)?/);
120135
if (!match) return null;
121136

@@ -130,8 +145,13 @@ class GameGenie {
130145
return { value, addr, wantskey, key };
131146
}
132147

133-
encode(addr, value, key, wantskey) {
134-
const digits = Array(6);
148+
encode(
149+
addr: number,
150+
value: number,
151+
key?: number,
152+
wantskey?: boolean,
153+
): string {
154+
const digits: number[] = Array(6);
135155

136156
digits[0] = (value & 7) + ((value >> 4) & 8);
137157
digits[1] = ((value >> 4) & 7) + ((addr >> 4) & 8);

src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
// @ts-nocheck
21
import Browser from "./browser/index.ts";
32
import Controller from "./controller.ts";
43
import GameGenie from "./gamegenie.ts";
54
import NES from "./nes.ts";
65

76
export { Browser, Controller, GameGenie, NES };
7+
8+
export type { BrowserOptions } from "./browser/index.ts";
9+
export type { ButtonKey, ControllerState } from "./controller.ts";
10+
export type { GameGeniePatch } from "./gamegenie.ts";
11+
export type { ControllerId, EmulatorData, NESOptions, RomData } from "./nes.ts";

0 commit comments

Comments
 (0)