@8bitscript/mega65 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 8BitScript contributors
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/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@8bitscript/mega65",
3
+ "version": "0.1.0",
4
+ "description": "MEGA65 target support for 8BitScript: the hardware underneath the portable APIs.",
5
+ "license": "MIT",
6
+ "8bitscript": {
7
+ "entry": "./src/index.8bs",
8
+ "exports": {
9
+ "./screen": "./src/screen.8bs",
10
+ "./text": "./src/text.8bs",
11
+ "./input": "./src/input.8bs",
12
+ "./pointer": "./src/pointer.8bs"
13
+ },
14
+ "hardware": {
15
+ "facts": {
16
+ "video.columns": 80,
17
+ "video.rows": 25,
18
+ "video.cellWidth": 8,
19
+ "video.cellHeight": 8,
20
+ "video.palette": 256,
21
+ "video.cellColors": 2,
22
+ "video.colorPerCell": true,
23
+ "video.glyphs": 256,
24
+ "video.blockWidth": 2,
25
+ "video.blockHeight": 2,
26
+ "video.bitmap": true,
27
+ "video.layers": 1,
28
+ "video.scroll": true,
29
+ "video.sprites": 8,
30
+ "video.spritesPerLine": 8,
31
+ "video.spriteWidth": 64,
32
+ "video.spriteHeight": 255,
33
+ "video.spriteColors": 15,
34
+ "video.frameRate": 60,
35
+ "audio.voices": 16,
36
+ "audio.noise": true,
37
+ "audio.envelope": true,
38
+ "audio.filter": true,
39
+ "audio.pcm": true,
40
+ "audio.volume": true,
41
+ "audio.entropy": true,
42
+ "input.keyboard": true,
43
+ "input.joysticks": 2,
44
+ "input.pads": 0,
45
+ "input.mouse": false,
46
+ "input.paddles": false,
47
+ "storage.save": true,
48
+ "memory.ram": 45055,
49
+ "memory.banked": true,
50
+ "memory.bankedKib": 320,
51
+ "storage.kib": 783
52
+ },
53
+ "options": {},
54
+ "presets": {}
55
+ }
56
+ },
57
+ "files": [
58
+ "src"
59
+ ],
60
+ "publishConfig": {
61
+ "access": "public"
62
+ }
63
+ }
package/src/index.8bs ADDED
@@ -0,0 +1,79 @@
1
+ // @8bitscript/mega65 — MEGA65 target support: the hardware underneath.
2
+ //
3
+ // The module entry point named by the "8bitscript".entry field in this
4
+ // package's package.json, resolved and linked for:
5
+ //
6
+ // import { borderColor, backgroundColor, memoryPointer } from "@8bitscript/mega65";
7
+ //
8
+ // The surface is the hardware, one register at a time. What a program
9
+ // usually wants sits one layer up, in this package's own implementations of
10
+ // the portable capability packages — `./src/screen.8bs` behind
11
+ // @8bitscript/screen and `./src/text.8bs` behind @8bitscript/text, named by
12
+ // the "8bitscript".exports map in package.json — which import these
13
+ // registers and are built on nothing else.
14
+ //
15
+ // A program built with mos-mega65-clang loads at $2001 and runs in MEGA65
16
+ // mode: the VIC-IV I/O personality is live, the CPU is at 40.5 MHz, the
17
+ // ROM's screen is 80 columns wide at $0800, and the SDK's start-up leaves
18
+ // interrupts off (`sei`, `$01 = $3E`, `$D030 = $44`) until exit — all
19
+ // measured under xmega65, see AGENTS.md. The registers below are the
20
+ // VIC-II-compatible ones the VIC-IV still answers to at the C64's
21
+ // addresses, which is enough for a border, a background, and a character
22
+ // set; the MEGA65's own registers (SCRNPTR, the palettes, the DMAgic, the
23
+ // four SIDs) are not exported yet, and AGENTS.md is where their rules are.
24
+
25
+ // $D020, the VIC-II(-compatible) border colour register.
26
+ @address(0xD020)
27
+ export let borderColor: volatile<u8>;
28
+
29
+ // $D021, the VIC-II(-compatible) background colour register (background #0).
30
+ @address(0xD021)
31
+ export let backgroundColor: volatile<u8>;
32
+
33
+ // $D018, the VIC-II-compatible memory pointer, laid out as the C64's: bits
34
+ // 4-7 the screen base in 1K units, bits 1-3 the character set base in 2K
35
+ // units. mega65.h names the default screen character matrix directly:
36
+ // DEFAULT_SCREEN at $0800 — not the C64's $0400 — so the screen half is
37
+ // $2x, and $24 selects the upper-case/graphics set at $1000 (the same 2K
38
+ // the C64 has there) where $26 would be the lower-case set at $1800. The
39
+ // MEGA65 re-derives its own wider VIC-IV pointers from a legacy $D018
40
+ // write, so this is enough; verified on screen under xmega65, not inferred.
41
+ @address(0xD018)
42
+ export let memoryPointer: volatile<u8>;
43
+
44
+ // $D030, the VIC-III control register A. The SDK's start-up sets it to $44
45
+ // (PAL-RAM on, CROM9 on, no C65 ROM mapped, CRAM2K clear). Bit 0, CRAM2K,
46
+ // maps the second kilobyte of colour RAM at $DC00-$DFFF in place of the
47
+ // two CIAs — which the 80-column screen's cells 1024-1999 need, and which
48
+ // text.8bs sets before every run of text (the frame runtime polls $D012,
49
+ // not a CIA, so nothing here misses them). Writing anything but bit 0
50
+ // re-maps ROM over the program: keep the other bits as the SDK left them.
51
+ @address(0xD030)
52
+ export let vicIIIControlA: volatile<u8>;
53
+
54
+ export namespace VicIIIControl {
55
+ const SDK_STARTUP: utinyint = 0x44; // what .init.010 stores
56
+ const COLOR_RAM_2K: utinyint = 0x45; // the same, plus CRAM2K
57
+ }
58
+
59
+ // ---- CIA1 ($DC00): the keyboard matrix and the two control ports ------------
60
+ //
61
+ // A MEGA65 is a C64 here: the same chip at the same address, the same
62
+ // eight columns by eight rows, the same key in the same place, and the
63
+ // same two control ports on the same bits. So @8bitscript/c64/keys's codes
64
+ // are the right codes on this machine, and `@8bitscript/mega65/input`
65
+ // reads them exactly as the C64's layer does.
66
+ //
67
+ // What this machine adds and that layer does not yet read: the MEGA65's
68
+ // own extended keyboard — the full-travel keys a C64 never had, including
69
+ // **ALT**, CAPS LOCK and the four dedicated cursor keys — which the
70
+ // hardware presents through its own registers rather than through this
71
+ // matrix. That is the next thing to wire up here.
72
+ @address(0xDC00)
73
+ export let cia1PortA: volatile<u8>;
74
+ @address(0xDC01)
75
+ export let cia1PortB: volatile<u8>;
76
+ @address(0xDC02)
77
+ export let cia1DirectionA: volatile<u8>;
78
+ @address(0xDC03)
79
+ export let cia1DirectionB: volatile<u8>;
package/src/input.8bs ADDED
@@ -0,0 +1,168 @@
1
+ // @8bitscript/mega65/input — the MEGA65 behind @8bitscript/input.
2
+ //
3
+ // Named by "8bitscript".exports["./input"] in this package's package.json,
4
+ // and by "8bitscript".entry.mega65 in @8bitscript/input's, so a portable
5
+ // program writes
6
+ //
7
+ // import { input } from "@8bitscript/input";
8
+ //
9
+ // and gets this file on a MEGA65.
10
+ //
11
+ // ---- a C64's matrix on a machine that is not a C64 -----------------------
12
+ //
13
+ // For input, this machine is a C64: CIA1 at $DC00, eight columns by eight
14
+ // rows, the same key in the same place, the two control ports on the same
15
+ // bits. So the codes below are @8bitscript/c64/keys's codes, and the scan
16
+ // is the C64's — write a byte with one 0 bit to port A to select a column,
17
+ // read port B for that column's eight keys.
18
+ //
19
+ // - **left / right / up / down** — the C64-shaped cursor keys, with
20
+ // SHIFT reversing each, or'd with a joystick in port 2.
21
+ // - **confirm** — RETURN, or fire. **cancel** — RUN/STOP.
22
+ //
23
+ // **What this leaves on the table is most of the keyboard.** A MEGA65 has
24
+ // full-travel keys a C64 never had — four dedicated cursor keys, **ALT**,
25
+ // CAPS LOCK, NO SCROLL, HELP — presented through the machine's own
26
+ // registers rather than through this matrix. Reading them is the next
27
+ // thing to do here, and it would make this the machine where a menu takes
28
+ // ALT+letter. Until then a MEGA65 navigates exactly like a C64, which is
29
+ // correct but is less than the hardware offers, and that is worth knowing
30
+ // rather than discovering.
31
+ //
32
+ // **No mouse.** This package's hardware catalog has no options at all yet,
33
+ // so `input.mouse` is false and there is nothing to detect. A 1351 on a
34
+ // MEGA65 would be the C64's driver against these same registers.
35
+ import { cia1PortA, cia1PortB, cia1DirectionA, cia1DirectionB } from "./index.8bs";
36
+
37
+ // The C64 matrix's codes, for the keys this file maps.
38
+ namespace Key {
39
+ const RETURN: utinyint = 1;
40
+ const CURSOR_RIGHT: utinyint = 2; // shifted: cursor left
41
+ const CURSOR_DOWN: utinyint = 7; // shifted: cursor up
42
+ const SHIFT_LEFT: utinyint = 15;
43
+ const SHIFT_RIGHT: utinyint = 52;
44
+ const STOP: utinyint = 63; // RUN/STOP
45
+ }
46
+
47
+ const COLUMN_SELECT: array<utinyint, 8> = [254, 253, 251, 247, 239, 223, 191, 127];
48
+ const ROW_BIT: array<utinyint, 8> = [1, 2, 4, 8, 16, 32, 64, 128];
49
+
50
+ let matrix: array<utinyint, 8>;
51
+
52
+ namespace Edge {
53
+ const LEFT: utinyint = 1;
54
+ const RIGHT: utinyint = 2;
55
+ const UP: utinyint = 4;
56
+ const DOWN: utinyint = 8;
57
+ const CONFIRM: utinyint = 16;
58
+ const CANCEL: utinyint = 32;
59
+ }
60
+
61
+ let held: utinyint = 0;
62
+ let before: utinyint = 0;
63
+ let began: utinyint = 0;
64
+
65
+ // Is this key down in the last scan?
66
+ function keyDown(key: utinyint): bool {
67
+ return (matrix[key >> 3] & ROW_BIT[key & 7]) != 0;
68
+ }
69
+
70
+ export namespace input {
71
+
72
+ // Nothing to set up: no pointer to give a ceiling to.
73
+ function begin(): void {
74
+ }
75
+
76
+ function poll(): void {
77
+ cia1DirectionA = 0xFF;
78
+ cia1DirectionB = 0x00;
79
+ for (let column: utinyint = 0; column < 8; column++) {
80
+ cia1PortA = COLUMN_SELECT[column];
81
+ matrix[column] = cia1PortB ^ 0xFF;
82
+ }
83
+ cia1PortA = 0xFF;
84
+ // Port A back at $FF is what makes port B read as joystick 1 and
85
+ // port A as joystick 2, exactly as on a C64.
86
+ let stick: utinyint = (cia1PortA ^ 0xFF) & 0x1F;
87
+
88
+ before = held;
89
+ held = 0;
90
+
91
+ let shifted: bool = keyDown(Key.SHIFT_LEFT) || keyDown(Key.SHIFT_RIGHT);
92
+ if (keyDown(Key.CURSOR_RIGHT)) {
93
+ if (shifted) {
94
+ held = held | Edge.LEFT;
95
+ } else {
96
+ held = held | Edge.RIGHT;
97
+ }
98
+ }
99
+ if (keyDown(Key.CURSOR_DOWN)) {
100
+ if (shifted) {
101
+ held = held | Edge.UP;
102
+ } else {
103
+ held = held | Edge.DOWN;
104
+ }
105
+ }
106
+ if (keyDown(Key.RETURN)) {
107
+ held = held | Edge.CONFIRM;
108
+ }
109
+ if (keyDown(Key.STOP)) {
110
+ held = held | Edge.CANCEL;
111
+ }
112
+
113
+ // The stick in port 2: up 1, down 2, left 4, right 8, fire 16.
114
+ if ((stick & 4) != 0) {
115
+ held = held | Edge.LEFT;
116
+ }
117
+ if ((stick & 8) != 0) {
118
+ held = held | Edge.RIGHT;
119
+ }
120
+ if ((stick & 1) != 0) {
121
+ held = held | Edge.UP;
122
+ }
123
+ if ((stick & 2) != 0) {
124
+ held = held | Edge.DOWN;
125
+ }
126
+ if ((stick & 16) != 0) {
127
+ held = held | Edge.CONFIRM;
128
+ }
129
+
130
+ began = held & (before ^ 0xFF);
131
+ }
132
+
133
+ function left(): bool {
134
+ return (began & Edge.LEFT) != 0;
135
+ }
136
+
137
+ function right(): bool {
138
+ return (began & Edge.RIGHT) != 0;
139
+ }
140
+
141
+ function up(): bool {
142
+ return (began & Edge.UP) != 0;
143
+ }
144
+
145
+ function down(): bool {
146
+ return (began & Edge.DOWN) != 0;
147
+ }
148
+
149
+ function confirm(): bool {
150
+ return (began & Edge.CONFIRM) != 0;
151
+ }
152
+
153
+ function cancel(): bool {
154
+ return (began & Edge.CANCEL) != 0;
155
+ }
156
+
157
+ function pointer(): bool {
158
+ return false;
159
+ }
160
+
161
+ function pointerCell(): usmallint {
162
+ return 0;
163
+ }
164
+
165
+ function pointerButton(): bool {
166
+ return false;
167
+ }
168
+ }
@@ -0,0 +1,40 @@
1
+ // @8bitscript/mega65/pointer — the MEGA65 behind @8bitscript/pointer.
2
+ //
3
+ // Every call here does nothing and `DRAWS` is false, and as with the C128
4
+ // this is a missing driver rather than a missing capability: the VIC-IV is
5
+ // a superset of the VIC-II, so the eight sprites are there (and more
6
+ // besides), and the machine has control ports a 1351 fits.
7
+ //
8
+ // Two things are missing, in this order. **The input half:**
9
+ // @8bitscript/mega65/input reads the keyboard and the C64's joystick
10
+ // arrangement and reports no pointer, because packages/mega65's catalog
11
+ // has no control-port options at all yet — no value to set
12
+ // `#fact(input.mouse)` true, so nothing to fit and no fact to compile a
13
+ // pointer in from. **Then the drawing half:** this package has no sprites
14
+ // layer, so there is nothing for a file like @8bitscript/c64/pointer to
15
+ // sit on.
16
+ //
17
+ // Worth knowing before either is written: this machine starts up with
18
+ // interrupts off and at 40.5 MHz (see packages/mega65/AGENTS.md), so it
19
+ // has cycles to spare for a pointer — the constraint here is entirely that
20
+ // nobody has written the two layers, not the hardware.
21
+ //
22
+ // It costs the MEGA65 nothing meanwhile: `DRAWS` is a const and every
23
+ // function is empty, so a program's `if (pointer.DRAWS)` folds away and
24
+ // LLVM deletes the calls.
25
+ export namespace pointer {
26
+
27
+ const DRAWS: bool = false;
28
+
29
+ function begin(): void {
30
+ }
31
+
32
+ function setColor(color: utinyint): void {
33
+ }
34
+
35
+ function update(): void {
36
+ }
37
+
38
+ function hide(): void {
39
+ }
40
+ }
package/src/screen.8bs ADDED
@@ -0,0 +1,106 @@
1
+ // @8bitscript/mega65/screen — the MEGA65's implementation of @8bitscript/screen.
2
+ //
3
+ // Named by "8bitscript".exports["./screen"] in this package's package.json,
4
+ // and what
5
+ //
6
+ // import { screen, BorderColor, BackgroundColor } from "@8bitscript/screen";
7
+ //
8
+ // resolves to when the build is for the MEGA65: @8bitscript/screen's entry
9
+ // is keyed by machine and delegates here. Built on the registers
10
+ // @8bitscript/mega65 itself exports, one layer up from them. Every machine's
11
+ // screen.8bs exports this same surface, which is what lets a program import
12
+ // it from @8bitscript/screen and never name the hardware.
13
+ import { borderColor, backgroundColor } from "./index.8bs";
14
+
15
+ // ---- the screen: border and background --------------------------------------
16
+ //
17
+ // Border and background have their own registers at the C64's addresses;
18
+ // the VIC-IV's are 8 bits wide (256 palette entries), and the shared eight
19
+ // names stay on 0-15 so a program's colour means the same thing everywhere.
20
+ // The screen a build runs on is 80x25 at $0800 (see text.8bs), so a blank
21
+ // clears 2000 cells.
22
+ export namespace screen {
23
+ function setColors(border: u8, background: u8): void {
24
+ borderColor = border & 15;
25
+ backgroundColor = background & 15;
26
+ }
27
+
28
+ // `blank(border, background)`: every cell blank and both colours set —
29
+ // black when left off, or `BorderColor.KEEP` / `BackgroundColor.KEEP`
30
+ // to leave one as it is. `setBorder` and `setBackground` are one
31
+ // colour each, `setColors` the pair; `text.setColor` owns what the
32
+ // next print looks like.
33
+ function blank(border: utinyint = BorderColor.BLACK, background: utinyint = BackgroundColor.BLACK): void {
34
+ if (border != BorderColor.KEEP) {
35
+ screen.setBorder(border);
36
+ }
37
+ if (background != BackgroundColor.KEEP) {
38
+ screen.setBackground(background);
39
+ }
40
+ for (let cell: usmallint = 0; cell < 2000; cell++) {
41
+ memory.write(0x0800 + cell, 32); // 32: the space screen code
42
+ }
43
+ }
44
+
45
+ function setBackground(background: u8): void {
46
+ backgroundColor = background & 15;
47
+ }
48
+
49
+ function setBorder(border: u8): void {
50
+ borderColor = border & 15;
51
+ }
52
+ }
53
+
54
+ // ---- colour names ----------------------------------------------------------
55
+ //
56
+ // The same eight names every machine's screen.8bs exports — Black, White,
57
+ // Red, Cyan, Purple, Green, Blue, Yellow — so a program can write
58
+ // `screen.setColors(BorderColor.BLUE, BackgroundColor.BLACK)` through
59
+ // @8bitscript/screen and get blue on every target; the values are this
60
+ // machine's own. In the VIC-II-compatible view both registers take the
61
+ // C64's sixteen colours in the C64's numbering, so both namespaces carry
62
+ // all sixteen.
63
+
64
+ export namespace BorderColor {
65
+ const BLACK: utinyint = 0;
66
+ const WHITE: utinyint = 1;
67
+ const RED: utinyint = 2;
68
+ const CYAN: utinyint = 3;
69
+ const PURPLE: utinyint = 4;
70
+ const GREEN: utinyint = 5;
71
+ const BLUE: utinyint = 6;
72
+ const YELLOW: utinyint = 7;
73
+ const ORANGE: utinyint = 8;
74
+ const BROWN: utinyint = 9;
75
+ const LIGHT_RED: utinyint = 10;
76
+ const DARK_GREY: utinyint = 11;
77
+ const GREY: utinyint = 12;
78
+ const LIGHT_GREEN: utinyint = 13;
79
+ const LIGHT_BLUE: utinyint = 14;
80
+ const LIGHT_GREY: utinyint = 15;
81
+ // Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
82
+ // a value no register here takes.
83
+ const KEEP: utinyint = 255;
84
+ }
85
+
86
+ export namespace BackgroundColor {
87
+ const BLACK: utinyint = 0;
88
+ const WHITE: utinyint = 1;
89
+ const RED: utinyint = 2;
90
+ const CYAN: utinyint = 3;
91
+ const PURPLE: utinyint = 4;
92
+ const GREEN: utinyint = 5;
93
+ const BLUE: utinyint = 6;
94
+ const YELLOW: utinyint = 7;
95
+ const ORANGE: utinyint = 8;
96
+ const BROWN: utinyint = 9;
97
+ const LIGHT_RED: utinyint = 10;
98
+ const DARK_GREY: utinyint = 11;
99
+ const GREY: utinyint = 12;
100
+ const LIGHT_GREEN: utinyint = 13;
101
+ const LIGHT_BLUE: utinyint = 14;
102
+ const LIGHT_GREY: utinyint = 15;
103
+ // Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
104
+ // a value no register here takes.
105
+ const KEEP: utinyint = 255;
106
+ }
package/src/text.8bs ADDED
@@ -0,0 +1,175 @@
1
+ // @8bitscript/mega65/text — the MEGA65's implementation of @8bitscript/text.
2
+ //
3
+ // Named by "8bitscript".exports["./text"] in this package's package.json,
4
+ // and what
5
+ //
6
+ // import { text } from "@8bitscript/text";
7
+ //
8
+ // resolves to when the build is for the MEGA65. Built on the registers
9
+ // @8bitscript/mega65 exports. Every machine's text.8bs exports this same
10
+ // namespace — the same names, the same shapes, ASCII codes, and a cell 0
11
+ // at the top-left corner inside the border — so a program that imports it
12
+ // from @8bitscript/text draws the same thing on every target.
13
+ import { memoryPointer, vicIIIControlA, VicIIIControl } from "./index.8bs";
14
+
15
+ // The screen is addressed by flat cell index — a position is
16
+ // `y * text.COLUMNS + x` — and these are the plainest things a text API
17
+ // can be built from: poke one cell's character code, poke one cell's colour, and how many
18
+ // cells the whole screen has. Whether a caller uses these to clear the
19
+ // screen, lay out a label, or nothing at all is entirely up to the caller —
20
+ // this package draws nothing on its own. mega65.h names the default screen
21
+ // character matrix directly: DEFAULT_SCREEN at $0800 — not the C64's $0400
22
+ // — and in the MEGA65 mode a build runs in, that screen is 80x25, 2000
23
+ // cells (LINESTEP 80, CHRCOUNT 80, H640 set: measured under xmega65, see
24
+ // AGENTS.md; an earlier version of this file drew for 40 columns and
25
+ // wrapped every second row onto the right half of the one above). Colour
26
+ // RAM is at $D800 for cells 0-1023, and for cells 1024-1999 at
27
+ // $DC00-$DFCF only while $D030's CRAM2K bit is set — otherwise those
28
+ // addresses are the two CIAs — so prepare() sets that bit before a run.
29
+ //
30
+ // ---- character codes: ASCII in, screen codes out ----------------------------
31
+ //
32
+ // `text.putChar` takes ASCII on every machine — space (32), '0'-'9'
33
+ // (48-57), 'A'-'Z' (65-90) and the punctuation `! , - . : ?` — upper case
34
+ // only: that is the portable set, the characters every target's character
35
+ // set has (the NES ships its own font, and that is what it has). The
36
+ // hardware wants screen codes: in every Commodore character ROM, codes
37
+ // 32-63 are those same ASCII values (space, digits, that punctuation), and
38
+ // 'A'-'Z' are 1-26, so asciiToScreenCode() moves 64-95 down by 64 and
39
+ // leaves 32-63 alone. Whether 1-26 then LOOK like 'A'-'Z' or 'a'-'z' is
40
+ // the character set's choice — the ROM's boot state, whatever ran before,
41
+ // a user's SHIFT+C= — so this package owns the choice rather than
42
+ // inheriting one (LLVM-MOS's libc used to flip the machine to lower-case
43
+ // before main() with a hidden CHR$(14); packages/backend-6502's
44
+ // commodoreCharsetGuard() keeps that out of the link). putChar() selects
45
+ // the upper-case set, every
46
+ // call, through the memory pointer (see @8bitscript/mega65's
47
+ // `memoryPointer` for the $24 it takes here): one register write, and
48
+ // "TICK" reads as TICK here the way it does on the NES, the Atari, and the
49
+ // X16, none of which have a lower-case set to fall into.
50
+ namespace CharacterSet {
51
+ const UPPERCASE_SCREEN_AT_0800: utinyint = 0x24;
52
+ }
53
+
54
+ function asciiToScreenCode(code: utinyint): utinyint {
55
+ if (code >= 64 && code < 96) {
56
+ return code - 64;
57
+ }
58
+ return code;
59
+ }
60
+
61
+ // ---- print: strings and number fields ------------------------------------
62
+ //
63
+ // `text.print(cell, s)` writes a string's characters into consecutive
64
+ // cells from `cell`, and `text.printNumber(cell, value, width)` writes
65
+ // `value` as exactly `width` decimal digits, zero-padded and right-aligned,
66
+ // so a field on a HUD never shifts columns. Both draw in the current colour —
67
+ // white until `text.setColor(TextColor.CYAN)` changes it, and that one
68
+ // call then colours everything printed after it, on the machines that have
69
+ // per-cell colour. They are also the two
70
+ // functions the compiler's template layout targets: `text.print(0,
71
+ // \`TICK ${ticks:1}\`)` is laid out at compile time into these same calls
72
+ // (see packages/compiler/src/ir).
73
+ //
74
+ // Under them: `place()` puts one character at one cell in the current
75
+ // colour, and is all a run of text costs per character, with `prepare()` selecting the
76
+ // upper-case set once for the run.
77
+ // `text.putChar` and `text.putColor` stay the one-cell pokes a caller can
78
+ // build anything from.
79
+ let currentColor: utinyint = 1; // white, until text.setColor() says otherwise
80
+ let currentReverse: bool = false; // until text.setReverse() says otherwise
81
+
82
+ // Reverse video is bit 7 of the screen code, as on the C64. Colour RAM's
83
+ // upper nibble is also an attribute field (blink, reverse) — keep it
84
+ // clear, and invert by the ROM copy instead.
85
+ function toScreen(code: utinyint): utinyint {
86
+ let screen: utinyint = asciiToScreenCode(code);
87
+ if (currentReverse) {
88
+ screen = screen + 128;
89
+ }
90
+ return screen;
91
+ }
92
+
93
+ // What a run of text needs once, before its first character.
94
+ function prepare(): void {
95
+ memoryPointer = CharacterSet.UPPERCASE_SCREEN_AT_0800;
96
+ vicIIIControlA = VicIIIControl.COLOR_RAM_2K;
97
+ }
98
+
99
+ function place(cell: usmallint, code: utinyint): void {
100
+ memory.write(0x0800 + cell, toScreen(code));
101
+ memory.write(0xD800 + cell, currentColor & 15); // the upper nibble is attributes (blink, reverse) — keep it clear
102
+ }
103
+
104
+ // ---- digits -------------------------------------------------------------
105
+ //
106
+ // A number is written one place at a time, high to low, by subtracting the
107
+ // place value until it no longer fits: the 6502 has no divide instruction,
108
+ // and `value / 10` would link a 250-byte routine to do it. Places above the
109
+ // field are still taken off, so a field narrower than its number shows the
110
+ // low digits; places the number does not reach print as zeros.
111
+ const DIGIT_PLACES: array<usmallint, 5> = [10000, 1000, 100, 10, 1];
112
+
113
+ export namespace text {
114
+ const CELL_COUNT: usmallint = 2000; // 80 columns x 25 rows
115
+ const COLUMNS: utinyint = 80; // cells per row, so cell = y * text.COLUMNS + x
116
+
117
+ function putChar(cell: usmallint, code: utinyint): void {
118
+ prepare();
119
+ memory.write(0x0800 + cell, toScreen(code));
120
+ }
121
+
122
+ function putColor(cell: usmallint, color: utinyint): void {
123
+ vicIIIControlA = VicIIIControl.COLOR_RAM_2K; // cells past 1023 live behind CRAM2K
124
+ memory.write(0xD800 + cell, color & 15);
125
+ }
126
+
127
+ function setColor(color: utinyint): void {
128
+ currentColor = color;
129
+ }
130
+
131
+ function setReverse(on: bool): void {
132
+ currentReverse = on;
133
+ }
134
+
135
+ function print(cell: usmallint, s: string): void {
136
+ prepare();
137
+ for (let i: utinyint = 0; i < s.length; i++) {
138
+ place(cell + i, s[i]);
139
+ }
140
+ }
141
+
142
+ function printNumber(cell: usmallint, value: usmallint, width: utinyint): void {
143
+ prepare();
144
+ let k: utinyint = width;
145
+ if (k < DIGIT_PLACES.length) {
146
+ k = DIGIT_PLACES.length;
147
+ }
148
+ while (k > 0) {
149
+ let digit: utinyint = 48; // '0'
150
+ if (k <= DIGIT_PLACES.length) {
151
+ while (value >= DIGIT_PLACES[DIGIT_PLACES.length - k]) {
152
+ value = value - DIGIT_PLACES[DIGIT_PLACES.length - k];
153
+ digit++;
154
+ }
155
+ }
156
+ if (k <= width) {
157
+ place(cell + width - k, digit);
158
+ }
159
+ k--;
160
+ }
161
+ }
162
+ }
163
+
164
+ // The colours `text.setColor()` takes: the eight names every machine
165
+ // shares, with this machine's colour-RAM values.
166
+ export namespace TextColor {
167
+ const BLACK: utinyint = 0;
168
+ const WHITE: utinyint = 1;
169
+ const RED: utinyint = 2;
170
+ const CYAN: utinyint = 3;
171
+ const PURPLE: utinyint = 4;
172
+ const GREEN: utinyint = 5;
173
+ const BLUE: utinyint = 6;
174
+ const YELLOW: utinyint = 7;
175
+ }