@8bitscript/cx16 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 +21 -0
- package/package.json +153 -0
- package/src/banks.8bs +128 -0
- package/src/index.8bs +111 -0
- package/src/input.8bs +146 -0
- package/src/mouse.8bs +187 -0
- package/src/pointer.8bs +77 -0
- package/src/screen.8bs +230 -0
- package/src/text.8bs +173 -0
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,153 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@8bitscript/cx16",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Commander X16 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
|
+
"./banks": "./src/banks.8bs",
|
|
12
|
+
"./mouse": "./src/mouse.8bs",
|
|
13
|
+
"./input": "./src/input.8bs",
|
|
14
|
+
"./pointer": "./src/pointer.8bs"
|
|
15
|
+
},
|
|
16
|
+
"hardware": {
|
|
17
|
+
"facts": {
|
|
18
|
+
"video.columns": 76,
|
|
19
|
+
"video.rows": 56,
|
|
20
|
+
"video.cellWidth": 8,
|
|
21
|
+
"video.cellHeight": 8,
|
|
22
|
+
"video.palette": 256,
|
|
23
|
+
"video.cellColors": 2,
|
|
24
|
+
"video.colorPerCell": true,
|
|
25
|
+
"video.glyphs": 256,
|
|
26
|
+
"video.blockWidth": 2,
|
|
27
|
+
"video.blockHeight": 2,
|
|
28
|
+
"video.bitmap": true,
|
|
29
|
+
"video.layers": 2,
|
|
30
|
+
"video.scroll": true,
|
|
31
|
+
"video.sprites": 128,
|
|
32
|
+
"video.spritesPerLine": 46,
|
|
33
|
+
"video.spriteWidth": 64,
|
|
34
|
+
"video.spriteHeight": 64,
|
|
35
|
+
"video.spriteColors": 255,
|
|
36
|
+
"video.frameRate": 60,
|
|
37
|
+
"audio.voices": 25,
|
|
38
|
+
"audio.noise": true,
|
|
39
|
+
"audio.envelope": true,
|
|
40
|
+
"audio.filter": false,
|
|
41
|
+
"audio.pcm": true,
|
|
42
|
+
"audio.volume": true,
|
|
43
|
+
"audio.entropy": false,
|
|
44
|
+
"input.keyboard": true,
|
|
45
|
+
"input.joysticks": 0,
|
|
46
|
+
"input.pads": 2,
|
|
47
|
+
"input.mouse": true,
|
|
48
|
+
"input.paddles": false,
|
|
49
|
+
"storage.save": true,
|
|
50
|
+
"memory.ram": 38655,
|
|
51
|
+
"memory.banked": true,
|
|
52
|
+
"memory.bankedKib": 512,
|
|
53
|
+
"storage.kib": 32768
|
|
54
|
+
},
|
|
55
|
+
"run": {
|
|
56
|
+
"x16emu": [
|
|
57
|
+
"-capture"
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
"options": {
|
|
61
|
+
"ram": {
|
|
62
|
+
"label": "Banked RAM",
|
|
63
|
+
"default": "512",
|
|
64
|
+
"detect": "@8bitscript/cx16/banks",
|
|
65
|
+
"values": {
|
|
66
|
+
"64": {
|
|
67
|
+
"label": "64 KiB of banked RAM",
|
|
68
|
+
"run": {
|
|
69
|
+
"x16emu": [
|
|
70
|
+
"-ram",
|
|
71
|
+
"64"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"facts": {
|
|
75
|
+
"memory.bankedKib": 64
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"128": {
|
|
79
|
+
"label": "128 KiB of banked RAM",
|
|
80
|
+
"run": {
|
|
81
|
+
"x16emu": [
|
|
82
|
+
"-ram",
|
|
83
|
+
"128"
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
"facts": {
|
|
87
|
+
"memory.bankedKib": 128
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"256": {
|
|
91
|
+
"label": "256 KiB of banked RAM",
|
|
92
|
+
"run": {
|
|
93
|
+
"x16emu": [
|
|
94
|
+
"-ram",
|
|
95
|
+
"256"
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
"facts": {
|
|
99
|
+
"memory.bankedKib": 256
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"512": {
|
|
103
|
+
"label": "512 KiB of banked RAM",
|
|
104
|
+
"run": {
|
|
105
|
+
"x16emu": [
|
|
106
|
+
"-ram",
|
|
107
|
+
"512"
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
"facts": {
|
|
111
|
+
"memory.bankedKib": 512
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"1024": {
|
|
115
|
+
"label": "1024 KiB of banked RAM",
|
|
116
|
+
"run": {
|
|
117
|
+
"x16emu": [
|
|
118
|
+
"-ram",
|
|
119
|
+
"1024"
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
"facts": {
|
|
123
|
+
"memory.bankedKib": 1024
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"2048": {
|
|
127
|
+
"label": "2048 KiB of banked RAM",
|
|
128
|
+
"run": {
|
|
129
|
+
"x16emu": [
|
|
130
|
+
"-ram",
|
|
131
|
+
"2048"
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
"facts": {
|
|
135
|
+
"memory.bankedKib": 2048
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"presets": {}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"files": [
|
|
145
|
+
"src"
|
|
146
|
+
],
|
|
147
|
+
"publishConfig": {
|
|
148
|
+
"access": "public"
|
|
149
|
+
},
|
|
150
|
+
"scripts": {
|
|
151
|
+
"test": "node --test"
|
|
152
|
+
}
|
|
153
|
+
}
|
package/src/banks.8bs
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// @8bitscript/cx16/banks — how much banked RAM this X16 has.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./banks"] in this package's package.json:
|
|
4
|
+
//
|
|
5
|
+
// import { banks } from "@8bitscript/cx16/banks";
|
|
6
|
+
//
|
|
7
|
+
// let kib: usmallint = banks.kib(); // 64 .. 2048
|
|
8
|
+
//
|
|
9
|
+
// A run-time hardware probe, like @8bitscript/c64/reu. The X16's banked
|
|
10
|
+
// RAM is not a fixed amount: a board carries anywhere from 64 KiB to 2
|
|
11
|
+
// MiB of it, `x16emu -ram <KiB>` emulates each, and one binary runs on
|
|
12
|
+
// all of them. The build says which the *emulator* is fitted with (the
|
|
13
|
+
// catalog's `ram` option; `Memory.BANKED_KIB` from @8bitscript/system),
|
|
14
|
+
// and this says what the machine the program is running on really has. A
|
|
15
|
+
// program that never imports this file carries none of it.
|
|
16
|
+
//
|
|
17
|
+
// Banked RAM is 8 KiB pages seen through the window at `$A000`–`$BFFF`,
|
|
18
|
+
// the page selected by a write to `$0000` (the X16 puts its two bank
|
|
19
|
+
// registers in the zero page, which is why the SDK's own zero-page
|
|
20
|
+
// allocation starts at `$0022`). A machine has a power of two of them, 8
|
|
21
|
+
// pages (64 KiB) to 256 (2 MiB). So the count is found by asking about
|
|
22
|
+
// pages 8, 16, 32, 64 and 128 in turn: the first one the machine does not
|
|
23
|
+
// really have is the count.
|
|
24
|
+
//
|
|
25
|
+
// "Does not really have" has two shapes, and the probe tests for both,
|
|
26
|
+
// because which one a machine shows is the machine's business:
|
|
27
|
+
//
|
|
28
|
+
// 1. **It is a page it does have, under another number.** A board that
|
|
29
|
+
// keeps fewer than 8 bits of the page register wraps, so page N is
|
|
30
|
+
// page (N mod count). Every page tested here is one above a power of
|
|
31
|
+
// two, so where it wraps it lands on page 1 — the page the KERNAL
|
|
32
|
+
// leaves to us — and the test is whether page N+1 shows a marker just
|
|
33
|
+
// written to page 1. It is confirmed with a second, different marker,
|
|
34
|
+
// so a page that merely happens to hold those two bytes is not
|
|
35
|
+
// mistaken for a mirror.
|
|
36
|
+
// 2. **It is nothing at all.** Then the window floats: writes do not
|
|
37
|
+
// stick and reads return whatever was last on the bus. Verified under
|
|
38
|
+
// x16emu, which is this shape — with `-ram 64`, a byte written to
|
|
39
|
+
// page 200 read back as `$A0`, the high byte of the window's own
|
|
40
|
+
// address, and page 9 read `$3E`. So the second test is the plain
|
|
41
|
+
// one: write two different bytes and read both back.
|
|
42
|
+
//
|
|
43
|
+
// Only pages 1 and N+1 are ever written, and page N+1 only after it has
|
|
44
|
+
// been shown not to be a mirror — page 0 is the KERNAL's own workspace
|
|
45
|
+
// and is never touched. The two bytes at `$A000`–`$A001` of page 1 are
|
|
46
|
+
// scribbled on, so call this at start-up, before storing anything in
|
|
47
|
+
// banked RAM, and keep the answer. Page 1 is left selected, which is
|
|
48
|
+
// where the KERNAL leaves it.
|
|
49
|
+
//
|
|
50
|
+
// (The KERNAL can answer this too — `MEMTOP` with carry set returns the
|
|
51
|
+
// page count — but calling it means a JSR with register conventions the
|
|
52
|
+
// language has no spelling for. See ../AGENTS.md.)
|
|
53
|
+
|
|
54
|
+
namespace Bank {
|
|
55
|
+
const SELECT: usmallint = 0x0000; // the RAM page register
|
|
56
|
+
const WINDOW: usmallint = 0xA000; // first byte of the 8 KiB window
|
|
57
|
+
const WINDOW1: usmallint = 0xA001; // and the second
|
|
58
|
+
const OURS: utinyint = 1; // the page the KERNAL leaves to us
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export namespace banks {
|
|
62
|
+
// Put `value` and its complement in our own page's first two bytes.
|
|
63
|
+
function mark(value: utinyint): void {
|
|
64
|
+
memory.write(Bank.SELECT, Bank.OURS);
|
|
65
|
+
memory.write(Bank.WINDOW, value);
|
|
66
|
+
memory.write(Bank.WINDOW1, value ^ 0xFF);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Does `page` show what mark(value) wrote — is it our own page under
|
|
70
|
+
// another number?
|
|
71
|
+
function shows(page: utinyint, value: utinyint): bool {
|
|
72
|
+
memory.write(Bank.SELECT, page);
|
|
73
|
+
if (memory.read(Bank.WINDOW) != value) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return memory.read(Bank.WINDOW1) == (value ^ 0xFF);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Is `page` real memory? Two different bytes, both read back: a
|
|
80
|
+
// floating window echoes at most one of them.
|
|
81
|
+
function backed(page: utinyint): bool {
|
|
82
|
+
memory.write(Bank.SELECT, page);
|
|
83
|
+
memory.write(Bank.WINDOW, 0x5A);
|
|
84
|
+
memory.write(Bank.WINDOW1, 0xA5);
|
|
85
|
+
if (memory.read(Bank.WINDOW) != 0x5A) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return memory.read(Bank.WINDOW1) == 0xA5;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The 8 KiB pages of banked RAM this machine has: 8, 16, 32, 64, 128
|
|
92
|
+
// or 256.
|
|
93
|
+
function count(): usmallint {
|
|
94
|
+
let pages: utinyint = 8;
|
|
95
|
+
while (pages != 0) {
|
|
96
|
+
let above: utinyint = pages + Bank.OURS;
|
|
97
|
+
banks.mark(0x5A);
|
|
98
|
+
if (banks.shows(above, 0x5A)) {
|
|
99
|
+
banks.mark(0xC3);
|
|
100
|
+
if (banks.shows(above, 0xC3)) {
|
|
101
|
+
return banks.answer(pages); // a mirror of our own page
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!banks.backed(above)) {
|
|
105
|
+
return banks.answer(pages); // nothing there
|
|
106
|
+
}
|
|
107
|
+
pages = pages * 2;
|
|
108
|
+
}
|
|
109
|
+
return banks.answer(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Leave page 1 selected, as the KERNAL does, and widen the count; 0
|
|
113
|
+
// pages means the register never ran out, which is all 256.
|
|
114
|
+
function answer(pages: utinyint): usmallint {
|
|
115
|
+
memory.write(Bank.SELECT, Bank.OURS);
|
|
116
|
+
if (pages == 0) {
|
|
117
|
+
return 256;
|
|
118
|
+
}
|
|
119
|
+
let found: usmallint = pages;
|
|
120
|
+
return found;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// KiB of banked RAM plugged in: 64 on the smallest board, 2048 on the
|
|
124
|
+
// largest. One call at start-up; keep the answer.
|
|
125
|
+
function kib(): usmallint {
|
|
126
|
+
return banks.count() * 8;
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/index.8bs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// @8bitscript/cx16 — Commander X16 target support: the hardware underneath.
|
|
2
|
+
//
|
|
3
|
+
// See ../AGENTS.md before extending this package: the X16 has a great deal
|
|
4
|
+
// of hardware, all of it behind windows, ports, and firmware, and the rules
|
|
5
|
+
// there are about keeping bank state, VERA state, and optional hardware out
|
|
6
|
+
// of game code.
|
|
7
|
+
//
|
|
8
|
+
// The module entry point named by the "8bitscript".entry field in this
|
|
9
|
+
// package's package.json, resolved and linked for:
|
|
10
|
+
//
|
|
11
|
+
// import { setVramAddress, locateTextMap, mapBank, mapLow } from "@8bitscript/cx16";
|
|
12
|
+
//
|
|
13
|
+
// What a program usually wants sits one layer up, in this package's own
|
|
14
|
+
// implementations of the portable capability packages — `./src/screen.8bs`
|
|
15
|
+
// behind @8bitscript/screen and `./src/text.8bs` behind @8bitscript/text,
|
|
16
|
+
// named by the "8bitscript".exports map in package.json — which are built
|
|
17
|
+
// on what this file exports and on nothing else. Where a Commodore
|
|
18
|
+
// package's hardware surface is a set of registers, the X16's is a PORT:
|
|
19
|
+
// VERA's video memory is not CPU-addressable, so what this file exports is
|
|
20
|
+
// the address-port sequence every VRAM access performs, and where the text
|
|
21
|
+
// map currently is.
|
|
22
|
+
//
|
|
23
|
+
// ---- what the program finds when main() runs ---------------------------
|
|
24
|
+
//
|
|
25
|
+
// LLVM-MOS's cx16 start-up code prints CHR$(15) through the KERNAL's CHROUT
|
|
26
|
+
// immediately before calling main() — verified by disassembling a linked
|
|
27
|
+
// build (`lda #$0f / jsr $ffd2 / jsr main`, in the SDK's start-up
|
|
28
|
+
// sequence after `_start`; llvm-objdump labels the block with a local
|
|
29
|
+
// symbol, `shift`). CHR$(15) switches the X16 KERNAL's screen editor into
|
|
30
|
+
// ISO mode so that C's ASCII strings print as ASCII, and the KERNAL clears
|
|
31
|
+
// the screen as part of that switch. Two consequences every program on this
|
|
32
|
+
// target inherits, and the screen and text modules are written around:
|
|
33
|
+
//
|
|
34
|
+
// 1. The screen is blank when main() starts — the boot banner is gone —
|
|
35
|
+
// and it is the KERNAL's default blue (palette index 6) because that
|
|
36
|
+
// is the background nibble the KERNAL cleared every cell with. A
|
|
37
|
+
// program that sets a border and nothing else therefore appears to do
|
|
38
|
+
// nothing: examples/borders looked like exactly that, a solid blue
|
|
39
|
+
// screen, before screen.8bs painted anything itself.
|
|
40
|
+
//
|
|
41
|
+
// 2. The screen is in ISO mode: the character set VERA is drawing from
|
|
42
|
+
// is the KERNAL's ISO-8859-15 set, whose tile index IS the character
|
|
43
|
+
// code — 'A' is 65, '0' is 48, space is 32 — not the PETSCII screen
|
|
44
|
+
// codes (A = 1) the Commodore machines' screen memory takes.
|
|
45
|
+
// `text.putChar` takes ASCII on every machine, so here it passes the
|
|
46
|
+
// code straight through, where the Commodore packages translate.
|
|
47
|
+
//
|
|
48
|
+
// ---- VRAM through VERA's port ---------------------------------------------
|
|
49
|
+
//
|
|
50
|
+
// The text layer (layer 1 in the KERNAL's boot configuration: 1bpp
|
|
51
|
+
// 16-colour text, a tile map 128 tiles wide by 64 high, of which 80x60 are
|
|
52
|
+
// on screen at boot and 76x56 inside this package's border inset —
|
|
53
|
+
// screen_init in the ROM's kernal/drivers/x16/screen.s) takes
|
|
54
|
+
// each cell's colours from that cell's own attribute byte — high nibble
|
|
55
|
+
// background, low nibble foreground — stored in VRAM, which the CPU reaches
|
|
56
|
+
// only through VERA's address/data port: write the 17-bit address into
|
|
57
|
+
// $9F20 (bits 7:0), $9F21 (15:8) and $9F22 (bit 16, plus the auto-increment
|
|
58
|
+
// step in bits 7:4), then read or write $9F23 (DATA0), which advances the
|
|
59
|
+
// address by that step. Cell (x, y) is char byte base + y*256 + x*2,
|
|
60
|
+
// attribute byte one higher.
|
|
61
|
+
//
|
|
62
|
+
// Where `base` is comes from VERA, not from this file: the KERNAL puts
|
|
63
|
+
// the map at $1B000 today, but that is a constant in its source
|
|
64
|
+
// (inc/io.inc), not a published contract, and the ROM's own documentation
|
|
65
|
+
// reserves the right to move it. VERA's layer registers read back what
|
|
66
|
+
// was written to them (verified in the emulator's video_read), so
|
|
67
|
+
// locateTextMap() below reads L1_MAPBASE ($9F35) — the map address
|
|
68
|
+
// divided by 512 — before every access and derives the 17-bit base from
|
|
69
|
+
// it. What this file still assumes is the shape screen_init gives the
|
|
70
|
+
// map: 128 tiles wide (a 256-byte row stride), and wholly within one
|
|
71
|
+
// 64 KiB half of VRAM so the low 16 bits of any cell fit a usmallint.
|
|
72
|
+
//
|
|
73
|
+
// Interrupts are on while main() runs (the frame driver only disables
|
|
74
|
+
// them afterwards), and the KERNAL's IRQ handler drives the mouse-cursor
|
|
75
|
+
// sprite and the cursor blink through this same port. That is safe: both
|
|
76
|
+
// wrap their work in the ROM's screen_save_state/screen_restore_state,
|
|
77
|
+
// which save VERA CTRL, FX_CTRL and ADDR0 (kernal/drivers/x16/screen.s),
|
|
78
|
+
// so an interrupt landing between the three address writes and the data
|
|
79
|
+
// write finds everything as it left it.
|
|
80
|
+
//
|
|
81
|
+
// Writing CTRL ($9F25) also clears ADDRSEL (bit 0), selecting DATA0 — the
|
|
82
|
+
// port everything here uses; screen.8bs sets CTRL itself both times it
|
|
83
|
+
// needs the other DCSEL view, and leaves it 0 (the KERNAL's default).
|
|
84
|
+
|
|
85
|
+
// The text map's VRAM address, as (bit 16, low 16 bits), refreshed from
|
|
86
|
+
// VERA by locateTextMap() — see above for why it is read, not known. Two
|
|
87
|
+
// globals rather than a return value because a function returns one
|
|
88
|
+
// value, and this is two.
|
|
89
|
+
export let mapBank: utinyint = 0;
|
|
90
|
+
export let mapLow: usmallint = 0;
|
|
91
|
+
|
|
92
|
+
// L1_MAPBASE ($9F35) holds the map address >> 9: bit 7 is VRAM bit 16,
|
|
93
|
+
// bits 6:0 are address bits 15:9.
|
|
94
|
+
export function locateTextMap(): void {
|
|
95
|
+
mapBank = memory.read(0x9F35) / 128;
|
|
96
|
+
// Two steps, not `(byte % 128) * 512`: the product can reach 65024,
|
|
97
|
+
// past a 16-bit signed int, and in the C this becomes that is
|
|
98
|
+
// undefined arithmetic on a promoted byte (LLVM-MOS happens to wrap;
|
|
99
|
+
// nothing promises it). Multiplying a usmallint keeps it unsigned.
|
|
100
|
+
mapLow = memory.read(0x9F35) % 128;
|
|
101
|
+
mapLow = mapLow * 512;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Point DATA0 at a 17-bit VRAM address (bank = bit 16, low = bits 15:0),
|
|
105
|
+
// with the auto-increment step index in the top nibble of $9F22 (0 = no
|
|
106
|
+
// step, 1 = +1, 2 = +2 — VERA's table of steps, not a byte count).
|
|
107
|
+
export function setVramAddress(bank: utinyint, low: usmallint, stepIndex: utinyint): void {
|
|
108
|
+
memory.write(0x9F20, low % 256);
|
|
109
|
+
memory.write(0x9F21, low / 256);
|
|
110
|
+
memory.write(0x9F22, bank + stepIndex * 16);
|
|
111
|
+
}
|
package/src/input.8bs
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// @8bitscript/cx16/input — the Commander X16 behind @8bitscript/input.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./input"] in this package's package.json,
|
|
4
|
+
// and by "8bitscript".entry.cx16 in @8bitscript/input's, so a portable
|
|
5
|
+
// program writes
|
|
6
|
+
//
|
|
7
|
+
// import { input } from "@8bitscript/input";
|
|
8
|
+
//
|
|
9
|
+
// and gets this file on an X16.
|
|
10
|
+
//
|
|
11
|
+
// ---- what this layer answers ----------------------------------------------
|
|
12
|
+
//
|
|
13
|
+
// **The pointer, through the KERNAL.** `$FF68` mouse_config, `$FF71`
|
|
14
|
+
// mouse_scan and `$FF6B` mouse_get, wrapped by ./mouse.8bs. A menu asks
|
|
15
|
+
// "did the user just press", so the button is edge-triggered: true on the
|
|
16
|
+
// one frame the press begins, false while it is held. `poll()` has to be
|
|
17
|
+
// called exactly once a frame — twice a frame and every press is seen once
|
|
18
|
+
// and then swallowed. See ./mouse.8bs for why mouse_scan is called from
|
|
19
|
+
// here rather than left to the KERNAL IRQ (the frame driver has interrupts
|
|
20
|
+
// off).
|
|
21
|
+
//
|
|
22
|
+
// **Nothing else yet.** The keyboard is PS/2 decoded into a KERNAL queue
|
|
23
|
+
// (`$FFE4` GETIN) and the SNES-style pads are another KERNAL call
|
|
24
|
+
// (`$FF56` joystick_get). Both are still unreachable from this file for
|
|
25
|
+
// the same reason the mouse was: they need `asm6502` blocks and care about
|
|
26
|
+
// what the KERNAL expects to still be true when they are called. They are
|
|
27
|
+
// the next thing this layer should grow, not a gap this file hides —
|
|
28
|
+
// `left()`/`right()`/`confirm()`/`cancel()` stay false, and a program
|
|
29
|
+
// still links.
|
|
30
|
+
//
|
|
31
|
+
// **`input.mouse` is true on this machine by default**, because the
|
|
32
|
+
// hardware really does have one. `HAS_MOUSE` is that fact, so on a future
|
|
33
|
+
// build that could take the mouse out the pointer half of this file is
|
|
34
|
+
// `if (false)` and LLVM deletes it. Today there is no such catalog option;
|
|
35
|
+
// the emulator's mouse is always there.
|
|
36
|
+
//
|
|
37
|
+
// **The pointer is reported in cells, not pixels.** `pointerCell()` is a
|
|
38
|
+
// flat cell index, the same number `text.print()` takes. The KERNAL's
|
|
39
|
+
// position is a pixel from the top-left of the *active area* — the same
|
|
40
|
+
// origin cell (0, 0) has after screen.8bs insets the display so VERA's
|
|
41
|
+
// border colour has somewhere to show. An earlier draft subtracted that
|
|
42
|
+
// 16-pixel inset again, and the arrow sat two cells down-right of the
|
|
43
|
+
// cell `pointerCell()` named: mouse_get said (319, 239) and the sprite
|
|
44
|
+
// was on screen at (335, 254), which is (319, 239) plus the inset, so
|
|
45
|
+
// the KERNAL is already in the inset space. Shift down by the 8-pixel
|
|
46
|
+
// cell size; a pointer past the visible 76×56 clamps to the last cell.
|
|
47
|
+
import { mouse } from "./mouse.8bs";
|
|
48
|
+
|
|
49
|
+
const HAS_MOUSE: bool = #fact(input.mouse);
|
|
50
|
+
|
|
51
|
+
const COLUMNS: usmallint = #fact(video.columns);
|
|
52
|
+
const ROWS: usmallint = #fact(video.rows);
|
|
53
|
+
const CELL_SHIFT: utinyint = 3; // 8 pixels a cell
|
|
54
|
+
|
|
55
|
+
namespace Edge {
|
|
56
|
+
const LEFT: utinyint = 1;
|
|
57
|
+
const RIGHT: utinyint = 2;
|
|
58
|
+
const UP: utinyint = 4;
|
|
59
|
+
const DOWN: utinyint = 8;
|
|
60
|
+
const CONFIRM: utinyint = 16;
|
|
61
|
+
const CANCEL: utinyint = 32;
|
|
62
|
+
const BUTTON: utinyint = 64;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let held: utinyint = 0;
|
|
66
|
+
let before: utinyint = 0;
|
|
67
|
+
let began: utinyint = 0;
|
|
68
|
+
|
|
69
|
+
let atCell: usmallint = 0;
|
|
70
|
+
|
|
71
|
+
// Pixel to a column or row on the visible grid: shift down by the cell
|
|
72
|
+
// size, clamp to the last cell. mouse_get's origin is the active area's
|
|
73
|
+
// top-left (see the header), so there is no inset to subtract.
|
|
74
|
+
function cellOn(pixel: usmallint, limit: usmallint): usmallint {
|
|
75
|
+
let at: usmallint = pixel >> CELL_SHIFT;
|
|
76
|
+
if (at >= limit) {
|
|
77
|
+
return limit - 1;
|
|
78
|
+
}
|
|
79
|
+
return at;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export namespace input {
|
|
83
|
+
|
|
84
|
+
function begin(): void {
|
|
85
|
+
if (HAS_MOUSE) {
|
|
86
|
+
mouse.begin();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Once a frame, right after waitFrame(), and never twice.
|
|
91
|
+
function poll(): void {
|
|
92
|
+
if (HAS_MOUSE) {
|
|
93
|
+
mouse.poll();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
before = held;
|
|
97
|
+
held = 0;
|
|
98
|
+
|
|
99
|
+
if (HAS_MOUSE) {
|
|
100
|
+
let column: usmallint = cellOn(mouse.x(), COLUMNS);
|
|
101
|
+
let row: usmallint = cellOn(mouse.y(), ROWS);
|
|
102
|
+
atCell = row * COLUMNS + column;
|
|
103
|
+
if (mouse.left()) {
|
|
104
|
+
held = held | Edge.BUTTON;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
began = held & (before ^ 0xFF);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function left(): bool {
|
|
112
|
+
return (began & Edge.LEFT) != 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function right(): bool {
|
|
116
|
+
return (began & Edge.RIGHT) != 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function up(): bool {
|
|
120
|
+
return (began & Edge.UP) != 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function down(): bool {
|
|
124
|
+
return (began & Edge.DOWN) != 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function confirm(): bool {
|
|
128
|
+
return (began & Edge.CONFIRM) != 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function cancel(): bool {
|
|
132
|
+
return (began & Edge.CANCEL) != 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function pointer(): bool {
|
|
136
|
+
return HAS_MOUSE;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function pointerCell(): usmallint {
|
|
140
|
+
return atCell;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function pointerButton(): bool {
|
|
144
|
+
return (began & Edge.BUTTON) != 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/mouse.8bs
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// @8bitscript/cx16/mouse — the X16's PS/2 mouse, through the KERNAL.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./mouse"] in this package's package.json:
|
|
4
|
+
//
|
|
5
|
+
// import { mouse } from "@8bitscript/cx16/mouse";
|
|
6
|
+
//
|
|
7
|
+
// mouse.begin(); // once, at start-up
|
|
8
|
+
// ...
|
|
9
|
+
// waitFrame();
|
|
10
|
+
// mouse.poll(); // once a frame
|
|
11
|
+
// if (mouse.present()) { ... mouse.x() ... }
|
|
12
|
+
//
|
|
13
|
+
// The X16 has no control-port mouse and no registers a program pokes. The
|
|
14
|
+
// pointer is a KERNAL service: `$FF68` mouse_config turns it on (and the
|
|
15
|
+
// firmware draws it as VERA sprite 0), `$FF71` mouse_scan reads the PS/2
|
|
16
|
+
// device and moves that sprite, `$FF6B` mouse_get copies the position and
|
|
17
|
+
// buttons out. All three were read in the ROM this package was verified
|
|
18
|
+
// against — x16-rom `fbe32a60`, `kernal/drivers/x16/ps2mouse.s` — not
|
|
19
|
+
// recalled.
|
|
20
|
+
//
|
|
21
|
+
// **Why poll() calls ps2data_fetch then mouse_scan.** The frame driver
|
|
22
|
+
// disables interrupts (FRAME_SYNC.cx16's `sei`, every waitFrame(), so it
|
|
23
|
+
// can own VERA's VSYNC bit instead of racing the KERNAL IRQ for it). The
|
|
24
|
+
// KERNAL IRQ is what would have called extapi `$08` ps2data_fetch (poll
|
|
25
|
+
// the SMC) and then mouse_scan. mouse_scan only applies bytes already in
|
|
26
|
+
// that buffer — with the IRQ silenced, a scan alone is a no-op and the
|
|
27
|
+
// sprite stays at mouse_config's rest position. `$FEAB` with A=8 is the
|
|
28
|
+
// published fetch; it is not re-entrant, which is why poll() `sei`s first.
|
|
29
|
+
// mouse_scan then saves and restores VERA address state around the sprite
|
|
30
|
+
// write (the same screen_save_state/restore_state the IRQ uses), so a
|
|
31
|
+
// program's text port is still where it left it.
|
|
32
|
+
//
|
|
33
|
+
// **Why the buffer lives at `$80`–`$84`.** mouse_get wants a zero-page
|
|
34
|
+
// address in X and writes four consecutive bytes there (X lo/hi, Y lo/hi),
|
|
35
|
+
// returning buttons in A. The SDK's cx16 link script gives the *program*
|
|
36
|
+
// `$02`–`$7F` (`__basic_zp_end = $80`), and LLVM-MOS allocates that range
|
|
37
|
+
// for imaginary registers and `.zp.noinit` variables. An earlier draft
|
|
38
|
+
// parked this buffer at `$7B` and `sta $7B` for the buttons overwrote
|
|
39
|
+
// `there` every poll — `present()` was false on a machine whose pointer
|
|
40
|
+
// was drawing in the middle of the screen. `$80`–`$84` is the first byte
|
|
41
|
+
// the program is not given, so the linker will not put a variable there.
|
|
42
|
+
// The KERNAL's mouse state lives in banked KVARSB0, not here.
|
|
43
|
+
//
|
|
44
|
+
// **Presence is the fact, not a probe.** A 1351 says it is there through
|
|
45
|
+
// the SID's pots; the X16's KERNAL does not. `input.mouse` is true on the
|
|
46
|
+
// stock sheet because the hardware has a mouse (the emulator's is always
|
|
47
|
+
// there), and `present()` is that fact. A stored flag set before a KERNAL
|
|
48
|
+
// call and read after it is not safe: `asm6502` is opaque, LLVM may keep
|
|
49
|
+
// the flag in a register the call trashes — measured, with the pointer
|
|
50
|
+
// drawing at (319, 239) while `present()` was false. A board whose SMC
|
|
51
|
+
// reports `BAT_FAIL` ($FC) leaves the mouse off and this file will still
|
|
52
|
+
// say present. That case has not been seen under x16emu; *to verify* on a
|
|
53
|
+
// machine with the mouse unplugged.
|
|
54
|
+
//
|
|
55
|
+
// **The rest position is the centre**, not the corner. mouse_config with
|
|
56
|
+
// a nonzero size (this file asks screen_mode, $FF5F, carry set, for the
|
|
57
|
+
// current 8-pixel size — 80×60 in the KERNAL's text mode) parks the
|
|
58
|
+
// pointer at half that range. Under x16emu that is pixel (319, 239) of
|
|
59
|
+
// the 640×480 output. Movement is not exercised headlessly: nothing in a
|
|
60
|
+
// `--screenshot` run moves the host pointer x16emu reads.
|
|
61
|
+
const HAS_MOUSE: bool = #fact(input.mouse);
|
|
62
|
+
|
|
63
|
+
// mouse_get's 4-byte position (X, then Y, 16-bit little-endian each) and
|
|
64
|
+
// the button byte it returns in A. See the header for why these addresses.
|
|
65
|
+
@address(0x80)
|
|
66
|
+
let posX: volatile<usmallint>;
|
|
67
|
+
@address(0x82)
|
|
68
|
+
let posY: volatile<usmallint>;
|
|
69
|
+
@address(0x84)
|
|
70
|
+
let rawButtons: volatile<utinyint>;
|
|
71
|
+
|
|
72
|
+
// Copied out of the buffer during poll(), so every caller in a frame
|
|
73
|
+
// shares one reading — the same discipline @8bitscript/c64/mouse holds
|
|
74
|
+
// to, even though these locations do not run on their own the way the
|
|
75
|
+
// SID's converter does.
|
|
76
|
+
let atX: usmallint = 0;
|
|
77
|
+
let atY: usmallint = 0;
|
|
78
|
+
let heldButtons: utinyint = 0;
|
|
79
|
+
let started: bool = false;
|
|
80
|
+
|
|
81
|
+
export namespace Mouse {
|
|
82
|
+
const LEFT: utinyint = 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export namespace mouse {
|
|
86
|
+
|
|
87
|
+
// Once, at start-up, before the first poll(). Turns the KERNAL mouse
|
|
88
|
+
// on at the current screen's 8-pixel size, which also loads the default
|
|
89
|
+
// pointer into sprite 0 and parks it at the centre. Harmless to call
|
|
90
|
+
// twice: the second call is skipped, so input.begin() and pointer.begin()
|
|
91
|
+
// can both name it without re-centering.
|
|
92
|
+
function begin(): void {
|
|
93
|
+
if (HAS_MOUSE) {
|
|
94
|
+
if (started) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
started = true;
|
|
98
|
+
asm6502 {
|
|
99
|
+
sec
|
|
100
|
+
jsr $FF5F
|
|
101
|
+
lda #1
|
|
102
|
+
jsr $FF68
|
|
103
|
+
ldx #$80
|
|
104
|
+
jsr $FF6B
|
|
105
|
+
sta $84
|
|
106
|
+
}
|
|
107
|
+
atX = posX;
|
|
108
|
+
atY = posY;
|
|
109
|
+
heldButtons = rawButtons;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Once a frame, after waitFrame(). Fetch from the SMC first (the IRQ
|
|
114
|
+
// is off), then mouse_scan, then mouse_get into the buffer, then copy.
|
|
115
|
+
// One reading, kept.
|
|
116
|
+
function poll(): void {
|
|
117
|
+
if (HAS_MOUSE) {
|
|
118
|
+
asm6502 {
|
|
119
|
+
sei
|
|
120
|
+
lda #8
|
|
121
|
+
jsr $FEAB
|
|
122
|
+
jsr $FF71
|
|
123
|
+
ldx #$80
|
|
124
|
+
jsr $FF6B
|
|
125
|
+
sta $84
|
|
126
|
+
}
|
|
127
|
+
atX = posX;
|
|
128
|
+
atY = posY;
|
|
129
|
+
heldButtons = rawButtons;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// True on a build fitted with a mouse, which is every stock X16. This
|
|
134
|
+
// is the fact, not a stored flag: an `asm6502` block is opaque to
|
|
135
|
+
// LLVM, so a bool set before a KERNAL call and read after it can be
|
|
136
|
+
// kept in a register the call trashes. Measured: `there = true` in
|
|
137
|
+
// begin() then mouse_get in poll() left present() false while the
|
|
138
|
+
// pointer was drawing at (319, 239).
|
|
139
|
+
function present(): bool {
|
|
140
|
+
return HAS_MOUSE;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Pixel position from the top-left of the active area — the same
|
|
144
|
+
// origin cell (0, 0) has after screen.8bs insets the display. The
|
|
145
|
+
// KERNAL parks a freshly configured mouse at half the current
|
|
146
|
+
// screen_mode range; under x16emu that is (319, 239), which is the
|
|
147
|
+
// centre of the 640×480 the mode names, not of the inset picture.
|
|
148
|
+
function x(): usmallint {
|
|
149
|
+
return atX;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function y(): usmallint {
|
|
153
|
+
return atY;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The left button, as of the last poll(). Bit 0 of mouse_get's A,
|
|
157
|
+
// which ps2mouse.s documents as left (bit 1 right, bit 2 middle).
|
|
158
|
+
function left(): bool {
|
|
159
|
+
return (heldButtons & Mouse.LEFT) != 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Hide the pointer and stop tracking. pointer.hide() is the caller;
|
|
163
|
+
// the next show() turns tracking back on without re-centering
|
|
164
|
+
// (mouse_config $FF, size left as-is).
|
|
165
|
+
function hide(): void {
|
|
166
|
+
if (HAS_MOUSE) {
|
|
167
|
+
asm6502 {
|
|
168
|
+
lda #0
|
|
169
|
+
ldx #0
|
|
170
|
+
ldy #0
|
|
171
|
+
jsr $FF68
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Undo hide(): mouse on, keep the current cursor and position.
|
|
177
|
+
function show(): void {
|
|
178
|
+
if (HAS_MOUSE) {
|
|
179
|
+
asm6502 {
|
|
180
|
+
lda #$FF
|
|
181
|
+
ldx #0
|
|
182
|
+
ldy #0
|
|
183
|
+
jsr $FF68
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/pointer.8bs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// @8bitscript/cx16/pointer — the X16 behind @8bitscript/pointer: the
|
|
2
|
+
// KERNAL's own mouse cursor.
|
|
3
|
+
//
|
|
4
|
+
// Named by "8bitscript".exports["./pointer"] in this package's package.json,
|
|
5
|
+
// and by "8bitscript".entry.cx16 in @8bitscript/pointer's, so a portable
|
|
6
|
+
// program writes
|
|
7
|
+
//
|
|
8
|
+
// import { pointer } from "@8bitscript/pointer";
|
|
9
|
+
//
|
|
10
|
+
// and gets this file on an X16. It is the *visible* half of the pointer
|
|
11
|
+
// @8bitscript/input already reports: `input` says where the pointer is and
|
|
12
|
+
// whether its button went down, and this says what the user sees while
|
|
13
|
+
// they aim it.
|
|
14
|
+
//
|
|
15
|
+
// ---- the firmware draws it ------------------------------------------------
|
|
16
|
+
//
|
|
17
|
+
// This is the one machine of the nine where a program need not draw a
|
|
18
|
+
// cursor at all. `$FF68` mouse_config (./mouse.8bs's begin()) loads the
|
|
19
|
+
// KERNAL's default pointer — Susan Kare's 16×16 1bpp arrow — into VERA
|
|
20
|
+
// sprite 0 and the firmware moves it. `update()` here is therefore empty
|
|
21
|
+
// on the happy path: `input.poll()` has already called mouse_scan, which
|
|
22
|
+
// writes the sprite's position. A program that never polls gets an arrow
|
|
23
|
+
// that never moves, the same contract as on the C64.
|
|
24
|
+
//
|
|
25
|
+
// `DRAWS` is `#fact(input.mouse)`, true on the stock machine. The rest
|
|
26
|
+
// position is the *centre* of the 640×480 output, not the corner: that is
|
|
27
|
+
// where mouse_config parks it, verified in ps2mouse.s (x16-rom fbe32a60).
|
|
28
|
+
//
|
|
29
|
+
// `setColor` does nothing. The firmware pointer is 1bpp black and white,
|
|
30
|
+
// not a VIC-style sprite colour this layer can poke without fighting the
|
|
31
|
+
// KERNAL's sprite_set_image. Studio asks for white, which is already what
|
|
32
|
+
// the arrow's fill is.
|
|
33
|
+
//
|
|
34
|
+
// `hide()` turns the KERNAL mouse off (mouse_config 0) and `update()`
|
|
35
|
+
// turns it back on without re-centering (mouse_config $FF, size left
|
|
36
|
+
// as-is), so a program that hides for one frame — the pointer example's
|
|
37
|
+
// cancel — gets the arrow back on the next without a jump.
|
|
38
|
+
import { mouse } from "./mouse.8bs";
|
|
39
|
+
|
|
40
|
+
const HAS_MOUSE: bool = #fact(input.mouse);
|
|
41
|
+
|
|
42
|
+
let hidden: bool = false;
|
|
43
|
+
|
|
44
|
+
export namespace pointer {
|
|
45
|
+
|
|
46
|
+
const DRAWS: bool = #fact(input.mouse);
|
|
47
|
+
|
|
48
|
+
function begin(): void {
|
|
49
|
+
if (HAS_MOUSE) {
|
|
50
|
+
// input.begin() has usually already done this; mouse.begin()
|
|
51
|
+
// is skipped the second time so the pointer is not re-centered.
|
|
52
|
+
mouse.begin();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setColor(color: utinyint): void {
|
|
57
|
+
if (HAS_MOUSE) {
|
|
58
|
+
// The firmware pointer is black and white; see the header.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function update(): void {
|
|
63
|
+
if (HAS_MOUSE) {
|
|
64
|
+
if (hidden) {
|
|
65
|
+
mouse.show();
|
|
66
|
+
hidden = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hide(): void {
|
|
72
|
+
if (HAS_MOUSE) {
|
|
73
|
+
mouse.hide();
|
|
74
|
+
hidden = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/screen.8bs
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// @8bitscript/cx16/screen — the Commander X16's implementation of
|
|
2
|
+
// @8bitscript/screen.
|
|
3
|
+
//
|
|
4
|
+
// Named by "8bitscript".exports["./screen"] in this package's package.json,
|
|
5
|
+
// and what
|
|
6
|
+
//
|
|
7
|
+
// import { screen, BorderColor, BackgroundColor } from "@8bitscript/screen";
|
|
8
|
+
//
|
|
9
|
+
// resolves to when the build is for the X16: @8bitscript/screen's entry is
|
|
10
|
+
// keyed by machine and delegates here. Built on the VERA port protocol
|
|
11
|
+
// @8bitscript/cx16 exports — read that file first for what the KERNAL
|
|
12
|
+
// leaves on screen and how VRAM is reached. Every machine's screen.8bs
|
|
13
|
+
// exports this same surface, which is what lets a program import it from
|
|
14
|
+
// @8bitscript/screen and never name the hardware.
|
|
15
|
+
import { mapBank, mapLow, locateTextMap, setVramAddress } from "./index.8bs";
|
|
16
|
+
|
|
17
|
+
// ---- where a border comes from --------------------------------------------
|
|
18
|
+
//
|
|
19
|
+
// VERA's border colour is a plain CPU-addressable register — but the
|
|
20
|
+
// border it colours has no size by default. VERA draws layers inside an
|
|
21
|
+
// "active area" whose edges are DC_HSTART/HSTOP/VSTART/VSTOP, and the
|
|
22
|
+
// KERNAL boots with that area set to the entire 640x480 output, so nothing
|
|
23
|
+
// is left over for the border colour to fill. Setting the border alone is
|
|
24
|
+
// invisible — the second reason examples/borders once showed only blue.
|
|
25
|
+
//
|
|
26
|
+
// So setColors() shrinks the active area by 16 pixels on every side —
|
|
27
|
+
// HSTART/HSTOP count in units of 4 pixels (0..160), VSTART/VSTOP in units
|
|
28
|
+
// of 2 (0..240), hence 4/156 and 8/232 — and VERA fills the 16-pixel ring
|
|
29
|
+
// outside it with the border colour. The text layer is positioned relative
|
|
30
|
+
// to the active area, so cell (0,0) sits at the inset's top-left corner
|
|
31
|
+
// and the map's rightmost 4 columns and bottom 4 rows fall outside the
|
|
32
|
+
// active area: the grid a program sees is the 76x56 that is drawn
|
|
33
|
+
// (text.COLUMNS / text.CELL_COUNT say so), not the map's 80x60. This
|
|
34
|
+
// is the closest thing the X16 has to the VIC/VIC-II's dedicated border
|
|
35
|
+
// area, and like the NES's drawn frame it is honest about what it is: a
|
|
36
|
+
// choice this module makes, not a register the machine has.
|
|
37
|
+
//
|
|
38
|
+
// "Relative" needs one correction, measured rather than assumed. Under
|
|
39
|
+
// x16emu r50 the layer's first line lands TWO lines above the active
|
|
40
|
+
// area's top edge whenever VSTART is nonzero: with a 16-line inset, row 0
|
|
41
|
+
// of text occupied lines 14-21 and its top two lines were hidden under
|
|
42
|
+
// the border, and the same two lines with a 32-line inset. The emulator's
|
|
43
|
+
// video.c shows why — its layer line counter starts accumulating on the
|
|
44
|
+
// VSTART line through a two-line register-history pipeline (the one that
|
|
45
|
+
// models VERA's own raster-effect latency), so the active area's first
|
|
46
|
+
// line already reads as layer line 2. setColors() compensates with layer
|
|
47
|
+
// 1's vertical scroll: 510, which is -2 modulo the map's 512 lines, puts
|
|
48
|
+
// layer line 0 on the active area's first line exactly (verified the same
|
|
49
|
+
// way: row 0's glyphs occupy lines 16-23 with the 16-line inset, none
|
|
50
|
+
// hidden). The horizontal edge needs no such correction. Real hardware
|
|
51
|
+
// has not been checked; if VERA silicon has no such offset, the visible
|
|
52
|
+
// symptom would be the map's last row showing in the top two lines — so
|
|
53
|
+
// the repaint below covers that row too and keeps its characters blank.
|
|
54
|
+
//
|
|
55
|
+
// The four DC_ registers live at $9F29-$9F2C under two views selected by
|
|
56
|
+
// DCSEL (CTRL $9F25 bit 1): DCSEL=0 is VIDEO/HSCALE/VSCALE/BORDER, DCSEL=1
|
|
57
|
+
// is HSTART/HSTOP/VSTART/VSTOP — confirmed against llvm-mos-sdk's cx16.h,
|
|
58
|
+
// which lays out `struct __vera` exactly so. setColors() sets DCSEL
|
|
59
|
+
// itself both times and leaves it 0 (the KERNAL's default), so nothing
|
|
60
|
+
// here assumes what a previous program left it at.
|
|
61
|
+
//
|
|
62
|
+
// ---- the background: painted, not set ---------------------------------------
|
|
63
|
+
//
|
|
64
|
+
// There is no screen-background register at all: each text cell's colours
|
|
65
|
+
// come from its own attribute byte in VRAM (see @8bitscript/cx16). So the
|
|
66
|
+
// background is painted: setColors() writes every one of the 4800
|
|
67
|
+
// on-screen cells' attribute bytes to `background` in the high nibble and
|
|
68
|
+
// white (1) in the low — 60 rows of 80 auto-incrementing writes — the same
|
|
69
|
+
// way the Commodore packages draw their digits in white against whatever
|
|
70
|
+
// background is chosen. Around 60,000 cycles at 8MHz, a few milliseconds:
|
|
71
|
+
// VERA's VRAM is safe to write at any point in the frame (a write mid-frame
|
|
72
|
+
// can tear for that one frame; it cannot corrupt anything, unlike the
|
|
73
|
+
// NES's port), so this needs no vblank budget and setColors() is safe from
|
|
74
|
+
// the frame loop as well as from setup. Its side effect is deliberate and
|
|
75
|
+
// documented: any foreground colour a program set with text.putColor() is
|
|
76
|
+
// reset to white by the next setColors().
|
|
77
|
+
//
|
|
78
|
+
// Colours are VERA palette indices. The KERNAL's default palette starts
|
|
79
|
+
// with the sixteen C64 colours in C64 order — 0 black, 1 white, 2 red,
|
|
80
|
+
// 3 cyan, 4 purple, 5 green, 6 blue, 7 yellow, and so on — verified on
|
|
81
|
+
// screen under x16emu for the values examples/borders uses, so the
|
|
82
|
+
// Commodore packages' colour numbers mean the same colours here.
|
|
83
|
+
|
|
84
|
+
// The border exists once the active area is inset — 16 pixels in on
|
|
85
|
+
// every side — and layer 1 is scrolled so cell (0,0) lands at its corner.
|
|
86
|
+
// Done by whichever screen call comes first, once.
|
|
87
|
+
let insetDone: utinyint = 0;
|
|
88
|
+
|
|
89
|
+
function insetPicture(): void {
|
|
90
|
+
if (insetDone == 1) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
insetDone = 1;
|
|
94
|
+
// DCSEL=1: the active-area edges.
|
|
95
|
+
memory.write(0x9F25, 0x02);
|
|
96
|
+
memory.write(0x9F29, 4); // HSTART: 16px
|
|
97
|
+
memory.write(0x9F2A, 156); // HSTOP: 624px
|
|
98
|
+
memory.write(0x9F2B, 8); // VSTART: 16px
|
|
99
|
+
memory.write(0x9F2C, 232); // VSTOP: 464px
|
|
100
|
+
memory.write(0x9F25, 0x00); // back to DCSEL=0
|
|
101
|
+
// Layer 1's scroll: horizontal 0, vertical 510 = -2 mod the map's
|
|
102
|
+
// 512 lines — the two-line correction the header explains, so that
|
|
103
|
+
// cell (0,0) lands exactly at the active area's top-left corner.
|
|
104
|
+
memory.write(0x9F37, 0x00); // L1_HSCROLL_L
|
|
105
|
+
memory.write(0x9F38, 0x00); // L1_HSCROLL_H
|
|
106
|
+
memory.write(0x9F39, 0xFE); // L1_VSCROLL_L: 510 & 0xFF
|
|
107
|
+
memory.write(0x9F3A, 0x01); // L1_VSCROLL_H: 510 >> 8
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export namespace screen {
|
|
111
|
+
function setColors(border: u8, background: u8): void {
|
|
112
|
+
insetPicture();
|
|
113
|
+
// DCSEL=0 (and ADDRSEL=0): the border colour itself.
|
|
114
|
+
memory.write(0x9F25, 0x00);
|
|
115
|
+
memory.write(0x9F2C, border);
|
|
116
|
+
|
|
117
|
+
// Every cell's attribute byte: background high, white low. All 64
|
|
118
|
+
// map rows, not just the 60 on screen — row 63 is what the
|
|
119
|
+
// vertical-scroll correction above would expose if real hardware
|
|
120
|
+
// lacks the offset it corrects (see the header), so it gets the
|
|
121
|
+
// same colours and, below, blank characters.
|
|
122
|
+
locateTextMap();
|
|
123
|
+
for (let row: usmallint = 0; row < 64; row++) {
|
|
124
|
+
setVramAddress(mapBank, mapLow + 1 + row * 256, 2);
|
|
125
|
+
for (let col: utinyint = 0; col < 80; col++) {
|
|
126
|
+
memory.write(0x9F23, background * 16 + 1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
setVramAddress(mapBank, mapLow + 63 * 256, 2);
|
|
130
|
+
for (let col: utinyint = 0; col < 80; col++) {
|
|
131
|
+
memory.write(0x9F23, 32);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// `blank(border, background)`: every cell blank and both colours set —
|
|
136
|
+
// black when left off, or `BorderColor.KEEP` / `BackgroundColor.KEEP`
|
|
137
|
+
// to leave one as it is. `setBorder` and `setBackground` are one
|
|
138
|
+
// colour each, `setColors` the pair; `text.setColor` owns what the
|
|
139
|
+
// next print looks like.
|
|
140
|
+
function blank(border: utinyint = BorderColor.BLACK, background: utinyint = BackgroundColor.BLACK): void {
|
|
141
|
+
if (border != BorderColor.KEEP) {
|
|
142
|
+
screen.setBorder(border);
|
|
143
|
+
}
|
|
144
|
+
if (background != BackgroundColor.KEEP) {
|
|
145
|
+
screen.setBackground(background);
|
|
146
|
+
}
|
|
147
|
+
// Every character of all 64 map rows (see setColors above for why
|
|
148
|
+
// 64) blank; the attribute bytes between them are left alone.
|
|
149
|
+
locateTextMap();
|
|
150
|
+
for (let row: usmallint = 0; row < 64; row++) {
|
|
151
|
+
setVramAddress(mapBank, mapLow + row * 256, 2);
|
|
152
|
+
for (let col: utinyint = 0; col < 80; col++) {
|
|
153
|
+
memory.write(0x9F23, 32);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function setBackground(background: u8): void {
|
|
159
|
+
insetPicture();
|
|
160
|
+
// The background lives in every cell's attribute byte, so this is
|
|
161
|
+
// the same repaint setColors() does: background high, white low.
|
|
162
|
+
locateTextMap();
|
|
163
|
+
for (let row: usmallint = 0; row < 64; row++) {
|
|
164
|
+
setVramAddress(mapBank, mapLow + 1 + row * 256, 2);
|
|
165
|
+
for (let col: utinyint = 0; col < 80; col++) {
|
|
166
|
+
memory.write(0x9F23, background * 16 + 1);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function setBorder(border: u8): void {
|
|
172
|
+
insetPicture();
|
|
173
|
+
memory.write(0x9F25, 0x00); // DCSEL=0: the border colour register
|
|
174
|
+
memory.write(0x9F2C, border);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---- colour names ----------------------------------------------------------
|
|
179
|
+
//
|
|
180
|
+
// The same eight names every machine's screen.8bs exports — Black, White,
|
|
181
|
+
// Red, Cyan, Purple, Green, Blue, Yellow — so a program can write
|
|
182
|
+
// `screen.setColors(BorderColor.BLUE, BackgroundColor.BLACK)` through
|
|
183
|
+
// @8bitscript/screen and get blue on every target. The values are the first
|
|
184
|
+
// sixteen entries of VERA's default palette, which are the C64's sixteen
|
|
185
|
+
// colours in the C64's numbering (see above), so both namespaces carry all
|
|
186
|
+
// sixteen.
|
|
187
|
+
|
|
188
|
+
export namespace BorderColor {
|
|
189
|
+
const BLACK: utinyint = 0;
|
|
190
|
+
const WHITE: utinyint = 1;
|
|
191
|
+
const RED: utinyint = 2;
|
|
192
|
+
const CYAN: utinyint = 3;
|
|
193
|
+
const PURPLE: utinyint = 4;
|
|
194
|
+
const GREEN: utinyint = 5;
|
|
195
|
+
const BLUE: utinyint = 6;
|
|
196
|
+
const YELLOW: utinyint = 7;
|
|
197
|
+
const ORANGE: utinyint = 8;
|
|
198
|
+
const BROWN: utinyint = 9;
|
|
199
|
+
const LIGHT_RED: utinyint = 10;
|
|
200
|
+
const DARK_GREY: utinyint = 11;
|
|
201
|
+
const GREY: utinyint = 12;
|
|
202
|
+
const LIGHT_GREEN: utinyint = 13;
|
|
203
|
+
const LIGHT_BLUE: utinyint = 14;
|
|
204
|
+
const LIGHT_GREY: utinyint = 15;
|
|
205
|
+
// Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
|
|
206
|
+
// a value no register here takes.
|
|
207
|
+
const KEEP: utinyint = 255;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export namespace BackgroundColor {
|
|
211
|
+
const BLACK: utinyint = 0;
|
|
212
|
+
const WHITE: utinyint = 1;
|
|
213
|
+
const RED: utinyint = 2;
|
|
214
|
+
const CYAN: utinyint = 3;
|
|
215
|
+
const PURPLE: utinyint = 4;
|
|
216
|
+
const GREEN: utinyint = 5;
|
|
217
|
+
const BLUE: utinyint = 6;
|
|
218
|
+
const YELLOW: utinyint = 7;
|
|
219
|
+
const ORANGE: utinyint = 8;
|
|
220
|
+
const BROWN: utinyint = 9;
|
|
221
|
+
const LIGHT_RED: utinyint = 10;
|
|
222
|
+
const DARK_GREY: utinyint = 11;
|
|
223
|
+
const GREY: utinyint = 12;
|
|
224
|
+
const LIGHT_GREEN: utinyint = 13;
|
|
225
|
+
const LIGHT_BLUE: utinyint = 14;
|
|
226
|
+
const LIGHT_GREY: utinyint = 15;
|
|
227
|
+
// Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
|
|
228
|
+
// a value no register here takes.
|
|
229
|
+
const KEEP: utinyint = 255;
|
|
230
|
+
}
|
package/src/text.8bs
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// @8bitscript/cx16/text — the Commander X16's implementation of
|
|
2
|
+
// @8bitscript/text.
|
|
3
|
+
//
|
|
4
|
+
// Named by "8bitscript".exports["./text"] in this package's package.json,
|
|
5
|
+
// and what
|
|
6
|
+
//
|
|
7
|
+
// import { text } from "@8bitscript/text";
|
|
8
|
+
//
|
|
9
|
+
// resolves to when the build is for the X16. Built on the VERA port protocol
|
|
10
|
+
// @8bitscript/cx16 exports — read that file first for what the KERNAL
|
|
11
|
+
// leaves on screen and how VRAM is reached. Every machine's text.8bs
|
|
12
|
+
// exports this same namespace — the same names, the same shapes, ASCII
|
|
13
|
+
// codes, and a cell 0 at the top-left corner inside the border — so a
|
|
14
|
+
// program that imports it from @8bitscript/text draws the same thing on
|
|
15
|
+
// every target.
|
|
16
|
+
import { mapBank, mapLow, locateTextMap, setVramAddress } from "./index.8bs";
|
|
17
|
+
|
|
18
|
+
// 76x56 = 4256 cells, row-major (the map is 80x60; screen.8bs's border
|
|
19
|
+
// inset shows 76x56 of it, and that is the grid); codes are ASCII, as on every machine, and
|
|
20
|
+
// here that is also what the hardware wants (ISO mode — see
|
|
21
|
+
// @8bitscript/cx16). Each cell is two bytes in the map — the character,
|
|
22
|
+
// then its attribute byte: background colour in the high nibble,
|
|
23
|
+
// foreground in the low.
|
|
24
|
+
//
|
|
25
|
+
// putColor is real here, unlike on the NES and Atari: it sets a cell's
|
|
26
|
+
// foreground nibble and leaves the background nibble as it finds it — a
|
|
27
|
+
// read-modify-write through the same port, with the auto-increment step at
|
|
28
|
+
// 0 so the read and the write land on one byte (VERA fetches the byte when
|
|
29
|
+
// the address is set, so DATA0 reads it back without moving). Whose
|
|
30
|
+
// background that nibble holds is not this module's business: the KERNAL's
|
|
31
|
+
// blue before screen.setColors() has painted, the painted colour after.
|
|
32
|
+
|
|
33
|
+
// ---- print: strings and number fields ------------------------------------
|
|
34
|
+
//
|
|
35
|
+
// `text.print(cell, s)` writes a string's characters into consecutive
|
|
36
|
+
// cells from `cell`, and `text.printNumber(cell, value, width)` writes
|
|
37
|
+
// `value` as exactly `width` decimal digits, zero-padded and right-aligned,
|
|
38
|
+
// so a field on a HUD never shifts columns. Both draw in the current colour —
|
|
39
|
+
// white until `text.setColor(TextColor.CYAN)` changes it, and that one
|
|
40
|
+
// call then colours everything printed after it, on the machines that have
|
|
41
|
+
// per-cell colour. They are also the two
|
|
42
|
+
// functions the compiler's template layout targets: `text.print(0,
|
|
43
|
+
// \`TICK ${ticks:1}\`)` is laid out at compile time into these same calls
|
|
44
|
+
// (see packages/compiler/src/ir).
|
|
45
|
+
//
|
|
46
|
+
// Under them: `locate()` turns a cell into a
|
|
47
|
+
// VERA address without dividing, and a run of text lets VERA step the
|
|
48
|
+
// address itself.
|
|
49
|
+
// `text.putChar` and `text.putColor` stay the one-cell pokes a caller can
|
|
50
|
+
// build anything from.
|
|
51
|
+
let currentColor: utinyint = 1; // white, until text.setColor() says otherwise
|
|
52
|
+
let currentReverse: bool = false; // until text.setReverse() says otherwise
|
|
53
|
+
|
|
54
|
+
// ISO mode has no reverse copies of the glyphs, so invert is a swap of
|
|
55
|
+
// the attribute nibbles: the cell fills with the text colour and the
|
|
56
|
+
// glyph pixels take whatever background `screen.setColors` left. Port 1
|
|
57
|
+
// is already sitting on this cell's colour byte after locate().
|
|
58
|
+
function attributeByte(): utinyint {
|
|
59
|
+
let attr: utinyint = memory.read(0x9F24);
|
|
60
|
+
if (currentReverse) {
|
|
61
|
+
return (currentColor * 16) + (attr / 16);
|
|
62
|
+
}
|
|
63
|
+
return (attr & 0xF0) | currentColor;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Point VERA at a cell, and hand back its column for a caller that walks
|
|
67
|
+
// on from there. Rows are 256 bytes apart in the map, cells two bytes —
|
|
68
|
+
// character, then colour (background in the high nibble, text in the low).
|
|
69
|
+
// No division: with 76 cells to a row and 256 = 3 * 76 + 28, a cell's row
|
|
70
|
+
// is three per 256 plus (28 * high byte + low byte) / 76, and that last
|
|
71
|
+
// quotient, on a number below 704, is ((x / 4) * 54) / 1024 exactly —
|
|
72
|
+
// checked for every cell. Two address ports: port 0 walks character, colour,
|
|
73
|
+
// character... stepping by one, and is what gets written; port 1 walks the
|
|
74
|
+
// colour bytes stepping by two, and is read for each cell's background
|
|
75
|
+
// nibble so a print keeps whatever `screen.setBackground` put there.
|
|
76
|
+
// Leaves ADDRSEL at 0, which screen.8bs assumes.
|
|
77
|
+
function locate(cell: usmallint): utinyint {
|
|
78
|
+
locateTextMap();
|
|
79
|
+
let high: utinyint = cell / 256;
|
|
80
|
+
let x: usmallint = high * 28 + cell % 256;
|
|
81
|
+
let row: utinyint = high * 3 + ((x / 4) * 54) / 1024;
|
|
82
|
+
let col: utinyint = cell - row * 76;
|
|
83
|
+
let at: usmallint = mapLow + row * 256 + col * 2;
|
|
84
|
+
memory.write(0x9F25, 0x01); // ADDRSEL=1
|
|
85
|
+
setVramAddress(mapBank, at + 1, 2);
|
|
86
|
+
memory.write(0x9F25, 0x00); // ADDRSEL=0
|
|
87
|
+
setVramAddress(mapBank, at, 1);
|
|
88
|
+
return col;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---- digits -------------------------------------------------------------
|
|
92
|
+
//
|
|
93
|
+
// A number is written one place at a time, high to low, by subtracting the
|
|
94
|
+
// place value until it no longer fits: the 6502 has no divide instruction,
|
|
95
|
+
// and `value / 10` would link a 250-byte routine to do it. Places above the
|
|
96
|
+
// field are still taken off, so a field narrower than its number shows the
|
|
97
|
+
// low digits; places the number does not reach print as zeros.
|
|
98
|
+
const DIGIT_PLACES: array<usmallint, 5> = [10000, 1000, 100, 10, 1];
|
|
99
|
+
|
|
100
|
+
export namespace text {
|
|
101
|
+
const CELL_COUNT: usmallint = 4256; // 76 columns x 56 rows, inside the inset
|
|
102
|
+
const COLUMNS: utinyint = 76; // cells per row, so cell = y * text.COLUMNS + x
|
|
103
|
+
|
|
104
|
+
function putChar(cell: usmallint, code: utinyint): void {
|
|
105
|
+
locate(cell);
|
|
106
|
+
memory.write(0x9F23, code);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function putColor(cell: usmallint, color: utinyint): void {
|
|
110
|
+
locate(cell);
|
|
111
|
+
memory.read(0x9F23); // step port 0 past the character byte
|
|
112
|
+
memory.write(0x9F23, (memory.read(0x9F24) & 0xF0) | (color & 0x0F));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function setColor(color: utinyint): void {
|
|
116
|
+
currentColor = color;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function setReverse(on: bool): void {
|
|
120
|
+
currentReverse = on;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function print(cell: usmallint, s: string): void {
|
|
124
|
+
let col: utinyint = locate(cell);
|
|
125
|
+
for (let i: utinyint = 0; i < s.length; i++) {
|
|
126
|
+
memory.write(0x9F23, s[i]);
|
|
127
|
+
memory.write(0x9F23, attributeByte());
|
|
128
|
+
col++;
|
|
129
|
+
if (col == text.COLUMNS) {
|
|
130
|
+
col = locate(cell + i + 1); // the next row is not the next address
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function printNumber(cell: usmallint, value: usmallint, width: utinyint): void {
|
|
136
|
+
let col: utinyint = locate(cell);
|
|
137
|
+
let k: utinyint = width;
|
|
138
|
+
if (k < DIGIT_PLACES.length) {
|
|
139
|
+
k = DIGIT_PLACES.length;
|
|
140
|
+
}
|
|
141
|
+
while (k > 0) {
|
|
142
|
+
let digit: utinyint = 48; // '0'
|
|
143
|
+
if (k <= DIGIT_PLACES.length) {
|
|
144
|
+
while (value >= DIGIT_PLACES[DIGIT_PLACES.length - k]) {
|
|
145
|
+
value = value - DIGIT_PLACES[DIGIT_PLACES.length - k];
|
|
146
|
+
digit++;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (k <= width) {
|
|
150
|
+
memory.write(0x9F23, digit);
|
|
151
|
+
memory.write(0x9F23, attributeByte());
|
|
152
|
+
col++;
|
|
153
|
+
if (col == text.COLUMNS) {
|
|
154
|
+
col = locate(cell + width - k + 1); // the next row is not the next address
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
k--;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The colours `text.setColor()` takes: the eight names every machine
|
|
163
|
+
// shares, with this machine's colour-RAM values.
|
|
164
|
+
export namespace TextColor {
|
|
165
|
+
const BLACK: utinyint = 0;
|
|
166
|
+
const WHITE: utinyint = 1;
|
|
167
|
+
const RED: utinyint = 2;
|
|
168
|
+
const CYAN: utinyint = 3;
|
|
169
|
+
const PURPLE: utinyint = 4;
|
|
170
|
+
const GREEN: utinyint = 5;
|
|
171
|
+
const BLUE: utinyint = 6;
|
|
172
|
+
const YELLOW: utinyint = 7;
|
|
173
|
+
}
|