@henols/vice-mcp 0.1.11 → 0.2.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/backend-detect.mts +58 -8
- package/capability-registry.ts +388 -0
- package/disasm-decoder.ts +28 -4
- package/package.json +12 -1
- package/resources/backend-detect.mjs +30 -2
- package/resources/broker-launch.mjs +166 -34
- package/resources/vice-broker.mjs +25 -4
- package/stock-cia.ts +598 -0
- package/stock-connect.ts +137 -20
- package/stock-derived.ts +67 -14
- package/stock-diagnose.ts +994 -0
- package/stock-dispatch.ts +105 -4
- package/stock-handler.ts +29 -0
- package/stock-memory-search.ts +441 -0
- package/stock-memory.ts +92 -13
- package/stock-protocol.ts +328 -0
- package/stock-recycle.ts +500 -0
- package/stock-run-until.ts +400 -0
- package/stock-runstate.ts +46 -2
- package/stock-sprites.ts +712 -0
- package/stock-symbols.ts +431 -0
- package/stock-timing.ts +562 -0
- package/stock-vicii.ts +318 -0
- package/tools-manifest.stock.json +3031 -223
- package/version.ts +279 -0
- package/vice-proxy.ts +49 -6
package/stock-vicii.ts
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// stock-vicii.ts
|
|
3
|
+
//
|
|
4
|
+
// vice_vicii_get_state -- a DERIVED tool (DERIV-05): its answer is computed
|
|
5
|
+
// CLIENT-SIDE from bytes ONE MEM_GET returns, never from a single
|
|
6
|
+
// binary-monitor opcode the way a direct tool's answer is. Registered
|
|
7
|
+
// through withDerivedTool("vice_vicii_get_state", { needsSession: true }, ...)
|
|
8
|
+
// in stock-dispatch.ts (05-07's task), never withStockSession().
|
|
9
|
+
//
|
|
10
|
+
// WHY THIS FILE EXISTS: half of criterion 3 -- "a user can read decoded
|
|
11
|
+
// VIC-II state on the stock backend". The binary monitor has no VIC-II
|
|
12
|
+
// command at all; the readable memory-mapped register block $D000-$D02E is
|
|
13
|
+
// all there is, and CLAUDE.md's constraint is explicit that VIC-II
|
|
14
|
+
// *internal* state (raster-IRQ latch, VC/VCBASE, RC, the bad-line flip-flop,
|
|
15
|
+
// the border flip-flops, the per-sprite DMA sequencer) is not recoverable on
|
|
16
|
+
// stock. Criterion 3 turns entirely on representing that honestly instead of
|
|
17
|
+
// defaulting a missing field to `0` or omitting the key.
|
|
18
|
+
//
|
|
19
|
+
// WHAT NOT TO DO:
|
|
20
|
+
// - Never turn `sidefx` on, and never accept an argument that could turn
|
|
21
|
+
// it on. $D01E (sprite-sprite collision) and $D01F (sprite-background
|
|
22
|
+
// collision) CLEAR ON READ in hardware -- this tool exists specifically
|
|
23
|
+
// to read them, so a side-effecting read would destroy the very state
|
|
24
|
+
// the caller asked for. ONE unconditional `sidefx: false` read covers
|
|
25
|
+
// the whole $D000-$D02E block; there is no per-register branch anywhere
|
|
26
|
+
// in this file.
|
|
27
|
+
// - Do not conflate $D019/$D01A with the collision registers. $D019 is an
|
|
28
|
+
// interrupt STATUS register that does NOT clear on read (it is cleared
|
|
29
|
+
// only by WRITING a 1 to a bit), and $D01A is a plain enable mask with
|
|
30
|
+
// no read side effects at all. Both are decoded exactly like every other
|
|
31
|
+
// byte in the block -- the discipline is one unconditional read, never a
|
|
32
|
+
// per-register side-effect decision.
|
|
33
|
+
// - Never report an internal-only field as `0` or omit its key. The six
|
|
34
|
+
// unrecoverable fields are enumerated once in VICII_UNAVAILABLE_FIELDS
|
|
35
|
+
// and the `unavailable` object on the answer is BUILT from that
|
|
36
|
+
// registry, never as six hand-written literals, so the registry and the
|
|
37
|
+
// answer cannot drift.
|
|
38
|
+
// - Never resolve $D018's screen/charset/bitmap pointers to absolute
|
|
39
|
+
// addresses here. They are bank-relative (the VIC bank lives in CIA2
|
|
40
|
+
// port A, $DD00, outside this chip's register block) -- report them as
|
|
41
|
+
// `screenOffset`/`charsetOffset`/`bitmapOffset` with `relativeTo: "vic
|
|
42
|
+
// bank"| and let vice_cia_get_state (cia: 2) report vicBank and
|
|
43
|
+
// vice_sprite_get resolve the whole pointer chain. Duplicating that
|
|
44
|
+
// arithmetic here would be the "re-deriving a cross-cutting seam
|
|
45
|
+
// locally" anti-pattern for a field the caller can already get from two
|
|
46
|
+
// other tools.
|
|
47
|
+
// - CR-01 (2026-08-17): never read this block through bank `0x0000` --
|
|
48
|
+
// that is the CPU view and follows `$00`/`$01` banking, so it returns
|
|
49
|
+
// the RAM underneath $D000-$DFFF whenever the running program has I/O
|
|
50
|
+
// banked out ($01 = $34/$35, routine in loaders, depackers and IRQ
|
|
51
|
+
// handlers), producing a plausible-and-wrong answer with an empty
|
|
52
|
+
// `unavailable` set. Always resolve the emulator's own `io` bank
|
|
53
|
+
// through resolveRequiredBank() first and refuse when it is absent.
|
|
54
|
+
// - Never send anything but MEM_GET -- no ExitLoop/Exit/Continue, i.e.
|
|
55
|
+
// never an unrequested resume (Phase 3 D-05). `runState` on the answer
|
|
56
|
+
// (via stockAnswer()) reports the halt honestly.
|
|
57
|
+
// - Never build the answer outside stockAnswer() (D-06).
|
|
58
|
+
// - Never import hostpath.ts or vice-proxy.ts -- this tool takes no path
|
|
59
|
+
// argument at all, and hostpath-consumers.test.ts gates this file's
|
|
60
|
+
// absence from the closed host-path consumer set.
|
|
61
|
+
//
|
|
62
|
+
// FIELD-NAME PROVENANCE (Assumption A4's mitigation): every bit-field name
|
|
63
|
+
// below was transcribed once from
|
|
64
|
+
// .claude/skills/c64-memory-mapping/memmap.json's own entries for $D011
|
|
65
|
+
// (Screen control register #1 / VIC Control Register), $D012 (raster line),
|
|
66
|
+
// $D016 (Screen control register #2 / VIC Control Register), $D018 (Memory
|
|
67
|
+
// setup register / VIC Memory Control Register), $D019 (Interrupt status
|
|
68
|
+
// register / VIC Interrupt Flag Register) and $D01A (Interrupt control
|
|
69
|
+
// register / IRQ Mask Register), and cross-checked at write time. There is
|
|
70
|
+
// no automated drift check against that file -- a "committed literal,
|
|
71
|
+
// cross-checked once" posture, matching Phase 4 D-06's stance for the
|
|
72
|
+
// disassembler's opcode table.
|
|
73
|
+
import { CommandType, memGetBody } from "./stock-protocol.ts";
|
|
74
|
+
import { convertWireError, isErrorText, stockAnswer, type StockSessionHandler } from "./stock-handler.ts";
|
|
75
|
+
import { resolveRequiredBank } from "./stock-memory.ts";
|
|
76
|
+
|
|
77
|
+
/** True iff `value` is a well-formed, generic JSON object -- not null, not
|
|
78
|
+
* an array. Matches this module tree's own isPlainObject() convention
|
|
79
|
+
* (stock-memory.ts, stock-disassemble.ts et al.). */
|
|
80
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
81
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Start of the VIC-II's memory-mapped register block. */
|
|
85
|
+
export const VICII_BASE = 0xd000;
|
|
86
|
+
/** End of the VIC-II's memory-mapped register block (inclusive). */
|
|
87
|
+
export const VICII_END = 0xd02e;
|
|
88
|
+
/** Length in bytes of [VICII_BASE, VICII_END] inclusive -- 47. */
|
|
89
|
+
export const VICII_LENGTH = VICII_END - VICII_BASE + 1;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The six VIC-II fields the binary monitor's memory-mapped register block
|
|
93
|
+
* cannot express, each paired with a reason naming WHY it is unreadable and,
|
|
94
|
+
* where applicable, WHAT is readable instead. `decodeVicii()` builds the
|
|
95
|
+
* answer's `unavailable` object from this registry -- never as hand-written
|
|
96
|
+
* literals -- so the two cannot drift (T-05-03-02's mitigation).
|
|
97
|
+
*/
|
|
98
|
+
export const VICII_UNAVAILABLE_FIELDS: readonly (readonly [string, string])[] = Object.freeze([
|
|
99
|
+
[
|
|
100
|
+
"rasterIrqLine",
|
|
101
|
+
"the raster-compare latch written to $D012 plus $D011 bit 7 -- reading those addresses returns the CURRENT raster line, not the compare value, and the binary monitor has no VIC-II command that exposes the latch. The current raster line IS available as this answer's own rasterLine.",
|
|
102
|
+
],
|
|
103
|
+
[
|
|
104
|
+
"videoCounter",
|
|
105
|
+
"the VIC-II's internal VC/VCBASE video matrix counter -- not exposed anywhere in the memory-mapped register map.",
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
"rowCounter",
|
|
109
|
+
"the VIC-II's internal RC character-row counter -- not exposed anywhere in the memory-mapped register map.",
|
|
110
|
+
],
|
|
111
|
+
[
|
|
112
|
+
"badLineCondition",
|
|
113
|
+
"the internal bad-line flip-flop that gates VIC-II DMA steals -- not exposed anywhere in the memory-mapped register map.",
|
|
114
|
+
],
|
|
115
|
+
[
|
|
116
|
+
"borderFlipFlops",
|
|
117
|
+
"the internal vertical and horizontal border flip-flops that gate the border unit -- not exposed anywhere in the memory-mapped register map.",
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
"spriteDmaState",
|
|
121
|
+
"the internal per-sprite DMA / MC / MCBASE sequencer state -- not exposed anywhere in the memory-mapped register map.",
|
|
122
|
+
],
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
/** Returns the eight per-bit booleans of `raw`, bit 0 first, so callers do
|
|
126
|
+
* not repeat the shift/mask idiom eight times per register. */
|
|
127
|
+
function bits8(raw: number): boolean[] {
|
|
128
|
+
const out: boolean[] = [];
|
|
129
|
+
for (let i = 0; i < 8; i += 1) {
|
|
130
|
+
out.push(((raw >> i) & 1) === 1);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Pure decoder over exactly 47 bytes covering $D000-$D02E. Throws a plain
|
|
137
|
+
* `Error` on any other length -- the handler guards the length before
|
|
138
|
+
* calling this, so a mismatch here is a programmer-error path, never a
|
|
139
|
+
* caller-facing one.
|
|
140
|
+
*/
|
|
141
|
+
export function decodeVicii(bytes: Uint8Array): Record<string, unknown> {
|
|
142
|
+
if (bytes.length !== VICII_LENGTH) {
|
|
143
|
+
throw new Error(`decodeVicii: expected exactly ${VICII_LENGTH} bytes, got ${bytes.length}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const registersHex = Buffer.from(bytes).toString("hex");
|
|
147
|
+
|
|
148
|
+
const spriteX: number[] = [];
|
|
149
|
+
const spriteY: number[] = [];
|
|
150
|
+
const msbByte = bytes[0x10]!;
|
|
151
|
+
for (let i = 0; i < 8; i += 1) {
|
|
152
|
+
const low = bytes[0x00 + i * 2]!;
|
|
153
|
+
const msb = (msbByte >> i) & 1;
|
|
154
|
+
spriteX.push(low | (msb << 8));
|
|
155
|
+
spriteY.push(bytes[0x01 + i * 2]!);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const spriteEnabled = bits8(bytes[0x15]!);
|
|
159
|
+
const spriteExpandY = bits8(bytes[0x17]!);
|
|
160
|
+
const spritePriorityBehindBackground = bits8(bytes[0x1b]!);
|
|
161
|
+
const spriteMulticolour = bits8(bytes[0x1c]!);
|
|
162
|
+
const spriteExpandX = bits8(bytes[0x1d]!);
|
|
163
|
+
const spriteColour: number[] = [];
|
|
164
|
+
for (let i = 0; i < 8; i += 1) {
|
|
165
|
+
spriteColour.push(bytes[0x27 + i]! & 0x0f);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const control1Raw = bytes[0x11]!;
|
|
169
|
+
const control1 = {
|
|
170
|
+
raw: control1Raw,
|
|
171
|
+
yScroll: control1Raw & 0x07,
|
|
172
|
+
rows25: ((control1Raw >> 3) & 1) === 1,
|
|
173
|
+
screenOn: ((control1Raw >> 4) & 1) === 1,
|
|
174
|
+
bitmapMode: ((control1Raw >> 5) & 1) === 1,
|
|
175
|
+
extendedBackgroundMode: ((control1Raw >> 6) & 1) === 1,
|
|
176
|
+
rasterMsb: ((control1Raw >> 7) & 1) === 1,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const control2Raw = bytes[0x16]!;
|
|
180
|
+
const control2 = {
|
|
181
|
+
raw: control2Raw,
|
|
182
|
+
xScroll: control2Raw & 0x07,
|
|
183
|
+
columns40: ((control2Raw >> 3) & 1) === 1,
|
|
184
|
+
multicolourMode: ((control2Raw >> 4) & 1) === 1,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const rasterLine = bytes[0x12]! | (((control1Raw >> 7) & 1) << 8);
|
|
188
|
+
const lightPenX = bytes[0x13]!;
|
|
189
|
+
const lightPenY = bytes[0x14]!;
|
|
190
|
+
|
|
191
|
+
const memorySetupRaw = bytes[0x18]!;
|
|
192
|
+
const memorySetup = {
|
|
193
|
+
raw: memorySetupRaw,
|
|
194
|
+
screenOffset: ((memorySetupRaw >> 4) & 0x0f) * 1024,
|
|
195
|
+
charsetOffset: ((memorySetupRaw >> 1) & 0x07) * 2048,
|
|
196
|
+
bitmapOffset: ((memorySetupRaw >> 3) & 0x01) * 8192,
|
|
197
|
+
relativeTo: "vic bank",
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const interruptStatusRaw = bytes[0x19]!;
|
|
201
|
+
const interruptStatus = {
|
|
202
|
+
raw: interruptStatusRaw,
|
|
203
|
+
rasterIrq: (interruptStatusRaw & 1) === 1,
|
|
204
|
+
spriteBackgroundCollisionIrq: ((interruptStatusRaw >> 1) & 1) === 1,
|
|
205
|
+
spriteSpriteCollisionIrq: ((interruptStatusRaw >> 2) & 1) === 1,
|
|
206
|
+
lightPenIrq: ((interruptStatusRaw >> 3) & 1) === 1,
|
|
207
|
+
anyIrqPending: ((interruptStatusRaw >> 7) & 1) === 1,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const interruptEnableRaw = bytes[0x1a]!;
|
|
211
|
+
const interruptEnable = {
|
|
212
|
+
raw: interruptEnableRaw,
|
|
213
|
+
rasterIrqEnabled: (interruptEnableRaw & 1) === 1,
|
|
214
|
+
spriteBackgroundCollisionIrqEnabled: ((interruptEnableRaw >> 1) & 1) === 1,
|
|
215
|
+
spriteSpriteCollisionIrqEnabled: ((interruptEnableRaw >> 2) & 1) === 1,
|
|
216
|
+
lightPenIrqEnabled: ((interruptEnableRaw >> 3) & 1) === 1,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const spriteSpriteCollisionRaw = bytes[0x1e]!;
|
|
220
|
+
const spriteSpriteCollision = {
|
|
221
|
+
raw: spriteSpriteCollisionRaw,
|
|
222
|
+
sprites: bits8(spriteSpriteCollisionRaw),
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const spriteBackgroundCollisionRaw = bytes[0x1f]!;
|
|
226
|
+
const spriteBackgroundCollision = {
|
|
227
|
+
raw: spriteBackgroundCollisionRaw,
|
|
228
|
+
sprites: bits8(spriteBackgroundCollisionRaw),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const borderColour = bytes[0x20]! & 0x0f;
|
|
232
|
+
const backgroundColour = bytes[0x21]! & 0x0f;
|
|
233
|
+
const extraBackgroundColour1 = bytes[0x22]! & 0x0f;
|
|
234
|
+
const extraBackgroundColour2 = bytes[0x23]! & 0x0f;
|
|
235
|
+
const extraBackgroundColour3 = bytes[0x24]! & 0x0f;
|
|
236
|
+
const spriteMulticolour1 = bytes[0x25]! & 0x0f;
|
|
237
|
+
const spriteMulticolour2 = bytes[0x26]! & 0x0f;
|
|
238
|
+
|
|
239
|
+
const unavailable: Record<string, { available: false; reason: string }> = {};
|
|
240
|
+
for (const [name, reason] of VICII_UNAVAILABLE_FIELDS) {
|
|
241
|
+
unavailable[name] = { available: false, reason };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
registersHex,
|
|
246
|
+
spriteX,
|
|
247
|
+
spriteY,
|
|
248
|
+
spriteEnabled,
|
|
249
|
+
spriteExpandY,
|
|
250
|
+
spritePriorityBehindBackground,
|
|
251
|
+
spriteMulticolour,
|
|
252
|
+
spriteExpandX,
|
|
253
|
+
spriteColour,
|
|
254
|
+
control1,
|
|
255
|
+
control2,
|
|
256
|
+
rasterLine,
|
|
257
|
+
lightPenX,
|
|
258
|
+
lightPenY,
|
|
259
|
+
memorySetup,
|
|
260
|
+
interruptStatus,
|
|
261
|
+
interruptEnable,
|
|
262
|
+
spriteSpriteCollision,
|
|
263
|
+
spriteBackgroundCollision,
|
|
264
|
+
borderColour,
|
|
265
|
+
backgroundColour,
|
|
266
|
+
extraBackgroundColour1,
|
|
267
|
+
extraBackgroundColour2,
|
|
268
|
+
extraBackgroundColour3,
|
|
269
|
+
spriteMulticolour1,
|
|
270
|
+
spriteMulticolour2,
|
|
271
|
+
unavailable,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export const handleViciiGetState: StockSessionHandler = async (args, session, _deps) => {
|
|
276
|
+
if (!isPlainObject(args)) {
|
|
277
|
+
return isErrorText("vice_vicii_get_state: arguments must be an object");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const unexpected = Object.keys(args);
|
|
281
|
+
if (unexpected.length > 0) {
|
|
282
|
+
return isErrorText(`vice_vicii_get_state: unexpected argument(s): ${unexpected.join(", ")} -- this tool takes no arguments`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const bankResolution = await resolveRequiredBank("vice_vicii_get_state", "io", session);
|
|
286
|
+
if (!bankResolution.ok) {
|
|
287
|
+
return bankResolution.result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const body = memGetBody({ sidefx: false, start: VICII_BASE, end: VICII_END, memspace: 0x00, bank: bankResolution.id });
|
|
291
|
+
|
|
292
|
+
let response;
|
|
293
|
+
try {
|
|
294
|
+
response = await session.client.send(CommandType.MemoryGet, body);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
return convertWireError("vice_vicii_get_state", err);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (response.type !== "memory_get") {
|
|
300
|
+
return isErrorText(
|
|
301
|
+
`vice_vicii_get_state: the binary monitor replied with an unexpected response type ("${response.type}"), expected "memory_get"`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (response.bytes.length !== VICII_LENGTH) {
|
|
306
|
+
return isErrorText(
|
|
307
|
+
`vice_vicii_get_state: expected ${VICII_LENGTH} byte(s), got ${response.bytes.length} -- a short read is a wrong answer, not a partial success`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return stockAnswer(session.client, {
|
|
312
|
+
base: VICII_BASE,
|
|
313
|
+
end: VICII_END,
|
|
314
|
+
length: VICII_LENGTH,
|
|
315
|
+
bank: { id: bankResolution.id, name: bankResolution.name },
|
|
316
|
+
...decodeVicii(response.bytes),
|
|
317
|
+
});
|
|
318
|
+
};
|