mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
b80436d99c
* Convert js/ram to a class * Convert js/mmu to Typescript * Convert js/apple2io to Typescript * Convert js/canvas to Typescript * Use new types in js/mmu * Rename js/symbols.js to js/symbols.ts * Remove the difference between readPages and writePages As @whscullin said in PR #38, there's no need to have both readable and writable pages since all implementations are currently both. This change combines them into `Page`. Likewise, `PageHandler` now extends `Page`. `Apple2IO` now implements `PageHandler`. This caught a bug where `end` had been renamed `endend` by mistake. There are a few other formatting changes as well. * Convert js/apple2 to Typescript * Convert js/prefs to Typescript * Convert all of the ROMs in js/roms to Typescript Now all of the ROMs are classes that extend the ROM class. There is some rudamentary checking to make sure that the length of the ROM matches the declared start and end pages. (This caught what looks to be an error in roms/apple2e, but it's hard for me to tell.) The typing also caught an error where the character ROM was being used for the main ROM for the apple2j version. * Convert js/roms/cards/* to Typescript * Convert js/formats/format_utils to Typescript This change also seems to fix a bug with `.po` image files that weren't being read correctly.
441 lines
12 KiB
TypeScript
441 lines
12 KiB
TypeScript
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
|
*
|
|
* Permission to use, copy, modify, distribute, and sell this software and its
|
|
* documentation for any purpose is hereby granted without fee, provided that
|
|
* the above copyright notice appear in all copies and that both that
|
|
* copyright notice and this permission notice appear in supporting
|
|
* documentation. No representations are made about the suitability of this
|
|
* software for any purpose. It is provided "as is" without express or
|
|
* implied warranty.
|
|
*/
|
|
|
|
import { byte, memory } from '../types';
|
|
import { base64_decode, base64_encode } from '../base64';
|
|
import { bytify, debug, toHex } from '../util';
|
|
|
|
export type Disk = {
|
|
format: string,
|
|
name: string,
|
|
volume: byte,
|
|
tracks: byte[],
|
|
readOnly: boolean,
|
|
};
|
|
|
|
export type Drive = {
|
|
format: string,
|
|
volume: 254,
|
|
tracks: memory[],
|
|
readOnly: false,
|
|
dirty: false
|
|
}
|
|
|
|
|
|
// var DO = [0x0,0x7,0xE,0x6,0xD,0x5,0xC,0x4,
|
|
// 0xB,0x3,0xA,0x2,0x9,0x1,0x8,0xF];
|
|
export const _DO = [
|
|
0x0, 0xD, 0xB, 0x9, 0x7, 0x5, 0x3, 0x1,
|
|
0xE, 0xC, 0xA, 0x8, 0x6, 0x4, 0x2, 0xF
|
|
];
|
|
|
|
// var PO = [0x0,0x8,0x1,0x9,0x2,0xa,0x3,0xb,
|
|
// 0x4,0xc,0x5,0xd,0x6,0xe,0x7,0xf];
|
|
export const _PO = [
|
|
0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe,
|
|
0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf
|
|
];
|
|
|
|
// var D13O = [
|
|
// 0x0, 0xa, 0x7, 0x4, 0x1, 0xb, 0x8, 0x5, 0x2, 0xc, 0x9, 0x6, 0x3
|
|
// ];
|
|
export const _D13O = [
|
|
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc
|
|
];
|
|
|
|
const _trans53 = [
|
|
0xab, 0xad, 0xae, 0xaf, 0xb5, 0xb6, 0xb7, 0xba,
|
|
0xbb, 0xbd, 0xbe, 0xbf, 0xd6, 0xd7, 0xda, 0xdb,
|
|
0xdd, 0xde, 0xdf, 0xea, 0xeb, 0xed, 0xee, 0xef,
|
|
0xf5, 0xf6, 0xf7, 0xfa, 0xfb, 0xfd, 0xfe, 0xff
|
|
];
|
|
|
|
const _trans62 = [
|
|
0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
|
|
0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3,
|
|
0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc,
|
|
0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, 0xd3,
|
|
0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde,
|
|
0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec,
|
|
0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
|
|
0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff
|
|
];
|
|
|
|
export const detrans62 = [
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
|
0x00, 0x00, 0x02, 0x03, 0x00, 0x04, 0x05, 0x06,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x08,
|
|
0x00, 0x00, 0x00, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
|
|
0x00, 0x00, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13,
|
|
0x00, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x1B, 0x00, 0x1C, 0x1D, 0x1E,
|
|
0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x20, 0x21,
|
|
0x00, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x2A, 0x2B,
|
|
0x00, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
|
|
0x00, 0x00, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
|
|
0x00, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F
|
|
];
|
|
|
|
/**
|
|
* From Beneath Apple DOS
|
|
*/
|
|
export function fourXfour(val: byte): [xx: byte, yy: byte] {
|
|
var xx = val & 0xaa;
|
|
var yy = val & 0x55;
|
|
|
|
xx >>= 1;
|
|
xx |= 0xaa;
|
|
yy |= 0xaa;
|
|
|
|
return [xx, yy];
|
|
}
|
|
|
|
export function defourXfour(xx: byte, yy: byte): byte {
|
|
return ((xx << 1) | 0x01) & yy;
|
|
}
|
|
|
|
export function explodeSector16(volume: byte, track: byte, sector: byte, data: memory) {
|
|
let buf = [];
|
|
let gap;
|
|
|
|
/*
|
|
* Gap 1/3 (40/0x28 bytes)
|
|
*/
|
|
|
|
if (sector === 0) // Gap 1
|
|
gap = 0x80;
|
|
else { // Gap 3
|
|
gap = track === 0 ? 0x28 : 0x26;
|
|
}
|
|
|
|
for (let idx = 0; idx < gap; idx++) {
|
|
buf.push(0xff);
|
|
}
|
|
|
|
/*
|
|
* Address Field
|
|
*/
|
|
|
|
let checksum = volume ^ track ^ sector;
|
|
buf = buf.concat([0xd5, 0xaa, 0x96]); // Address Prolog D5 AA 96
|
|
buf = buf.concat(fourXfour(volume));
|
|
buf = buf.concat(fourXfour(track));
|
|
buf = buf.concat(fourXfour(sector));
|
|
buf = buf.concat(fourXfour(checksum));
|
|
buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB
|
|
|
|
/*
|
|
* Gap 2 (5 bytes)
|
|
*/
|
|
|
|
for (let idx = 0; idx < 0x05; idx++) {
|
|
buf.push(0xff);
|
|
}
|
|
|
|
/*
|
|
* Data Field
|
|
*/
|
|
|
|
buf = buf.concat([0xd5, 0xaa, 0xad]); // Data Prolog D5 AA AD
|
|
|
|
let nibbles: byte[] = [];
|
|
const ptr2 = 0;
|
|
const ptr6 = 0x56;
|
|
|
|
for (let idx = 0; idx < 0x156; idx++) {
|
|
nibbles[idx] = 0;
|
|
}
|
|
|
|
let idx2 = 0x55;
|
|
for (let idx6 = 0x101; idx6 >= 0; idx6--) {
|
|
let val6 = data[idx6 % 0x100];
|
|
let val2: byte = nibbles[ptr2 + idx2];
|
|
|
|
val2 = (val2 << 1) | (val6 & 1);
|
|
val6 >>= 1;
|
|
val2 = (val2 << 1) | (val6 & 1);
|
|
val6 >>= 1;
|
|
|
|
nibbles[ptr6 + idx6] = val6;
|
|
nibbles[ptr2 + idx2] = val2;
|
|
|
|
if (--idx2 < 0)
|
|
idx2 = 0x55;
|
|
}
|
|
|
|
let last = 0;
|
|
for (let idx = 0; idx < 0x156; idx++) {
|
|
const val = nibbles[idx];
|
|
buf.push(_trans62[last ^ val]);
|
|
last = val;
|
|
}
|
|
buf.push(_trans62[last]);
|
|
|
|
buf = buf.concat([0xde, 0xaa, 0xf2]); // Epilog DE AA F2
|
|
|
|
/*
|
|
* Gap 3
|
|
*/
|
|
|
|
buf.push(0xff);
|
|
|
|
return buf;
|
|
}
|
|
|
|
export function explodeSector13(volume: byte, track: byte, sector: byte, data: byte[]) {
|
|
let buf = [];
|
|
let gap;
|
|
|
|
/*
|
|
* Gap 1/3 (40/0x28 bytes)
|
|
*/
|
|
|
|
if (sector === 0) // Gap 1
|
|
gap = 0x80;
|
|
else { // Gap 3
|
|
gap = track === 0 ? 0x28 : 0x26;
|
|
}
|
|
|
|
for (let idx = 0; idx < gap; idx++) {
|
|
buf.push(0xff);
|
|
}
|
|
|
|
/*
|
|
* Address Field
|
|
*/
|
|
|
|
let checksum = volume ^ track ^ sector;
|
|
buf = buf.concat([0xd5, 0xaa, 0xb5]); // Address Prolog D5 AA B5
|
|
buf = buf.concat(fourXfour(volume));
|
|
buf = buf.concat(fourXfour(track));
|
|
buf = buf.concat(fourXfour(sector));
|
|
buf = buf.concat(fourXfour(checksum));
|
|
buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB
|
|
|
|
/*
|
|
* Gap 2 (5 bytes)
|
|
*/
|
|
|
|
for (let idx = 0; idx < 0x05; idx++) {
|
|
buf.push(0xff);
|
|
}
|
|
|
|
/*
|
|
* Data Field
|
|
*/
|
|
|
|
buf = buf.concat([0xd5, 0xaa, 0xad]); // Data Prolog D5 AA AD
|
|
|
|
let nibbles = [];
|
|
|
|
let jdx = 0;
|
|
for (let idx = 0x32; idx >= 0; idx--) {
|
|
var a5 = data[jdx] >> 3;
|
|
var a3 = data[jdx] & 0x07;
|
|
jdx++;
|
|
var b5 = data[jdx] >> 3;
|
|
var b3 = data[jdx] & 0x07;
|
|
jdx++;
|
|
var c5 = data[jdx] >> 3;
|
|
var c3 = data[jdx] & 0x07;
|
|
jdx++;
|
|
var d5 = data[jdx] >> 3;
|
|
var d3 = data[jdx] & 0x07;
|
|
jdx++;
|
|
var e5 = data[jdx] >> 3;
|
|
var e3 = data[jdx] & 0x07;
|
|
jdx++;
|
|
nibbles[idx + 0x00] = a5;
|
|
nibbles[idx + 0x33] = b5;
|
|
nibbles[idx + 0x66] = c5;
|
|
nibbles[idx + 0x99] = d5;
|
|
nibbles[idx + 0xcc] = e5;
|
|
nibbles[idx + 0x100] = a3 << 2 | (d3 & 0x4) >> 1 | (e3 & 0x4) >> 2;
|
|
nibbles[idx + 0x133] = b3 << 2 | (d3 & 0x2) | (e3 & 0x2) >> 1;
|
|
nibbles[idx + 0x166] = c3 << 2 | (d3 & 0x1) << 1 | (e3 & 0x1);
|
|
}
|
|
nibbles[0xff] = data[jdx] >> 3;
|
|
nibbles[0x199] = data[jdx] & 0x07;
|
|
|
|
let last = 0;
|
|
for (let idx = 0x199; idx >= 0x100; idx--) {
|
|
const val = nibbles[idx];
|
|
buf.push(_trans53[last ^ val]);
|
|
last = val;
|
|
}
|
|
for (let idx = 0x0; idx < 0x100; idx++) {
|
|
const val = nibbles[idx];
|
|
buf.push(_trans53[last ^ val]);
|
|
last = val;
|
|
}
|
|
buf.push(_trans53[last]);
|
|
|
|
buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB
|
|
|
|
/*
|
|
* Gap 3
|
|
*/
|
|
|
|
buf.push(0xff);
|
|
|
|
return buf;
|
|
}
|
|
|
|
export function readSector(drive: Drive, track: byte, sector: byte) {
|
|
let _sector = drive.format == 'po' ? _PO[sector] : _DO[sector];
|
|
let val, state = 0;
|
|
let idx = 0;
|
|
let retry = 0;
|
|
let cur = drive.tracks[track];
|
|
|
|
function _readNext() {
|
|
var result = cur[idx++];
|
|
if (idx >= cur.length) {
|
|
idx = 0;
|
|
retry++;
|
|
}
|
|
return result;
|
|
}
|
|
function _skipBytes(count: number) {
|
|
idx += count;
|
|
if (idx >= cur.length) {
|
|
idx %= cur.length;
|
|
retry++;
|
|
}
|
|
}
|
|
let t = 0, s = 0, v = 0, checkSum;
|
|
let data = [];
|
|
while (retry < 4) {
|
|
switch (state) {
|
|
case 0:
|
|
val = _readNext();
|
|
state = (val === 0xd5) ? 1 : 0;
|
|
break;
|
|
case 1:
|
|
val = _readNext();
|
|
state = (val === 0xaa) ? 2 : 0;
|
|
break;
|
|
case 2:
|
|
val = _readNext();
|
|
state = (val === 0x96) ? 3 : (val === 0xad ? 4 : 0);
|
|
break;
|
|
case 3: // Address
|
|
v = defourXfour(_readNext(), _readNext()); // Volume
|
|
t = defourXfour(_readNext(), _readNext());
|
|
s = defourXfour(_readNext(), _readNext());
|
|
checkSum = defourXfour(_readNext(), _readNext());
|
|
if (checkSum != (v ^ t ^ s)) {
|
|
debug('Invalid header checksum:', toHex(v), toHex(t), toHex(s), toHex(checkSum));
|
|
}
|
|
_skipBytes(3); // Skip footer
|
|
state = 0;
|
|
break;
|
|
case 4: // Data
|
|
if (s === _sector && t === track) {
|
|
var data2 = [];
|
|
var last = 0;
|
|
for (let jdx = 0x55; jdx >= 0; jdx--) {
|
|
val = detrans62[_readNext() - 0x80] ^ last;
|
|
data2[jdx] = val;
|
|
last = val;
|
|
}
|
|
for (let jdx = 0; jdx < 0x100; jdx++) {
|
|
val = detrans62[_readNext() - 0x80] ^ last;
|
|
data[jdx] = val;
|
|
last = val;
|
|
}
|
|
checkSum = detrans62[_readNext() - 0x80] ^ last;
|
|
if (checkSum) {
|
|
debug('Invalid data checksum:', toHex(v), toHex(t), toHex(s), toHex(checkSum));
|
|
}
|
|
for (let kdx = 0, jdx = 0x55; kdx < 0x100; kdx++) {
|
|
data[kdx] <<= 1;
|
|
if ((data2[jdx] & 0x01) !== 0) {
|
|
data[kdx] |= 0x01;
|
|
}
|
|
data2[jdx] >>= 1;
|
|
|
|
data[kdx] <<= 1;
|
|
if ((data2[jdx] & 0x01) !== 0) {
|
|
data[kdx] |= 0x01;
|
|
}
|
|
data2[jdx] >>= 1;
|
|
|
|
if (--jdx < 0) jdx = 0x55;
|
|
}
|
|
return data;
|
|
}
|
|
else
|
|
_skipBytes(0x159); // Skip data, checksum and footer
|
|
state = 0;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
export function jsonEncode(cur: Drive, pretty: boolean) {
|
|
// For 'nib', tracks are encoded as strings. For all other formats,
|
|
// tracks are arrays of sectors which are encoded as strings.
|
|
let data: string[] | string[][] = [];
|
|
let format = 'dsk';
|
|
for (let t = 0; t < cur.tracks.length; t++) {
|
|
data[t] = [];
|
|
if (cur.format === 'nib') {
|
|
format = 'nib';
|
|
data[t] = base64_encode(cur.tracks[t]);
|
|
} else {
|
|
for (var s = 0; s < 0x10; s++) {
|
|
(data[t] as string[])[s] = base64_encode(readSector(cur, t, s));
|
|
}
|
|
}
|
|
}
|
|
return JSON.stringify({
|
|
'type': format,
|
|
'encoding': 'base64',
|
|
'volume': cur.volume,
|
|
'data': data,
|
|
'readOnly': cur.readOnly,
|
|
}, undefined, pretty ? ' ' : undefined);
|
|
}
|
|
|
|
export function jsonDecode(data: string) {
|
|
let tracks: memory[] = [];
|
|
let json = JSON.parse(data);
|
|
let v = json.volume;
|
|
let readOnly = json.readOnly;
|
|
for (let t = 0; t < json.data.length; t++) {
|
|
let track: byte[] = [];
|
|
for (let s = 0; s < json.data[t].length; s++) {
|
|
let _s = 15 - s;
|
|
let sector: string = json.data[t][_s];
|
|
const d = base64_decode(sector);
|
|
track = track.concat(explodeSector16(v, t, s, d));
|
|
}
|
|
tracks[t] = bytify(track);
|
|
}
|
|
let cur: Drive = {
|
|
volume: v,
|
|
format: json.type,
|
|
tracks,
|
|
readOnly,
|
|
dirty: false,
|
|
};
|
|
|
|
return cur;
|
|
}
|