@tmustier/pi-nes 0.2.34 → 0.2.36
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/README.md +10 -4
- package/extensions/nes/index.ts +129 -24
- package/extensions/nes/native/nes-core/Cargo.lock +814 -1
- package/extensions/nes/native/nes-core/Cargo.toml +5 -0
- package/extensions/nes/native/nes-core/index.d.ts +1 -0
- package/extensions/nes/native/nes-core/index.node +0 -0
- package/extensions/nes/native/nes-core/native.d.ts +1 -0
- package/extensions/nes/native/nes-core/package.json +3 -1
- package/extensions/nes/native/nes-core/src/audio_cpal.rs +217 -0
- package/extensions/nes/native/nes-core/src/lib.rs +47 -1
- package/extensions/nes/nes-core.ts +6 -3
- package/package.json +1 -1
- package/spec.md +1 -1
package/README.md
CHANGED
|
@@ -51,12 +51,13 @@ On first run, you'll be prompted to set your ROM directory and display quality.
|
|
|
51
51
|
|---------|-------------|
|
|
52
52
|
| `/nes` | Pick a ROM or reattach to running session |
|
|
53
53
|
| `/nes <path>` | Load a specific ROM file |
|
|
54
|
-
| `/nes config` |
|
|
54
|
+
| `/nes config` | Quick setup (ROM directory + audio) |
|
|
55
|
+
| `/nes-config` | Toggle audio, quality, and display style + advanced options |
|
|
55
56
|
| `/nes debug` | Show FPS and memory stats |
|
|
56
57
|
|
|
57
58
|
## Configuration
|
|
58
59
|
|
|
59
|
-
Config is stored at `~/.pi/nes/config.json`. Use `/nes config` for quick setup.
|
|
60
|
+
Config is stored at `~/.pi/nes/config.json`. Use `/nes config` for quick setup (ROM dir + audio), or `/nes-config` to toggle audio/quality/style inline and access advanced options.
|
|
60
61
|
|
|
61
62
|
```json
|
|
62
63
|
{
|
|
@@ -65,6 +66,7 @@ Config is stored at `~/.pi/nes/config.json`. Use `/nes config` for quick setup.
|
|
|
65
66
|
"renderer": "image",
|
|
66
67
|
"imageQuality": "balanced",
|
|
67
68
|
"videoFilter": "ntsc-composite",
|
|
69
|
+
"enableAudio": false,
|
|
68
70
|
"pixelScale": 1.0,
|
|
69
71
|
"keybindings": {
|
|
70
72
|
"up": ["up", "w"],
|
|
@@ -88,6 +90,7 @@ Config is stored at `~/.pi/nes/config.json`. Use `/nes config` for quick setup.
|
|
|
88
90
|
| `renderer` | `"image"` | `"image"` (Kitty graphics) or `"text"` (ANSI) |
|
|
89
91
|
| `imageQuality` | `"balanced"` | `"balanced"` (30 fps) or `"high"` (60 fps) |
|
|
90
92
|
| `videoFilter` | `"ntsc-composite"` | `"off"`, `"ntsc-composite"`, `"ntsc-svideo"`, `"ntsc-rgb"` |
|
|
93
|
+
| `enableAudio` | `false` | Enable audio output (requires native core built with `audio-cpal`) |
|
|
91
94
|
| `pixelScale` | `1.0` | Display scale (0.5–4.0) |
|
|
92
95
|
|
|
93
96
|
`videoFilter` applies a lightweight CRT/NTSC-inspired pass (horizontal bleed + scanlines). It runs in the native core and is optional.
|
|
@@ -109,8 +112,8 @@ Set `"renderer": "text"` if you prefer the ANSI renderer or have display issues.
|
|
|
109
112
|
|
|
110
113
|
## Limitations
|
|
111
114
|
|
|
112
|
-
- **
|
|
113
|
-
- **No save
|
|
115
|
+
- **Audio is opt-in** — Requires building the native core with `audio-cpal` and setting `enableAudio: true`
|
|
116
|
+
- **No auto-save** — Save manually just like you would with the original NES (battery-backed SRAM)
|
|
114
117
|
|
|
115
118
|
## Vendored Dependencies
|
|
116
119
|
|
|
@@ -133,6 +136,9 @@ npm install
|
|
|
133
136
|
cd extensions/nes/native/nes-core
|
|
134
137
|
npm install && npm run build
|
|
135
138
|
|
|
139
|
+
# Build the NES core with audio (optional)
|
|
140
|
+
npm run build:audio
|
|
141
|
+
|
|
136
142
|
# Build shared memory renderer (optional, faster on Kitty)
|
|
137
143
|
cd ../kitty-shm
|
|
138
144
|
npm install && npm run build
|
package/extensions/nes/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
import { DynamicBorder, getSettingsListTheme } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import { Container, SettingsList, Spacer, Text, type SettingItem } from "@mariozechner/pi-tui";
|
|
4
6
|
import { NesOverlayComponent } from "./nes-component.js";
|
|
5
7
|
import { createNesCore } from "./nes-core.js";
|
|
6
8
|
import {
|
|
@@ -12,6 +14,8 @@ import {
|
|
|
12
14
|
loadConfig,
|
|
13
15
|
normalizeConfig,
|
|
14
16
|
saveConfig,
|
|
17
|
+
type ImageQuality,
|
|
18
|
+
type NesConfig,
|
|
15
19
|
type VideoFilter,
|
|
16
20
|
} from "./config.js";
|
|
17
21
|
import { displayPath, resolvePathInput } from "./paths.js";
|
|
@@ -24,6 +28,17 @@ const IMAGE_RENDER_INTERVAL_BALANCED_MS = 1000 / 30;
|
|
|
24
28
|
const IMAGE_RENDER_INTERVAL_HIGH_MS = 1000 / 60;
|
|
25
29
|
const TEXT_RENDER_INTERVAL_MS = 1000 / 60;
|
|
26
30
|
|
|
31
|
+
const AUDIO_OPTIONS = ["off (default)", "on"] as const;
|
|
32
|
+
const QUALITY_OPTIONS: Array<{ label: string; value: ImageQuality }> = [
|
|
33
|
+
{ label: "Balanced (default) — 30 fps", value: "balanced" },
|
|
34
|
+
{ label: "High — 60 fps", value: "high" },
|
|
35
|
+
];
|
|
36
|
+
const DISPLAY_FILTER_OPTIONS: Array<{ label: string; value: VideoFilter }> = [
|
|
37
|
+
{ label: "CRT Classic (default) — authentic scanlines + color bleed", value: "ntsc-composite" },
|
|
38
|
+
{ label: "CRT Soft — subtle retro look", value: "ntsc-rgb" },
|
|
39
|
+
{ label: "Sharp — pixel-perfect, no filtering", value: "off" },
|
|
40
|
+
];
|
|
41
|
+
|
|
27
42
|
let activeSession: NesSession | null = null;
|
|
28
43
|
|
|
29
44
|
// ROM selection helpers.
|
|
@@ -160,31 +175,13 @@ async function configureWithWizard(
|
|
|
160
175
|
}
|
|
161
176
|
}
|
|
162
177
|
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
"High — 60 fps",
|
|
166
|
-
]);
|
|
167
|
-
if (!qualityChoice) {
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const isHighQuality = qualityChoice.startsWith("High");
|
|
172
|
-
const imageQuality = isHighQuality ? "high" : "balanced";
|
|
173
|
-
|
|
174
|
-
const filterOptions: Array<{ label: string; value: VideoFilter }> = [
|
|
175
|
-
{ label: "CRT Classic (default) — authentic scanlines + color bleed", value: "ntsc-composite" },
|
|
176
|
-
{ label: "CRT Soft — subtle retro look", value: "ntsc-rgb" },
|
|
177
|
-
{ label: "Sharp — pixel-perfect, no filtering", value: "off" },
|
|
178
|
-
];
|
|
179
|
-
const filterChoice = await ctx.ui.select(
|
|
180
|
-
"Display style",
|
|
181
|
-
filterOptions.map((option) => option.label),
|
|
182
|
-
);
|
|
183
|
-
if (!filterChoice) {
|
|
178
|
+
const audioChoice = await ctx.ui.select("Audio", ["Off (default)", "On"]);
|
|
179
|
+
if (!audioChoice) {
|
|
184
180
|
return false;
|
|
185
181
|
}
|
|
186
|
-
const
|
|
187
|
-
|
|
182
|
+
const enableAudio = audioChoice === "On";
|
|
183
|
+
const imageQuality = config.imageQuality;
|
|
184
|
+
const videoFilter = config.videoFilter;
|
|
188
185
|
const pixelScale = config.pixelScale;
|
|
189
186
|
|
|
190
187
|
const defaultSaveDir = getDefaultSaveDir(config.romDir);
|
|
@@ -194,21 +191,129 @@ async function configureWithWizard(
|
|
|
194
191
|
...config,
|
|
195
192
|
romDir,
|
|
196
193
|
saveDir,
|
|
194
|
+
enableAudio,
|
|
197
195
|
imageQuality,
|
|
198
196
|
videoFilter,
|
|
199
197
|
pixelScale,
|
|
200
198
|
});
|
|
201
199
|
await saveConfig(normalized);
|
|
202
200
|
ctx.ui.notify(`Saved config to ${getConfigPath()}`, "info");
|
|
201
|
+
await ctx.ui.select(
|
|
202
|
+
"Toggle audio, quality, and more in /nes-config",
|
|
203
|
+
["Run /nes to load your games"],
|
|
204
|
+
);
|
|
203
205
|
return true;
|
|
204
206
|
}
|
|
205
207
|
|
|
208
|
+
type ConfigMenuResult = "close" | "more";
|
|
209
|
+
|
|
210
|
+
type ConfigUpdate = Partial<NesConfig>;
|
|
211
|
+
|
|
212
|
+
class NesConfigMenu extends Container {
|
|
213
|
+
private readonly settingsList: SettingsList;
|
|
214
|
+
|
|
215
|
+
constructor(
|
|
216
|
+
config: NesConfig,
|
|
217
|
+
theme: Theme,
|
|
218
|
+
onUpdate: (update: ConfigUpdate) => void,
|
|
219
|
+
onDone: (result: ConfigMenuResult) => void,
|
|
220
|
+
) {
|
|
221
|
+
super();
|
|
222
|
+
const audioValue = config.enableAudio ? "on" : "off (default)";
|
|
223
|
+
const qualityValue =
|
|
224
|
+
QUALITY_OPTIONS.find((option) => option.value === config.imageQuality)?.label
|
|
225
|
+
?? QUALITY_OPTIONS[0].label;
|
|
226
|
+
const displayValue =
|
|
227
|
+
DISPLAY_FILTER_OPTIONS.find((option) => option.value === config.videoFilter)?.label
|
|
228
|
+
?? DISPLAY_FILTER_OPTIONS[0].label;
|
|
229
|
+
const items: SettingItem[] = [
|
|
230
|
+
{
|
|
231
|
+
id: "audio",
|
|
232
|
+
label: "Audio",
|
|
233
|
+
description: "Enable audio output (requires native core built with audio-cpal)",
|
|
234
|
+
currentValue: audioValue,
|
|
235
|
+
values: [...AUDIO_OPTIONS],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: "quality",
|
|
239
|
+
label: "Quality",
|
|
240
|
+
description: "Target frame rate for image rendering",
|
|
241
|
+
currentValue: qualityValue,
|
|
242
|
+
values: QUALITY_OPTIONS.map((option) => option.label),
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: "display",
|
|
246
|
+
label: "Display style",
|
|
247
|
+
description: "CRT filter preset (applied in the native core)",
|
|
248
|
+
currentValue: displayValue,
|
|
249
|
+
values: DISPLAY_FILTER_OPTIONS.map((option) => option.label),
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: "more",
|
|
253
|
+
label: "More settings",
|
|
254
|
+
description: "Quick setup, advanced JSON, or reset defaults",
|
|
255
|
+
currentValue: "Open",
|
|
256
|
+
values: ["Open"],
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
this.settingsList = new SettingsList(
|
|
261
|
+
items,
|
|
262
|
+
8,
|
|
263
|
+
getSettingsListTheme(),
|
|
264
|
+
(id, value) => {
|
|
265
|
+
if (id === "audio") {
|
|
266
|
+
onUpdate({ enableAudio: value === "on" });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (id === "quality") {
|
|
270
|
+
const option = QUALITY_OPTIONS.find((entry) => entry.label === value);
|
|
271
|
+
onUpdate({ imageQuality: option?.value ?? config.imageQuality });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (id === "display") {
|
|
275
|
+
const option = DISPLAY_FILTER_OPTIONS.find((entry) => entry.label === value);
|
|
276
|
+
onUpdate({ videoFilter: option?.value ?? config.videoFilter });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (id === "more") {
|
|
280
|
+
onDone("more");
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
() => onDone("close"),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
this.addChild(new DynamicBorder());
|
|
287
|
+
this.addChild(new Text(theme.bold(theme.fg("accent", "NES config")), 1, 0));
|
|
288
|
+
this.addChild(new Spacer(1));
|
|
289
|
+
this.addChild(this.settingsList);
|
|
290
|
+
this.addChild(new Spacer(1));
|
|
291
|
+
this.addChild(new DynamicBorder());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
handleInput(data: string): void {
|
|
295
|
+
this.settingsList.handleInput(data);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
206
299
|
async function editConfig(ctx: ExtensionCommandContext): Promise<void> {
|
|
207
300
|
if (!ctx.hasUI) {
|
|
208
301
|
ctx.ui.notify("NES config requires interactive mode", "error");
|
|
209
302
|
return;
|
|
210
303
|
}
|
|
211
304
|
const config = await loadConfig();
|
|
305
|
+
const updateConfig = async (update: ConfigUpdate) => {
|
|
306
|
+
const normalized = normalizeConfig({ ...config, ...update });
|
|
307
|
+
await saveConfig(normalized);
|
|
308
|
+
Object.assign(config, normalized);
|
|
309
|
+
ctx.ui.notify(`Saved config to ${getConfigPath()}`, "info");
|
|
310
|
+
};
|
|
311
|
+
const action = await ctx.ui.custom<ConfigMenuResult>((_tui, theme, _keybindings, done) =>
|
|
312
|
+
new NesConfigMenu(config, theme, (update) => void updateConfig(update), done),
|
|
313
|
+
);
|
|
314
|
+
if (action !== "more") {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
212
317
|
const choice = await ctx.ui.select("NES configuration", [
|
|
213
318
|
"Quick setup",
|
|
214
319
|
"Advanced (edit config JSON)",
|