@henols/vice-mcp 0.2.1 → 0.2.3
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 +4 -3
- package/THIRD-PARTY-NOTICES.md +423 -25
- package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +3040 -0
- package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
- package/anno-coverage.ts +2465 -0
- package/anno-derive.ts +590 -0
- package/anno-details.ts +169 -0
- package/anno-enum-gen.ts +919 -0
- package/anno-export-asm.ts +2396 -0
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-index.ts +150 -0
- package/anno-join.ts +480 -0
- package/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
- package/anno-provenance-ledger.ts +472 -0
- package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
- package/{r2000-regbits.json → anno-regbits.json} +2 -2
- package/anno-register.ts +399 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +4002 -0
- package/anno-symbols.ts +266 -0
- package/anno-tools.ts +3264 -0
- package/anno-types.ts +1909 -0
- package/backend-detect.mts +124 -312
- package/block-class.ts +201 -0
- package/build.ts +4 -2
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/disasm-decoder.ts +14 -14
- package/disasm-opcodes.ts +4 -4
- package/disasm-renderer.ts +2 -2
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/hostpath.ts +1 -1
- package/incident-record.ts +23 -12
- package/install-resources.ts +30 -14
- package/memmap-lookup.ts +285 -0
- package/package.json +48 -23
- package/prg-image.ts +118 -0
- package/repo-root.ts +107 -8
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +189 -16
- package/resources/broker-epoch.mjs +1 -1
- package/resources/broker-kill.mjs +8 -2
- package/resources/broker-launch.mjs +373 -214
- package/resources/broker-state.mjs +64 -18
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2561 -0
- package/resources/vice-broker.mjs +330 -184
- package/resources/vice-launcher.sh +130 -12
- package/stock-address.ts +6 -6
- package/stock-cia.ts +2 -2
- package/stock-condition.ts +8 -8
- package/stock-connect.ts +10 -6
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +200 -68
- package/stock-execution.ts +5 -3
- package/stock-handler.ts +1 -1
- package/stock-input.ts +9 -9
- package/stock-machine.ts +17 -6
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +17 -12
- package/stock-recycle.ts +83 -2
- package/stock-registers.ts +54 -29
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-sprites.ts +3 -3
- package/stock-symbols.ts +13 -13
- package/stock-timing.ts +2 -2
- package/stock-vicii.ts +1 -1
- package/stop-oracle.ts +167 -0
- package/text-capability-probe.ts +660 -0
- package/text-connect.ts +157 -0
- package/text-protocol.ts +810 -0
- package/text-tools.ts +778 -0
- package/textmon-backtrace.ts +385 -0
- package/textmon-cpuhistory.ts +335 -0
- package/textmon-memmap.ts +494 -0
- package/textmon-profile.ts +458 -0
- package/textmon-registers.ts +748 -0
- package/tools-manifest.stock.json +864 -3
- package/version.ts +1 -1
- package/vice-broker-client.ts +189 -42
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +392 -2175
- package/vsf-slice.ts +640 -0
- package/capability-registry.ts +0 -388
- package/r2000-cli.ts +0 -1103
- package/r2000-d64.ts +0 -310
- package/r2000-enum-gen.ts +0 -574
- package/r2000-launch.ts +0 -357
- package/r2000-mcp-client.ts +0 -596
- package/r2000-project.ts +0 -190
- package/r2000-symbols.ts +0 -388
- package/r2000-tools.ts +0 -914
- package/r2000-verify.ts +0 -184
- package/refresh-manifest.ts +0 -124
- package/tools-manifest.json +0 -1223
- package/vice-probe.ts +0 -278
- package/vice-sync.ts +0 -336
- package/vice.ts +0 -772
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
// textmon-registers.ts
|
|
2
|
+
//
|
|
3
|
+
// THE ONE owning module for VICE's `io` text-monitor output (PARSE-02,
|
|
4
|
+
// PARSE-03). Nothing else in this tree reads or interprets an `io` reply --
|
|
5
|
+
// a handler dials the wire, this module is the only place that turns the
|
|
6
|
+
// framed text into structured data.
|
|
7
|
+
//
|
|
8
|
+
// WHY THIS FILE EXISTS RATHER THAN LIVING INSIDE THE TOOL HANDLER: the same
|
|
9
|
+
// reasoning `textmon-memmap.ts`, `textmon-profile.ts` and `disasm-decoder.ts`
|
|
10
|
+
// state for themselves applies here unchanged -- keeping the parser
|
|
11
|
+
// import-free of transport code means any future non-tool consumer can
|
|
12
|
+
// depend on this one file without dragging in a socket, and PARSE-03's own
|
|
13
|
+
// never-throw discipline (D-42-3, decided once in plan 42-01 for all five
|
|
14
|
+
// text parsers this phase adds) means this module can never be the thing
|
|
15
|
+
// that silently launders a wire error into a parse result, because it
|
|
16
|
+
// never touches the wire at all.
|
|
17
|
+
//
|
|
18
|
+
// FOUR OUTCOMES, NOT TWO, FOR THE DECODED-PROSE SECTION (originally "three
|
|
19
|
+
// outcomes" per this plan's own D-42-3 refinement; a fourth outcome was
|
|
20
|
+
// added by plan 42-10 to close CR-01). `prof flat`, `chis`, `bt` and
|
|
21
|
+
// `memmapshow` are closed formats: every field is either recognised or it is
|
|
22
|
+
// drift. `io`'s middle section is not -- it is free-form decoded prose whose
|
|
23
|
+
// line set differs per chip, and a parser that refused every line it had
|
|
24
|
+
// not seen before would refuse a legitimate reply from a chip the committed
|
|
25
|
+
// captures never sampled. So THIS module has four outcomes for that section
|
|
26
|
+
// only: recognised fields decode to typed values; a recognised label whose
|
|
27
|
+
// VALUE cannot be parsed refuses by name (that is drift, and it is loud, via
|
|
28
|
+
// the `unparseable-value` refusal code); a line matching no recognised shape
|
|
29
|
+
// at all is preserved verbatim in `unrecognisedLines`, which the result
|
|
30
|
+
// carries and the tool reports as a count -- never mapped onto a typed
|
|
31
|
+
// field; and a required field that was NEVER OBSERVED AT ALL -- because its
|
|
32
|
+
// one recognised line was dropped, renamed or reordered out of recognition
|
|
33
|
+
// -- refuses by name via `incomplete-decoded-state`, naming every absent
|
|
34
|
+
// field, rather than being cast onto `IoDecodedState` with `undefined`
|
|
35
|
+
// silently sitting in a field typed `number`, `boolean` or `string`. The
|
|
36
|
+
// hex dump, the memspace marker and the sprite table are CLOSED sets and
|
|
37
|
+
// refuse like every other format's fields.
|
|
38
|
+
//
|
|
39
|
+
// WHAT NOT TO DO:
|
|
40
|
+
// - Never import anything -- not `node:` anything, not `text-protocol.ts`,
|
|
41
|
+
// not `textmon-fixtures.ts`, not even a type-only import of a sibling
|
|
42
|
+
// module. Purity is asserted mechanically by this file's own test.
|
|
43
|
+
// - Never throw on a malformed or drifted input (D-42-3). Return the
|
|
44
|
+
// discriminated `IoRegistersParseResult` instead.
|
|
45
|
+
// - Never slice the sprite table at guessed/literal column widths. Some
|
|
46
|
+
// real rows (`X-Pos:`) have NO separating space at all between adjacent
|
|
47
|
+
// values -- a whitespace split would collapse them into one token and
|
|
48
|
+
// silently shift every sprite's value one column left. Column offsets
|
|
49
|
+
// are derived from the `Sprites:` header line's OWN token positions,
|
|
50
|
+
// every time, never a literal width constant.
|
|
51
|
+
// - Never treat VICE's own two graceful-degradation replies ("No details
|
|
52
|
+
// available." / "No I/O regs available") as a successful decode of an
|
|
53
|
+
// empty chip. A caller that received `{ok:true, value:{sections:[]}}`
|
|
54
|
+
// for one of these could not tell "the chip reported it has nothing to
|
|
55
|
+
// show" from "this parse found nothing" -- the second reads like a
|
|
56
|
+
// defect in this project. Both strings are source-traced from
|
|
57
|
+
// `monitor.c:1980-2000` (not live-observed -- see this module's test
|
|
58
|
+
// file for why) and are recognised as their own named refusal codes,
|
|
59
|
+
// each carrying the observed string verbatim.
|
|
60
|
+
// - Never let an unrecognised decoded-prose line populate a typed field.
|
|
61
|
+
// Push it verbatim to `unrecognisedLines` and move on -- see the
|
|
62
|
+
// "THREE OUTCOMES" note above.
|
|
63
|
+
// - Never accept a memspace marker other than the main CPU's ("C"). A
|
|
64
|
+
// drive-contaminated default memspace (CLAUDE.md's own documented
|
|
65
|
+
// constraint) makes every following field describe the wrong CPU; the
|
|
66
|
+
// refusal names the `vice_device_console` remedy tool by name, matching
|
|
67
|
+
// plan 42-02's own established convention for the same hazard.
|
|
68
|
+
// - Never restore an unchecked cast onto `IoDecodedState`, and never
|
|
69
|
+
// supply a default, a zero, an empty string or an inferred value for a
|
|
70
|
+
// field the decoded-prose block never observed (this was CR-01).
|
|
71
|
+
// `REQUIRED_IO_DECODED_KEYS`'s completeness check in `decodeProseLines()`
|
|
72
|
+
// exists precisely to stop that -- an absent observation is refused by
|
|
73
|
+
// name via `incomplete-decoded-state`, never manufactured.
|
|
74
|
+
// - Never restore an unconditional sprite-table requirement (this was
|
|
75
|
+
// WR-02), and never narrow `vice_io_registers`'s advertised 0-65535
|
|
76
|
+
// address range to make a non-VIC-II chip disappear. A CIA1, CIA2 or
|
|
77
|
+
// SID reply is a legitimate, in-schema answer; it is refused by its own
|
|
78
|
+
// name via `unsupported-chip`, never dressed up as a malformed VIC-II
|
|
79
|
+
// section and never made unreachable by shrinking the published
|
|
80
|
+
// contract.
|
|
81
|
+
|
|
82
|
+
/** One 64-byte VIC-II register dump, decoded from four `>C:aaaa ...` rows.
|
|
83
|
+
* `baseAddress` is the first row's own address field -- never assumed. */
|
|
84
|
+
export interface IoRegisterDump {
|
|
85
|
+
readonly baseAddress: number;
|
|
86
|
+
readonly bytes: readonly number[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The three VIC-II display-mode bits, decoded from `Mode: ... (ECM/BMM/MCM=a/b/c)`. */
|
|
90
|
+
export interface IoDisplayMode {
|
|
91
|
+
readonly name: string;
|
|
92
|
+
readonly ecm: boolean;
|
|
93
|
+
readonly bmm: boolean;
|
|
94
|
+
readonly mcm: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The typed decode of `io`'s free-form middle prose section -- see this
|
|
98
|
+
* module's header comment for the three-outcome rule this section alone
|
|
99
|
+
* follows. Every field here comes from one of the six recognised label
|
|
100
|
+
* prefixes this module knows; nothing here is ever populated from an
|
|
101
|
+
* unrecognised line (see `IoRegisters.unrecognisedLines`). */
|
|
102
|
+
export interface IoDecodedState {
|
|
103
|
+
readonly rasterCycle: number;
|
|
104
|
+
readonly rasterLine: number;
|
|
105
|
+
readonly rasterIrqLine: number;
|
|
106
|
+
readonly mode: IoDisplayMode;
|
|
107
|
+
readonly borderColor: number;
|
|
108
|
+
readonly backgroundColor: number;
|
|
109
|
+
readonly scrollX: number;
|
|
110
|
+
readonly scrollY: number;
|
|
111
|
+
readonly rasterCounter: number;
|
|
112
|
+
readonly idle: boolean;
|
|
113
|
+
readonly screenColumns: number;
|
|
114
|
+
readonly screenRows: number;
|
|
115
|
+
readonly vc: number;
|
|
116
|
+
readonly vcbase: number;
|
|
117
|
+
readonly vmli: number;
|
|
118
|
+
readonly phi1: number;
|
|
119
|
+
readonly videoBase: number;
|
|
120
|
+
readonly charsetBase: number;
|
|
121
|
+
readonly charsetSource: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The closed, ten-member row-label set the sprite table carries -- an
|
|
125
|
+
* unknown row in it is drift, not free prose (see this module's header
|
|
126
|
+
* comment's "THREE OUTCOMES" note, which does NOT apply to this table). */
|
|
127
|
+
export type SpriteRowLabel =
|
|
128
|
+
| "Enabled"
|
|
129
|
+
| "DMA/dis"
|
|
130
|
+
| "Pointer"
|
|
131
|
+
| "MC"
|
|
132
|
+
| "MCBASE"
|
|
133
|
+
| "X-Pos"
|
|
134
|
+
| "Y-Pos"
|
|
135
|
+
| "X/Y-Exp"
|
|
136
|
+
| "Pri./MC"
|
|
137
|
+
| "Color";
|
|
138
|
+
|
|
139
|
+
/** One sprite-table row: a closed-set label plus exactly eight raw,
|
|
140
|
+
* trimmed cell values -- sliced at the header's own declared offsets, not
|
|
141
|
+
* at a guessed width (see this module's header comment). */
|
|
142
|
+
export interface SpriteTableRow {
|
|
143
|
+
readonly label: SpriteRowLabel;
|
|
144
|
+
readonly values: readonly string[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The full sprite table. `columns` is the count asserted from the
|
|
148
|
+
* `Sprites:` header line itself -- always 8 for a well-formed table, and
|
|
149
|
+
* the parser refuses (`sprite-column-count`) rather than proceeding when
|
|
150
|
+
* it is anything else. */
|
|
151
|
+
export interface SpriteTable {
|
|
152
|
+
readonly columns: number;
|
|
153
|
+
readonly rows: readonly SpriteTableRow[];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** One decoded chip section: the header names the chip, the dump is its
|
|
157
|
+
* 64-byte register range, `decoded` is the typed middle-prose state, and
|
|
158
|
+
* `sprites` is the fixed-shape sprite table. */
|
|
159
|
+
export interface IoChipSection {
|
|
160
|
+
readonly chip: string;
|
|
161
|
+
readonly dump: IoRegisterDump;
|
|
162
|
+
readonly decoded: IoDecodedState;
|
|
163
|
+
readonly sprites: SpriteTable;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The full decoded `io` reply. `sections` is an array even though the
|
|
167
|
+
* tool in plan 42-07 always dials a single-address form -- the command
|
|
168
|
+
* itself accepts a bare invocation that dumps every chip, and a parser
|
|
169
|
+
* that hard-assumed one section would refuse a legitimate reply. The array
|
|
170
|
+
* shape survives that reasoning unchanged, but only VIC-II sections
|
|
171
|
+
* decode (plan 42-11, WR-02): a CIA1, CIA2 or SID section is refused by
|
|
172
|
+
* the chip's own name rather than being reported as a VIC-II section
|
|
173
|
+
* missing its sprite table.
|
|
174
|
+
* `unrecognisedLines` carries every decoded-prose line this module did not
|
|
175
|
+
* recognise, verbatim, across every section -- drift stays visible in the
|
|
176
|
+
* answer instead of being dropped from it, and a caller reporting a
|
|
177
|
+
* non-zero count is reporting a real finding. */
|
|
178
|
+
export interface IoRegisters {
|
|
179
|
+
readonly sections: readonly IoChipSection[];
|
|
180
|
+
readonly unrecognisedLines: readonly string[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The closed refusal-code union (D-42-3): an empty response, a malformed
|
|
184
|
+
* dump row, an unrecognised memspace marker, a recognised decoded-prose
|
|
185
|
+
* label whose value could not be parsed, an unrecognised sprite-row label,
|
|
186
|
+
* a sprite header declaring a column count other than eight, the two
|
|
187
|
+
* graceful-degradation outcomes (see this module's header comment), a
|
|
188
|
+
* decoded-prose block that did not carry every one of `IoDecodedState`'s
|
|
189
|
+
* required fields -- a required label was dropped, renamed or reordered
|
|
190
|
+
* out of recognition (plan 42-10, CR-01) -- and a chip section this parser
|
|
191
|
+
* does not decode, whose register dump nonetheless read cleanly (plan
|
|
192
|
+
* 42-11, WR-02): the sprite-table and decoded-prose requirements are
|
|
193
|
+
* VIC-II-only, so a CIA1, CIA2 or SID reply is refused by the chip's own
|
|
194
|
+
* name rather than being reported as a malformed VIC-II section. */
|
|
195
|
+
export type IoRegistersRefusalCode =
|
|
196
|
+
| "empty-response"
|
|
197
|
+
| "malformed-dump"
|
|
198
|
+
| "unrecognised-memspace"
|
|
199
|
+
| "unparseable-value"
|
|
200
|
+
| "unrecognised-sprite-row"
|
|
201
|
+
| "sprite-column-count"
|
|
202
|
+
| "no-details-available"
|
|
203
|
+
| "no-io-regs-available"
|
|
204
|
+
| "incomplete-decoded-state"
|
|
205
|
+
| "unsupported-chip";
|
|
206
|
+
|
|
207
|
+
/** A parser refusal (D-42-3): returned, never thrown. `line` and
|
|
208
|
+
* `lineNumber` name the offending content whenever one exists. */
|
|
209
|
+
export interface TextParseRefusal {
|
|
210
|
+
readonly code: IoRegistersRefusalCode;
|
|
211
|
+
readonly message: string;
|
|
212
|
+
readonly line: string;
|
|
213
|
+
readonly lineNumber: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** D-42-3's discriminated shape: a parser never throws, it returns one of
|
|
217
|
+
* these two arms. */
|
|
218
|
+
export type IoRegistersParseResult = { ok: true; value: IoRegisters } | { ok: false; refusal: TextParseRefusal };
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Framing constants -- this module's OWN copies, never imported from
|
|
222
|
+
// text-protocol.ts (purity rule above). `io` carries no leading entry-echo
|
|
223
|
+
// prompt (unlike `memmapshow`) per fixtures/textmon/README.md's own
|
|
224
|
+
// "Framing" section -- only a TRAILING exit-prompt is ever stripped. Note
|
|
225
|
+
// while reading the captures: this command's trailing prompt carries a
|
|
226
|
+
// DIFFERENT address than the other four commands' (`(C:$d040)` here, vs.
|
|
227
|
+
// `(C:$e5d1)` for `prof flat`/`bt` on the same run) -- the parser keys on
|
|
228
|
+
// the prompt's SHAPE, never its value.
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
const TRAILING_PROMPT_RE = /\(C:\$[0-9A-Fa-f]{4}\)\s*$/;
|
|
232
|
+
|
|
233
|
+
/** VICE's own two graceful-degradation strings for this command
|
|
234
|
+
* (`monitor.c:1980-2000`), traced from source rather than observed live --
|
|
235
|
+
* this project did not build a VICE binary with the relevant chip support
|
|
236
|
+
* disabled this session (see this module's test file for the full
|
|
237
|
+
* citation). Exact literal match, case- and punctuation-sensitive. */
|
|
238
|
+
const NO_DETAILS_AVAILABLE_TEXT = "No details available.";
|
|
239
|
+
const NO_IO_REGS_AVAILABLE_TEXT = "No I/O regs available";
|
|
240
|
+
|
|
241
|
+
/** A chip-section header: a bare identifier then a single trailing colon,
|
|
242
|
+
* nothing else on the line (e.g. `VIC-II:`). */
|
|
243
|
+
const CHIP_HEADER_RE = /^([A-Za-z][A-Za-z0-9_./-]*):$/;
|
|
244
|
+
|
|
245
|
+
/** The loose shape that identifies a line as dump-row-SHAPED (`>` + a
|
|
246
|
+
* memspace letter + `:` + a 4-hex-digit address + two spaces) -- used to
|
|
247
|
+
* decide whether to keep consuming dump rows at all, distinct from the
|
|
248
|
+
* STRICT full-row regex below that validates one once this much has
|
|
249
|
+
* already matched. A line matching this loose shape but failing the strict
|
|
250
|
+
* regex is a STRUCTURAL malformation (`malformed-dump`), never silently
|
|
251
|
+
* skipped as "not a dump row". */
|
|
252
|
+
const DUMP_ROW_PREFIX_RE = /^>([A-Za-z]):([0-9a-fA-F]{4}) /;
|
|
253
|
+
|
|
254
|
+
/** One dump row's full, strict layout: memspace letter, 4-hex address, two
|
|
255
|
+
* spaces, four groups of four 2-hex-digit values (each group
|
|
256
|
+
* space-separated internally, two spaces between groups), three spaces,
|
|
257
|
+
* then the 16-character rendered character column. */
|
|
258
|
+
const DUMP_ROW_RE =
|
|
259
|
+
/^>([A-Za-z]):([0-9a-fA-F]{4}) ((?:[0-9a-fA-F]{2} ){3}[0-9a-fA-F]{2}) ((?:[0-9a-fA-F]{2} ){3}[0-9a-fA-F]{2}) ((?:[0-9a-fA-F]{2} ){3}[0-9a-fA-F]{2}) ((?:[0-9a-fA-F]{2} ){3}[0-9a-fA-F]{2}) (.{16})$/;
|
|
260
|
+
|
|
261
|
+
/** The main CPU's memspace marker letter -- the only one this module
|
|
262
|
+
* accepts anywhere a marker appears (CLAUDE.md's documented
|
|
263
|
+
* `default_memspace` contamination constraint). */
|
|
264
|
+
const MAIN_CPU_MEMSPACE = "C";
|
|
265
|
+
|
|
266
|
+
/** The only chip whose decoded display state and sprite table this parser
|
|
267
|
+
* models. The chip gate in `parseIoRegisters()` is what keeps every other
|
|
268
|
+
* chip section from being reported as a malformed VIC-II reply (plan
|
|
269
|
+
* 42-11, WR-02). */
|
|
270
|
+
const VIC_II_CHIP_NAME = "VIC-II";
|
|
271
|
+
|
|
272
|
+
const SPRITE_HEADER_PREFIX = "Sprites: ";
|
|
273
|
+
|
|
274
|
+
const SPRITE_ROW_LABELS: readonly SpriteRowLabel[] = [
|
|
275
|
+
"Enabled",
|
|
276
|
+
"DMA/dis",
|
|
277
|
+
"Pointer",
|
|
278
|
+
"MC",
|
|
279
|
+
"MCBASE",
|
|
280
|
+
"X-Pos",
|
|
281
|
+
"Y-Pos",
|
|
282
|
+
"X/Y-Exp",
|
|
283
|
+
"Pri./MC",
|
|
284
|
+
"Color",
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
function isSpriteRowLabel(value: string): value is SpriteRowLabel {
|
|
288
|
+
return (SPRITE_ROW_LABELS as readonly string[]).includes(value);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function makeRefusal(code: IoRegistersRefusalCode, message: string, line: string, lineNumber: number): TextParseRefusal {
|
|
292
|
+
return { code, message, line, lineNumber };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** The six recognised decoded-prose label prefixes, each paired with the
|
|
296
|
+
* full-line detail regex that extracts its typed values once the prefix
|
|
297
|
+
* has matched. A line whose PREFIX matches one of these but whose DETAIL
|
|
298
|
+
* regex does not is `unparseable-value`, naming this recogniser's `label`.
|
|
299
|
+
* A line matching no prefix at all is unrecognised (see the module header
|
|
300
|
+
* comment's "THREE OUTCOMES" note). */
|
|
301
|
+
const PROSE_RECOGNISERS: ReadonlyArray<{
|
|
302
|
+
label: string;
|
|
303
|
+
prefix: string;
|
|
304
|
+
detailRe: RegExp;
|
|
305
|
+
apply: (m: RegExpExecArray, state: Partial<Record<keyof IoDecodedState, unknown>>) => void;
|
|
306
|
+
}> = [
|
|
307
|
+
{
|
|
308
|
+
label: "Raster cycle/line",
|
|
309
|
+
prefix: "Raster cycle/line:",
|
|
310
|
+
detailRe: /^Raster cycle\/line: (\d+)\/(\d+) IRQ: (\d+)$/,
|
|
311
|
+
apply: (m, s) => {
|
|
312
|
+
s.rasterCycle = Number(m[1]);
|
|
313
|
+
s.rasterLine = Number(m[2]);
|
|
314
|
+
s.rasterIrqLine = Number(m[3]);
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
label: "Mode",
|
|
319
|
+
prefix: "Mode:",
|
|
320
|
+
detailRe: /^Mode: (.+) \(ECM\/BMM\/MCM=(\d)\/(\d)\/(\d)\)$/,
|
|
321
|
+
apply: (m, s) => {
|
|
322
|
+
s.mode = { name: m[1], ecm: m[2] === "1", bmm: m[3] === "1", mcm: m[4] === "1" } satisfies IoDisplayMode;
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
label: "Colors",
|
|
327
|
+
prefix: "Colors:",
|
|
328
|
+
detailRe: /^Colors: Border: ([0-9a-fA-F]) BG: ([0-9a-fA-F])$/,
|
|
329
|
+
apply: (m, s) => {
|
|
330
|
+
s.borderColor = parseInt(m[1]!, 16);
|
|
331
|
+
s.backgroundColor = parseInt(m[2]!, 16);
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
label: "Scroll X/Y",
|
|
336
|
+
prefix: "Scroll X/Y:",
|
|
337
|
+
detailRe: /^Scroll X\/Y: (\d+)\/(\d+), RC (\d+), Idle: (\d+), (\d+)x(\d+)$/,
|
|
338
|
+
apply: (m, s) => {
|
|
339
|
+
s.scrollX = Number(m[1]);
|
|
340
|
+
s.scrollY = Number(m[2]);
|
|
341
|
+
s.rasterCounter = Number(m[3]);
|
|
342
|
+
s.idle = m[4] === "1";
|
|
343
|
+
s.screenColumns = Number(m[5]);
|
|
344
|
+
s.screenRows = Number(m[6]);
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
label: "VC",
|
|
349
|
+
prefix: "VC $",
|
|
350
|
+
detailRe: /^VC \$([0-9a-fA-F]+), VCBASE \$([0-9a-fA-F]+), VMLI\s+(\d+), Phi1 \$([0-9a-fA-F]+)$/,
|
|
351
|
+
apply: (m, s) => {
|
|
352
|
+
s.vc = parseInt(m[1]!, 16);
|
|
353
|
+
s.vcbase = parseInt(m[2]!, 16);
|
|
354
|
+
s.vmli = Number(m[3]);
|
|
355
|
+
s.phi1 = parseInt(m[4]!, 16);
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
label: "Video",
|
|
360
|
+
prefix: "Video $",
|
|
361
|
+
detailRe: /^Video \$([0-9a-fA-F]+), Charset \$([0-9a-fA-F]+) \((.+)\)$/,
|
|
362
|
+
apply: (m, s) => {
|
|
363
|
+
s.videoBase = parseInt(m[1]!, 16);
|
|
364
|
+
s.charsetBase = parseInt(m[2]!, 16);
|
|
365
|
+
s.charsetSource = m[3];
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
/** The nineteen keys `IoDecodedState` declares as required, in the EXACT
|
|
371
|
+
* order the interface declares them. This is the guard for the one cast in
|
|
372
|
+
* this module -- `decodeProseLines()`'s `state as IoDecodedState` -- and the
|
|
373
|
+
* refusal message below lists absent keys in this same order, so the same
|
|
374
|
+
* drifted input always produces a byte-identical message. TypeScript cannot
|
|
375
|
+
* derive an array of keys from an interface at runtime, so this list's
|
|
376
|
+
* equality with `IoDecodedState`'s own declared field set is asserted by
|
|
377
|
+
* this module's own test (a census read off this file's real source), not
|
|
378
|
+
* by review alone -- a field added to the interface later without a
|
|
379
|
+
* matching addition here would otherwise silently re-open CR-01. */
|
|
380
|
+
export const REQUIRED_IO_DECODED_KEYS: readonly (keyof IoDecodedState)[] = [
|
|
381
|
+
"rasterCycle",
|
|
382
|
+
"rasterLine",
|
|
383
|
+
"rasterIrqLine",
|
|
384
|
+
"mode",
|
|
385
|
+
"borderColor",
|
|
386
|
+
"backgroundColor",
|
|
387
|
+
"scrollX",
|
|
388
|
+
"scrollY",
|
|
389
|
+
"rasterCounter",
|
|
390
|
+
"idle",
|
|
391
|
+
"screenColumns",
|
|
392
|
+
"screenRows",
|
|
393
|
+
"vc",
|
|
394
|
+
"vcbase",
|
|
395
|
+
"vmli",
|
|
396
|
+
"phi1",
|
|
397
|
+
"videoBase",
|
|
398
|
+
"charsetBase",
|
|
399
|
+
"charsetSource",
|
|
400
|
+
];
|
|
401
|
+
|
|
402
|
+
type DecodeProseResult =
|
|
403
|
+
| { ok: true; state: IoDecodedState }
|
|
404
|
+
| { ok: false; refusal: TextParseRefusal };
|
|
405
|
+
|
|
406
|
+
/** Decodes the free-form decoded-prose block per this module's
|
|
407
|
+
* three-outcome rule: a recognised prefix with a matching detail shape
|
|
408
|
+
* updates `state`; a recognised prefix whose detail shape fails to match
|
|
409
|
+
* refuses `unparseable-value`, naming the recogniser's label; a line
|
|
410
|
+
* matching no recognised prefix at all is pushed verbatim onto
|
|
411
|
+
* `unrecognisedLines` and never populates a typed field. Bounded by
|
|
412
|
+
* construction: one pass over `proseLines`, each iteration tries at most
|
|
413
|
+
* six fixed recognisers, no recursion. */
|
|
414
|
+
function decodeProseLines(proseLines: readonly string[], startLineNumber: number, unrecognisedLines: string[]): DecodeProseResult {
|
|
415
|
+
const state: Partial<Record<keyof IoDecodedState, unknown>> = {};
|
|
416
|
+
for (let i = 0; i < proseLines.length; i++) {
|
|
417
|
+
const original = proseLines[i]!;
|
|
418
|
+
const trimmed = original.trim();
|
|
419
|
+
const lineNumber = startLineNumber + i;
|
|
420
|
+
const recogniser = PROSE_RECOGNISERS.find((r) => trimmed.startsWith(r.prefix));
|
|
421
|
+
if (!recogniser) {
|
|
422
|
+
unrecognisedLines.push(original);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const match = recogniser.detailRe.exec(trimmed);
|
|
426
|
+
if (!match) {
|
|
427
|
+
return {
|
|
428
|
+
ok: false,
|
|
429
|
+
refusal: makeRefusal(
|
|
430
|
+
"unparseable-value",
|
|
431
|
+
`io: line ${lineNumber} carries the recognised "${recogniser.label}" label but its value could not be parsed: ${JSON.stringify(original)}`,
|
|
432
|
+
original,
|
|
433
|
+
lineNumber,
|
|
434
|
+
),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
recogniser.apply(match, state);
|
|
438
|
+
}
|
|
439
|
+
const absentKeys = REQUIRED_IO_DECODED_KEYS.filter((key) => !(key in state));
|
|
440
|
+
if (absentKeys.length > 0) {
|
|
441
|
+
const lastLineNumber = startLineNumber + proseLines.length - 1;
|
|
442
|
+
return {
|
|
443
|
+
ok: false,
|
|
444
|
+
refusal: makeRefusal(
|
|
445
|
+
"incomplete-decoded-state",
|
|
446
|
+
`io: the decoded-prose block spanning lines ${startLineNumber}-${lastLineNumber} did not carry every ` +
|
|
447
|
+
`required field -- absent: ${absentKeys.join(", ")} -- never returned as a complete decode`,
|
|
448
|
+
"",
|
|
449
|
+
startLineNumber,
|
|
450
|
+
),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
return { ok: true, state: state as IoDecodedState };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
interface SpriteHeaderResult {
|
|
457
|
+
ok: true;
|
|
458
|
+
boundaries: number[];
|
|
459
|
+
}
|
|
460
|
+
interface SpriteHeaderFailure {
|
|
461
|
+
ok: false;
|
|
462
|
+
count: number;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Derives the sprite table's column boundaries from the `Sprites:` header
|
|
466
|
+
* line's OWN token positions -- never a guessed literal width (see this
|
|
467
|
+
* module's header comment). Each column label's start position, minus one
|
|
468
|
+
* character, becomes that column's left boundary; the header line's own
|
|
469
|
+
* length is the final (right) boundary. This one-character shift is what
|
|
470
|
+
* makes the SAME boundary set correctly slice every row even though real
|
|
471
|
+
* rows' own label fields vary in length (7-11 characters) -- see this
|
|
472
|
+
* module's test file for the measured proof. */
|
|
473
|
+
function deriveSpriteBoundaries(headerLine: string): SpriteHeaderResult | SpriteHeaderFailure {
|
|
474
|
+
const allTokens = [...headerLine.matchAll(/\S+/g)];
|
|
475
|
+
const columnTokens = allTokens.slice(1); // drop the "Sprites:" label token itself
|
|
476
|
+
if (columnTokens.length !== 8) {
|
|
477
|
+
return { ok: false, count: columnTokens.length };
|
|
478
|
+
}
|
|
479
|
+
const boundaries = columnTokens.map((m) => m.index! - 1);
|
|
480
|
+
boundaries.push(headerLine.length);
|
|
481
|
+
return { ok: true, boundaries };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
type SpriteRowResult = { ok: true; row: SpriteTableRow } | { ok: false; label: string };
|
|
485
|
+
|
|
486
|
+
function parseSpriteRow(line: string, boundaries: readonly number[]): SpriteRowResult {
|
|
487
|
+
const rawLabel = line.slice(0, boundaries[0]).trim();
|
|
488
|
+
const label = rawLabel.replace(/:$/, "");
|
|
489
|
+
if (!isSpriteRowLabel(label)) {
|
|
490
|
+
return { ok: false, label: rawLabel };
|
|
491
|
+
}
|
|
492
|
+
const values: string[] = [];
|
|
493
|
+
for (let c = 0; c < 8; c++) {
|
|
494
|
+
values.push(line.slice(boundaries[c]!, boundaries[c + 1]).trim());
|
|
495
|
+
}
|
|
496
|
+
return { ok: true, row: { label, values } };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Parses `io`'s framed text-monitor reply into a structured
|
|
501
|
+
* {@link IoRegisters}. Input is the string `TextMonitorClient.command()`
|
|
502
|
+
* resolves to, or a fixture's `text` -- both must parse identically. Never
|
|
503
|
+
* throws (D-42-3): every failure mode returns `{ ok: false, refusal }`
|
|
504
|
+
* naming exactly what was wrong and where.
|
|
505
|
+
*
|
|
506
|
+
* Bounded by construction: one pass over the payload's lines, advancing a
|
|
507
|
+
* cursor that always moves forward by at least one line per iteration, no
|
|
508
|
+
* recursion anywhere in this function.
|
|
509
|
+
*/
|
|
510
|
+
export function parseIoRegisters(text: string): IoRegistersParseResult {
|
|
511
|
+
if (typeof text !== "string" || text.trim() === "") {
|
|
512
|
+
return {
|
|
513
|
+
ok: false,
|
|
514
|
+
refusal: makeRefusal(
|
|
515
|
+
"empty-response",
|
|
516
|
+
"io: the response was empty or whitespace-only -- never decoded as a zero-section reply, because an empty " +
|
|
517
|
+
"response and a real capture that recorded no chips anywhere are two different facts",
|
|
518
|
+
"",
|
|
519
|
+
0,
|
|
520
|
+
),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const body = text.replace(TRAILING_PROMPT_RE, "");
|
|
525
|
+
const trimmedBody = body.trim();
|
|
526
|
+
if (trimmedBody === "") {
|
|
527
|
+
return {
|
|
528
|
+
ok: false,
|
|
529
|
+
refusal: makeRefusal(
|
|
530
|
+
"empty-response",
|
|
531
|
+
"io: the response contained only prompt text and no content -- never decoded as a zero-section reply",
|
|
532
|
+
"",
|
|
533
|
+
0,
|
|
534
|
+
),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (trimmedBody === NO_DETAILS_AVAILABLE_TEXT) {
|
|
539
|
+
return {
|
|
540
|
+
ok: false,
|
|
541
|
+
refusal: makeRefusal(
|
|
542
|
+
"no-details-available",
|
|
543
|
+
`io: the chip reported "${NO_DETAILS_AVAILABLE_TEXT}" (source-traced, monitor.c:1980-2000) -- a named ` +
|
|
544
|
+
"outcome, never decoded as a successful zero-section reply",
|
|
545
|
+
trimmedBody,
|
|
546
|
+
1,
|
|
547
|
+
),
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
if (trimmedBody === NO_IO_REGS_AVAILABLE_TEXT) {
|
|
551
|
+
return {
|
|
552
|
+
ok: false,
|
|
553
|
+
refusal: makeRefusal(
|
|
554
|
+
"no-io-regs-available",
|
|
555
|
+
`io: the bank reported "${NO_IO_REGS_AVAILABLE_TEXT}" (source-traced, monitor.c:1980-2000) -- a named ` +
|
|
556
|
+
"outcome, never decoded as a successful zero-section reply",
|
|
557
|
+
trimmedBody,
|
|
558
|
+
1,
|
|
559
|
+
),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const lines = body.split("\n");
|
|
564
|
+
// A trailing "\n" before the (already-stripped) prompt splits into one
|
|
565
|
+
// trailing empty element -- drop exactly that split artifact, never any
|
|
566
|
+
// other blank line, so a genuinely blank line inside the payload still
|
|
567
|
+
// participates in section framing below rather than being silently
|
|
568
|
+
// swallowed here.
|
|
569
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
570
|
+
lines.pop();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const sections: IoChipSection[] = [];
|
|
574
|
+
const unrecognisedLines: string[] = [];
|
|
575
|
+
|
|
576
|
+
let i = 0;
|
|
577
|
+
while (i < lines.length) {
|
|
578
|
+
const headerLine = lines[i]!;
|
|
579
|
+
const lineNumber = i + 1;
|
|
580
|
+
const chipMatch = CHIP_HEADER_RE.exec(headerLine);
|
|
581
|
+
if (!chipMatch) {
|
|
582
|
+
unrecognisedLines.push(headerLine);
|
|
583
|
+
i++;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const chip = chipMatch[1]!;
|
|
587
|
+
i++;
|
|
588
|
+
|
|
589
|
+
// --- dump rows -----------------------------------------------------
|
|
590
|
+
const byteRows: number[][] = [];
|
|
591
|
+
let baseAddress: number | undefined;
|
|
592
|
+
while (i < lines.length && DUMP_ROW_PREFIX_RE.test(lines[i] ?? "")) {
|
|
593
|
+
const line = lines[i]!;
|
|
594
|
+
const rowLineNumber = i + 1;
|
|
595
|
+
const memspaceMatch = /^>([A-Za-z]):/.exec(line)!;
|
|
596
|
+
const memspaceLetter = memspaceMatch[1]!;
|
|
597
|
+
if (memspaceLetter !== MAIN_CPU_MEMSPACE) {
|
|
598
|
+
return {
|
|
599
|
+
ok: false,
|
|
600
|
+
refusal: makeRefusal(
|
|
601
|
+
"unrecognised-memspace",
|
|
602
|
+
`io: line ${rowLineNumber} carries memspace marker "${memspaceLetter}", not the main CPU's -- run ` +
|
|
603
|
+
`vice_device_console to reset the default device before retrying: ${JSON.stringify(line)}`,
|
|
604
|
+
line,
|
|
605
|
+
rowLineNumber,
|
|
606
|
+
),
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
const match = DUMP_ROW_RE.exec(line);
|
|
610
|
+
if (!match) {
|
|
611
|
+
return {
|
|
612
|
+
ok: false,
|
|
613
|
+
refusal: makeRefusal(
|
|
614
|
+
"malformed-dump",
|
|
615
|
+
`io: line ${rowLineNumber} does not match the expected 16-byte hex dump layout: ${JSON.stringify(line)}`,
|
|
616
|
+
line,
|
|
617
|
+
rowLineNumber,
|
|
618
|
+
),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
if (baseAddress === undefined) baseAddress = parseInt(match[2]!, 16);
|
|
622
|
+
const groups = [match[3]!, match[4]!, match[5]!, match[6]!];
|
|
623
|
+
const rowBytes = groups.flatMap((g) => g.split(" ").map((b) => parseInt(b, 16)));
|
|
624
|
+
byteRows.push(rowBytes);
|
|
625
|
+
i++;
|
|
626
|
+
}
|
|
627
|
+
if (byteRows.length === 0 || baseAddress === undefined) {
|
|
628
|
+
return {
|
|
629
|
+
ok: false,
|
|
630
|
+
refusal: makeRefusal(
|
|
631
|
+
"malformed-dump",
|
|
632
|
+
`io: chip section "${chip}" (line ${lineNumber}) has no register dump rows following its header`,
|
|
633
|
+
headerLine,
|
|
634
|
+
lineNumber,
|
|
635
|
+
),
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// --- chip gate (plan 42-11, WR-02) ------------------------------------
|
|
640
|
+
// The sprite table and decoded-prose sections below are VIC-II-only --
|
|
641
|
+
// a CIA1, CIA2 or SID reply (every one of them inside vice_io_registers'
|
|
642
|
+
// own advertised 0-65535 address range) is refused HERE, by the chip's
|
|
643
|
+
// own name, rather than being reported below as a VIC-II section
|
|
644
|
+
// missing its Sprites: header. This sits after the dump-row validation
|
|
645
|
+
// above (so a drive-contaminated memspace still refuses
|
|
646
|
+
// unrecognised-memspace first) and before every check below that would
|
|
647
|
+
// otherwise misread a legitimately different chip as a layout defect.
|
|
648
|
+
if (chip !== VIC_II_CHIP_NAME) {
|
|
649
|
+
const byteCount = byteRows.reduce((sum, row) => sum + row.length, 0);
|
|
650
|
+
const baseAddressHex = `$${baseAddress.toString(16).padStart(4, "0")}`;
|
|
651
|
+
return {
|
|
652
|
+
ok: false,
|
|
653
|
+
refusal: makeRefusal(
|
|
654
|
+
"unsupported-chip",
|
|
655
|
+
`io: chip "${chip}" is not supported -- its register dump read cleanly (${byteCount} bytes at ` +
|
|
656
|
+
`${baseAddressHex}), but only "${VIC_II_CHIP_NAME}" sections carry the decoded display state and ` +
|
|
657
|
+
`sprite table this parser models -- dial an address covered by the ${VIC_II_CHIP_NAME} chip for a ` +
|
|
658
|
+
"decoded answer",
|
|
659
|
+
headerLine,
|
|
660
|
+
lineNumber,
|
|
661
|
+
),
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// --- blank separator -------------------------------------------------
|
|
666
|
+
if (lines[i] !== "") {
|
|
667
|
+
return {
|
|
668
|
+
ok: false,
|
|
669
|
+
refusal: makeRefusal(
|
|
670
|
+
"malformed-dump",
|
|
671
|
+
`io: expected a blank line after chip "${chip}"'s register dump, found ${JSON.stringify(lines[i] ?? "<end of input>")}`,
|
|
672
|
+
lines[i] ?? "",
|
|
673
|
+
i + 1,
|
|
674
|
+
),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
i++;
|
|
678
|
+
|
|
679
|
+
// --- decoded prose -----------------------------------------------------
|
|
680
|
+
const proseStartLineNumber = i + 1;
|
|
681
|
+
const proseLines: string[] = [];
|
|
682
|
+
while (i < lines.length && lines[i] !== "") {
|
|
683
|
+
proseLines.push(lines[i]!);
|
|
684
|
+
i++;
|
|
685
|
+
}
|
|
686
|
+
const proseResult = decodeProseLines(proseLines, proseStartLineNumber, unrecognisedLines);
|
|
687
|
+
if (!proseResult.ok) return proseResult;
|
|
688
|
+
if (i < lines.length && lines[i] === "") i++; // consume the blank separator before the sprite table
|
|
689
|
+
|
|
690
|
+
// --- sprite table -----------------------------------------------------
|
|
691
|
+
const spriteHeaderLine = lines[i] ?? "";
|
|
692
|
+
const spriteHeaderLineNumber = i + 1;
|
|
693
|
+
if (!spriteHeaderLine.startsWith(SPRITE_HEADER_PREFIX)) {
|
|
694
|
+
return {
|
|
695
|
+
ok: false,
|
|
696
|
+
refusal: makeRefusal(
|
|
697
|
+
"sprite-column-count",
|
|
698
|
+
`io: chip "${chip}" is missing its expected "Sprites:" header line at line ${spriteHeaderLineNumber}: ${JSON.stringify(spriteHeaderLine)}`,
|
|
699
|
+
spriteHeaderLine,
|
|
700
|
+
spriteHeaderLineNumber,
|
|
701
|
+
),
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
const boundariesResult = deriveSpriteBoundaries(spriteHeaderLine);
|
|
705
|
+
if (!boundariesResult.ok) {
|
|
706
|
+
return {
|
|
707
|
+
ok: false,
|
|
708
|
+
refusal: makeRefusal(
|
|
709
|
+
"sprite-column-count",
|
|
710
|
+
`io: the "Sprites:" header at line ${spriteHeaderLineNumber} declares ${boundariesResult.count} columns, not 8: ${JSON.stringify(spriteHeaderLine)}`,
|
|
711
|
+
spriteHeaderLine,
|
|
712
|
+
spriteHeaderLineNumber,
|
|
713
|
+
),
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
i++;
|
|
717
|
+
|
|
718
|
+
const spriteRows: SpriteTableRow[] = [];
|
|
719
|
+
while (i < lines.length && lines[i] !== "") {
|
|
720
|
+
const rowLine = lines[i]!;
|
|
721
|
+
const rowLineNumber = i + 1;
|
|
722
|
+
const rowResult = parseSpriteRow(rowLine, boundariesResult.boundaries);
|
|
723
|
+
if (!rowResult.ok) {
|
|
724
|
+
return {
|
|
725
|
+
ok: false,
|
|
726
|
+
refusal: makeRefusal(
|
|
727
|
+
"unrecognised-sprite-row",
|
|
728
|
+
`io: line ${rowLineNumber} has a sprite-table row label outside the closed ten-label set: ${JSON.stringify(rowResult.label)}`,
|
|
729
|
+
rowLine,
|
|
730
|
+
rowLineNumber,
|
|
731
|
+
),
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
spriteRows.push(rowResult.row);
|
|
735
|
+
i++;
|
|
736
|
+
}
|
|
737
|
+
if (i < lines.length && lines[i] === "") i++; // consume the blank separator before the next section, if any
|
|
738
|
+
|
|
739
|
+
sections.push({
|
|
740
|
+
chip,
|
|
741
|
+
dump: { baseAddress, bytes: byteRows.flat() },
|
|
742
|
+
decoded: proseResult.state,
|
|
743
|
+
sprites: { columns: 8, rows: spriteRows },
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return { ok: true, value: { sections, unrecognisedLines } };
|
|
748
|
+
}
|