mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
Typescriptify ui/apple2.js
(#74)
This is mostly a mechanical change; there are still lots of things about `ui/apple2` that could be improved. The change also converts a few dependencies of `ui/apple2`, like `applesoft/compiler`. Besides the straight conversions, some other packages have changes to make all of the typing work out. Lastly, `@types/micromodal` has been added as a development dependency.
This commit is contained in:
parent
e3bbd2d640
commit
207bed3d27
@ -37,6 +37,11 @@ export interface Apple2Options {
|
|||||||
tick: () => void,
|
tick: () => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Stats {
|
||||||
|
frames: number,
|
||||||
|
renderedFrames: number,
|
||||||
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
cpu: CpuState,
|
cpu: CpuState,
|
||||||
vm: VideoModesState,
|
vm: VideoModesState,
|
||||||
@ -66,7 +71,7 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
|
|||||||
|
|
||||||
private tick: () => void;
|
private tick: () => void;
|
||||||
|
|
||||||
private stats = {
|
private stats: Stats = {
|
||||||
frames: 0,
|
frames: 0,
|
||||||
renderedFrames: 0
|
renderedFrames: 0
|
||||||
};
|
};
|
||||||
@ -215,7 +220,7 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
|
|||||||
this.cpu.reset();
|
this.cpu.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
getStats() {
|
getStats(): Stats {
|
||||||
return this.stats;
|
return this.stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,287 +0,0 @@
|
|||||||
export default function ApplesoftCompiler(mem)
|
|
||||||
{
|
|
||||||
var _mem = mem;
|
|
||||||
|
|
||||||
var LOMEM = 0x69;
|
|
||||||
var ARRAY_START = 0x6B;
|
|
||||||
var ARRAY_END = 0x6D;
|
|
||||||
var PROGRAM_START = 0x801;
|
|
||||||
|
|
||||||
var TOKENS = {
|
|
||||||
'END': 0x80,
|
|
||||||
'FOR': 0x81,
|
|
||||||
'NEXT': 0x82,
|
|
||||||
'DATA': 0x83,
|
|
||||||
'INPUT': 0x84,
|
|
||||||
'DEL': 0x85,
|
|
||||||
'DIM': 0x86,
|
|
||||||
'READ': 0x87,
|
|
||||||
'GR': 0x88,
|
|
||||||
'TEXT': 0x89,
|
|
||||||
'PR#': 0x8a,
|
|
||||||
'IN#': 0x8b,
|
|
||||||
'CALL': 0x8c,
|
|
||||||
'PLOT': 0x8d,
|
|
||||||
'HLIN': 0x8e,
|
|
||||||
'VLIN': 0x8f,
|
|
||||||
'HGR2': 0x90,
|
|
||||||
'HGR': 0x91,
|
|
||||||
'HCOLOR=': 0x92,
|
|
||||||
'HPLOT': 0x93,
|
|
||||||
'DRAW': 0x94,
|
|
||||||
'XDRAW': 0x95,
|
|
||||||
'HTAB': 0x96,
|
|
||||||
'HOME': 0x97,
|
|
||||||
'ROT=': 0x98,
|
|
||||||
'SCALE=': 0x99,
|
|
||||||
'SHLOAD': 0x9a,
|
|
||||||
'TRACE': 0x9b,
|
|
||||||
'NOTRACE': 0x9c,
|
|
||||||
'NORMAL': 0x9d,
|
|
||||||
'INVERSE': 0x9e,
|
|
||||||
'FLASH': 0x9f,
|
|
||||||
'COLOR=': 0xa0,
|
|
||||||
'POP=': 0xa1,
|
|
||||||
'VTAB': 0xa2,
|
|
||||||
'HIMEM:': 0xa3,
|
|
||||||
'LOMEM:': 0xa4,
|
|
||||||
'ONERR': 0xa5,
|
|
||||||
'RESUME': 0xa6,
|
|
||||||
'RECALL': 0xa7,
|
|
||||||
'STORE': 0xa8,
|
|
||||||
'SPEED=': 0xa9,
|
|
||||||
'LET': 0xaa,
|
|
||||||
'GOTO': 0xab,
|
|
||||||
'RUN': 0xac,
|
|
||||||
'IF': 0xad,
|
|
||||||
'RESTORE': 0xae,
|
|
||||||
'&': 0xaf,
|
|
||||||
'GOSUB': 0xb0,
|
|
||||||
'RETURN': 0xb1,
|
|
||||||
'REM': 0xb2,
|
|
||||||
'STOP': 0xb3,
|
|
||||||
'ON': 0xb4,
|
|
||||||
'WAIT': 0xb5,
|
|
||||||
'LOAD': 0xb6,
|
|
||||||
'SAVE': 0xb7,
|
|
||||||
'DEF': 0xb8,
|
|
||||||
'POKE': 0xb9,
|
|
||||||
'PRINT': 0xba,
|
|
||||||
'CONT': 0xbb,
|
|
||||||
'LIST': 0xbc,
|
|
||||||
'CLEAR': 0xbd,
|
|
||||||
'GET': 0xbe,
|
|
||||||
'NEW': 0xbf,
|
|
||||||
'TAB(': 0xc0,
|
|
||||||
'TO': 0xc1,
|
|
||||||
'FN': 0xc2,
|
|
||||||
'SPC(': 0xc3,
|
|
||||||
'THEN': 0xc4,
|
|
||||||
'AT': 0xc5,
|
|
||||||
'NOT': 0xc6,
|
|
||||||
'STEP': 0xc7,
|
|
||||||
'+': 0xc8,
|
|
||||||
'-': 0xc9,
|
|
||||||
'*': 0xca,
|
|
||||||
'/': 0xcb,
|
|
||||||
'^': 0xcc,
|
|
||||||
'AND': 0xcd,
|
|
||||||
'OR': 0xce,
|
|
||||||
'>': 0xcf,
|
|
||||||
'=': 0xd0,
|
|
||||||
'<': 0xd1,
|
|
||||||
'SGN': 0xd2,
|
|
||||||
'INT': 0xd3,
|
|
||||||
'ABS': 0xd4,
|
|
||||||
'USR': 0xd5,
|
|
||||||
'FRE': 0xd6,
|
|
||||||
'SCRN(': 0xd7,
|
|
||||||
'PDL': 0xd8,
|
|
||||||
'POS': 0xd9,
|
|
||||||
'SQR': 0xda,
|
|
||||||
'RND': 0xdb,
|
|
||||||
'LOG': 0xdc,
|
|
||||||
'EXP': 0xdd,
|
|
||||||
'COS': 0xde,
|
|
||||||
'SIN': 0xdf,
|
|
||||||
'TAN': 0xe0,
|
|
||||||
'ATN': 0xe1,
|
|
||||||
'PEEK': 0xe2,
|
|
||||||
'LEN': 0xe3,
|
|
||||||
'STR$': 0xe4,
|
|
||||||
'VAL': 0xe5,
|
|
||||||
'ASC': 0xe6,
|
|
||||||
'CHR$': 0xe7,
|
|
||||||
'LEFT$': 0xe8,
|
|
||||||
'RIGHT$': 0xe9,
|
|
||||||
'MID$': 0xea
|
|
||||||
};
|
|
||||||
|
|
||||||
var STATES = {
|
|
||||||
NORMAL: 0,
|
|
||||||
STRING: 1,
|
|
||||||
COMMENT: 2,
|
|
||||||
DATA: 3
|
|
||||||
};
|
|
||||||
|
|
||||||
function writeByte(addr, val) {
|
|
||||||
var page = addr >> 8,
|
|
||||||
off = addr & 0xff;
|
|
||||||
|
|
||||||
return _mem.write(page, off, val);
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeWord(addr, val) {
|
|
||||||
var lsb = val & 0xff;
|
|
||||||
var msb = val >> 8;
|
|
||||||
|
|
||||||
writeByte(addr, lsb);
|
|
||||||
writeByte(addr + 1, msb);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
compile: function(program) {
|
|
||||||
var lineNos = {};
|
|
||||||
|
|
||||||
function compileLine(line, offset) {
|
|
||||||
if (!line) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
var state = STATES.NORMAL;
|
|
||||||
var result = [0, 0, 0, 0];
|
|
||||||
var curChar = 0;
|
|
||||||
var character;
|
|
||||||
var lineNoStr = '';
|
|
||||||
|
|
||||||
while (line.length) {
|
|
||||||
character = line.charAt(curChar);
|
|
||||||
if (/\d/.test(character)) {
|
|
||||||
lineNoStr += character;
|
|
||||||
curChar++;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (curChar < line.length) {
|
|
||||||
character = line.charAt(curChar).toUpperCase();
|
|
||||||
switch (state) {
|
|
||||||
case STATES.NORMAL:
|
|
||||||
if (character !== ' ') {
|
|
||||||
if (character === '"') {
|
|
||||||
result.push(character.charCodeAt(0));
|
|
||||||
state = STATES.STRING;
|
|
||||||
curChar++;
|
|
||||||
} else {
|
|
||||||
var foundToken = '';
|
|
||||||
for (var possibleToken in TOKENS) {
|
|
||||||
if (possibleToken.charAt(0) == character) {
|
|
||||||
var tokenIdx = curChar + 1;
|
|
||||||
var idx = 1;
|
|
||||||
while (idx < possibleToken.length) {
|
|
||||||
if (line.charAt(tokenIdx) !== ' ') {
|
|
||||||
if (line.charAt(tokenIdx).toUpperCase() !== possibleToken.charAt(idx)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
tokenIdx++;
|
|
||||||
}
|
|
||||||
if (idx === possibleToken.length) {
|
|
||||||
// Found a token
|
|
||||||
if (possibleToken === 'AT') {
|
|
||||||
var lookAhead = line.charAt(tokenIdx + 1).toUpperCase();
|
|
||||||
// ATN takes precedence over AT
|
|
||||||
if (lookAhead === 'N') {
|
|
||||||
foundToken = 'ATN';
|
|
||||||
tokenIdx++;
|
|
||||||
}
|
|
||||||
// TO takes precedence over AT
|
|
||||||
if (lookAhead === 'O') {
|
|
||||||
result.push(lookAhead.charCodeAt(0));
|
|
||||||
foundToken = 'TO';
|
|
||||||
tokenIdx++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foundToken = possibleToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (foundToken) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (foundToken) {
|
|
||||||
result.push(TOKENS[foundToken]);
|
|
||||||
curChar = tokenIdx;
|
|
||||||
if (foundToken === 'REM') {
|
|
||||||
state = STATES.COMMENT;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.push(character.charCodeAt(0));
|
|
||||||
curChar++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
curChar++;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case STATES.COMMENT:
|
|
||||||
result.push(character.charCodeAt(0));
|
|
||||||
curChar++;
|
|
||||||
break;
|
|
||||||
case STATES.STRING:
|
|
||||||
result.push(character.charCodeAt(0));
|
|
||||||
if (character == '"') {
|
|
||||||
state = STATES.NORMAL;
|
|
||||||
}
|
|
||||||
curChar++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lineNoStr.length) {
|
|
||||||
var lineNo = parseInt(lineNoStr, 10);
|
|
||||||
if (lineNo < 0 || lineNo > 65535) {
|
|
||||||
throw new Error('Line number out of range');
|
|
||||||
}
|
|
||||||
if (lineNos[lineNoStr]) {
|
|
||||||
throw new Error('Duplicate line number');
|
|
||||||
}
|
|
||||||
lineNos[lineNoStr] = result;
|
|
||||||
|
|
||||||
// Next line pointer
|
|
||||||
result.push(0);
|
|
||||||
var nextLine = offset + result.length;
|
|
||||||
result[0] = nextLine & 0xff;
|
|
||||||
result[1] = nextLine >> 8;
|
|
||||||
|
|
||||||
// Line number
|
|
||||||
result[2] = lineNo & 0xff;
|
|
||||||
result[3] = lineNo >> 8;
|
|
||||||
} else {
|
|
||||||
throw new Error('Missing line number');
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
var compiled = [];
|
|
||||||
var lines = program.split(/[\r\n]+/g);
|
|
||||||
|
|
||||||
while (lines.length) {
|
|
||||||
var line = lines.shift();
|
|
||||||
var compiledLine = compileLine(line, PROGRAM_START + compiled.length);
|
|
||||||
compiled = compiled.concat(compiledLine);
|
|
||||||
}
|
|
||||||
compiled.push(0, 0);
|
|
||||||
|
|
||||||
for (var idx = 0; idx < compiled.length; idx++) {
|
|
||||||
writeByte(PROGRAM_START + idx, compiled[idx]);
|
|
||||||
}
|
|
||||||
writeWord(LOMEM, PROGRAM_START + compiled.length);
|
|
||||||
writeWord(ARRAY_START, PROGRAM_START + compiled.length);
|
|
||||||
writeWord(ARRAY_END, PROGRAM_START + compiled.length);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
288
js/applesoft/compiler.ts
Normal file
288
js/applesoft/compiler.ts
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
import { byte, KnownKeys, KnownValues, Memory, word } from '../types';
|
||||||
|
|
||||||
|
/** Map from keyword to token. */
|
||||||
|
const TOKENS = {
|
||||||
|
'END': 0x80,
|
||||||
|
'FOR': 0x81,
|
||||||
|
'NEXT': 0x82,
|
||||||
|
'DATA': 0x83,
|
||||||
|
'INPUT': 0x84,
|
||||||
|
'DEL': 0x85,
|
||||||
|
'DIM': 0x86,
|
||||||
|
'READ': 0x87,
|
||||||
|
'GR': 0x88,
|
||||||
|
'TEXT': 0x89,
|
||||||
|
'PR#': 0x8a,
|
||||||
|
'IN#': 0x8b,
|
||||||
|
'CALL': 0x8c,
|
||||||
|
'PLOT': 0x8d,
|
||||||
|
'HLIN': 0x8e,
|
||||||
|
'VLIN': 0x8f,
|
||||||
|
'HGR2': 0x90,
|
||||||
|
'HGR': 0x91,
|
||||||
|
'HCOLOR=': 0x92,
|
||||||
|
'HPLOT': 0x93,
|
||||||
|
'DRAW': 0x94,
|
||||||
|
'XDRAW': 0x95,
|
||||||
|
'HTAB': 0x96,
|
||||||
|
'HOME': 0x97,
|
||||||
|
'ROT=': 0x98,
|
||||||
|
'SCALE=': 0x99,
|
||||||
|
'SHLOAD': 0x9a,
|
||||||
|
'TRACE': 0x9b,
|
||||||
|
'NOTRACE': 0x9c,
|
||||||
|
'NORMAL': 0x9d,
|
||||||
|
'INVERSE': 0x9e,
|
||||||
|
'FLASH': 0x9f,
|
||||||
|
'COLOR=': 0xa0,
|
||||||
|
'POP=': 0xa1,
|
||||||
|
'VTAB': 0xa2,
|
||||||
|
'HIMEM:': 0xa3,
|
||||||
|
'LOMEM:': 0xa4,
|
||||||
|
'ONERR': 0xa5,
|
||||||
|
'RESUME': 0xa6,
|
||||||
|
'RECALL': 0xa7,
|
||||||
|
'STORE': 0xa8,
|
||||||
|
'SPEED=': 0xa9,
|
||||||
|
'LET': 0xaa,
|
||||||
|
'GOTO': 0xab,
|
||||||
|
'RUN': 0xac,
|
||||||
|
'IF': 0xad,
|
||||||
|
'RESTORE': 0xae,
|
||||||
|
'&': 0xaf,
|
||||||
|
'GOSUB': 0xb0,
|
||||||
|
'RETURN': 0xb1,
|
||||||
|
'REM': 0xb2,
|
||||||
|
'STOP': 0xb3,
|
||||||
|
'ON': 0xb4,
|
||||||
|
'WAIT': 0xb5,
|
||||||
|
'LOAD': 0xb6,
|
||||||
|
'SAVE': 0xb7,
|
||||||
|
'DEF': 0xb8,
|
||||||
|
'POKE': 0xb9,
|
||||||
|
'PRINT': 0xba,
|
||||||
|
'CONT': 0xbb,
|
||||||
|
'LIST': 0xbc,
|
||||||
|
'CLEAR': 0xbd,
|
||||||
|
'GET': 0xbe,
|
||||||
|
'NEW': 0xbf,
|
||||||
|
'TAB(': 0xc0,
|
||||||
|
'TO': 0xc1,
|
||||||
|
'FN': 0xc2,
|
||||||
|
'SPC(': 0xc3,
|
||||||
|
'THEN': 0xc4,
|
||||||
|
'AT': 0xc5,
|
||||||
|
'NOT': 0xc6,
|
||||||
|
'STEP': 0xc7,
|
||||||
|
'+': 0xc8,
|
||||||
|
'-': 0xc9,
|
||||||
|
'*': 0xca,
|
||||||
|
'/': 0xcb,
|
||||||
|
'^': 0xcc,
|
||||||
|
'AND': 0xcd,
|
||||||
|
'OR': 0xce,
|
||||||
|
'>': 0xcf,
|
||||||
|
'=': 0xd0,
|
||||||
|
'<': 0xd1,
|
||||||
|
'SGN': 0xd2,
|
||||||
|
'INT': 0xd3,
|
||||||
|
'ABS': 0xd4,
|
||||||
|
'USR': 0xd5,
|
||||||
|
'FRE': 0xd6,
|
||||||
|
'SCRN(': 0xd7,
|
||||||
|
'PDL': 0xd8,
|
||||||
|
'POS': 0xd9,
|
||||||
|
'SQR': 0xda,
|
||||||
|
'RND': 0xdb,
|
||||||
|
'LOG': 0xdc,
|
||||||
|
'EXP': 0xdd,
|
||||||
|
'COS': 0xde,
|
||||||
|
'SIN': 0xdf,
|
||||||
|
'TAN': 0xe0,
|
||||||
|
'ATN': 0xe1,
|
||||||
|
'PEEK': 0xe2,
|
||||||
|
'LEN': 0xe3,
|
||||||
|
'STR$': 0xe4,
|
||||||
|
'VAL': 0xe5,
|
||||||
|
'ASC': 0xe6,
|
||||||
|
'CHR$': 0xe7,
|
||||||
|
'LEFT$': 0xe8,
|
||||||
|
'RIGHT$': 0xe9,
|
||||||
|
'MID$': 0xea
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const LOMEM = 0x69;
|
||||||
|
const ARRAY_START = 0x6B;
|
||||||
|
const ARRAY_END = 0x6D;
|
||||||
|
const PROGRAM_START = 0x801;
|
||||||
|
|
||||||
|
const STATES = {
|
||||||
|
NORMAL: 0,
|
||||||
|
STRING: 1,
|
||||||
|
COMMENT: 2,
|
||||||
|
DATA: 3
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default class ApplesoftCompiler {
|
||||||
|
constructor(private mem: Memory) { }
|
||||||
|
|
||||||
|
private writeByte(addr: word, val: byte) {
|
||||||
|
const page = addr >> 8;
|
||||||
|
const off = addr & 0xff;
|
||||||
|
|
||||||
|
return this.mem.write(page, off, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeWord(addr: word, val: byte) {
|
||||||
|
const lsb = val & 0xff;
|
||||||
|
const msb = val >> 8;
|
||||||
|
|
||||||
|
this.writeByte(addr, lsb);
|
||||||
|
this.writeByte(addr + 1, msb);
|
||||||
|
}
|
||||||
|
|
||||||
|
compile(program: string) {
|
||||||
|
const lineNos: { [line: string]: byte[]} = {};
|
||||||
|
|
||||||
|
function compileLine(line: string | null | undefined, offset: number) {
|
||||||
|
if (!line) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let state: KnownValues<typeof STATES> = STATES.NORMAL;
|
||||||
|
const result = [0, 0, 0, 0];
|
||||||
|
let curChar = 0;
|
||||||
|
let character;
|
||||||
|
let lineNoStr = '';
|
||||||
|
|
||||||
|
while (line.length) {
|
||||||
|
character = line.charAt(curChar);
|
||||||
|
if (/\d/.test(character)) {
|
||||||
|
lineNoStr += character;
|
||||||
|
curChar++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (curChar < line.length) {
|
||||||
|
character = line.charAt(curChar).toUpperCase();
|
||||||
|
switch (state) {
|
||||||
|
case STATES.NORMAL:
|
||||||
|
if (character !== ' ') {
|
||||||
|
if (character === '"') {
|
||||||
|
result.push(character.charCodeAt(0));
|
||||||
|
state = STATES.STRING;
|
||||||
|
curChar++;
|
||||||
|
} else {
|
||||||
|
let foundToken = '';
|
||||||
|
let tokenIdx = -1;
|
||||||
|
for (const possibleToken in TOKENS) {
|
||||||
|
if (possibleToken.charAt(0) == character) {
|
||||||
|
tokenIdx = curChar + 1;
|
||||||
|
let idx = 1;
|
||||||
|
while (idx < possibleToken.length) {
|
||||||
|
if (line.charAt(tokenIdx) !== ' ') {
|
||||||
|
if (line.charAt(tokenIdx).toUpperCase() !== possibleToken.charAt(idx)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
tokenIdx++;
|
||||||
|
}
|
||||||
|
if (idx === possibleToken.length) {
|
||||||
|
// Found a token
|
||||||
|
if (possibleToken === 'AT') {
|
||||||
|
const lookAhead = line.charAt(tokenIdx + 1).toUpperCase();
|
||||||
|
// ATN takes precedence over AT
|
||||||
|
if (lookAhead === 'N') {
|
||||||
|
foundToken = 'ATN';
|
||||||
|
tokenIdx++;
|
||||||
|
}
|
||||||
|
// TO takes precedence over AT
|
||||||
|
if (lookAhead === 'O') {
|
||||||
|
result.push(lookAhead.charCodeAt(0));
|
||||||
|
foundToken = 'TO';
|
||||||
|
tokenIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foundToken = possibleToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundToken) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundToken) {
|
||||||
|
result.push(TOKENS[foundToken as KnownKeys<typeof TOKENS>]);
|
||||||
|
curChar = tokenIdx;
|
||||||
|
if (foundToken === 'REM') {
|
||||||
|
state = STATES.COMMENT;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push(character.charCodeAt(0));
|
||||||
|
curChar++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
curChar++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case STATES.COMMENT:
|
||||||
|
result.push(character.charCodeAt(0));
|
||||||
|
curChar++;
|
||||||
|
break;
|
||||||
|
case STATES.STRING:
|
||||||
|
result.push(character.charCodeAt(0));
|
||||||
|
if (character == '"') {
|
||||||
|
state = STATES.NORMAL;
|
||||||
|
}
|
||||||
|
curChar++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineNoStr.length) {
|
||||||
|
const lineNo = parseInt(lineNoStr, 10);
|
||||||
|
if (lineNo < 0 || lineNo > 65535) {
|
||||||
|
throw new Error('Line number out of range');
|
||||||
|
}
|
||||||
|
if (lineNos[lineNoStr]) {
|
||||||
|
throw new Error('Duplicate line number');
|
||||||
|
}
|
||||||
|
lineNos[lineNoStr] = result;
|
||||||
|
|
||||||
|
// Next line pointer
|
||||||
|
result.push(0);
|
||||||
|
const nextLine = offset + result.length;
|
||||||
|
result[0] = nextLine & 0xff;
|
||||||
|
result[1] = nextLine >> 8;
|
||||||
|
|
||||||
|
// Line number
|
||||||
|
result[2] = lineNo & 0xff;
|
||||||
|
result[3] = lineNo >> 8;
|
||||||
|
} else {
|
||||||
|
throw new Error('Missing line number');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let compiled: number[] = [];
|
||||||
|
const lines = program.split(/[\r\n]+/g);
|
||||||
|
|
||||||
|
while (lines.length) {
|
||||||
|
const line = lines.shift();
|
||||||
|
const compiledLine = compileLine(line, PROGRAM_START + compiled.length);
|
||||||
|
compiled = compiled.concat(compiledLine);
|
||||||
|
}
|
||||||
|
compiled.push(0, 0);
|
||||||
|
|
||||||
|
for (let idx = 0; idx < compiled.length; idx++) {
|
||||||
|
this.writeByte(PROGRAM_START + idx, compiled[idx]);
|
||||||
|
}
|
||||||
|
this.writeWord(LOMEM, PROGRAM_START + compiled.length);
|
||||||
|
this.writeWord(ARRAY_START, PROGRAM_START + compiled.length);
|
||||||
|
this.writeWord(ARRAY_END, PROGRAM_START + compiled.length);
|
||||||
|
}
|
||||||
|
}
|
@ -1,173 +0,0 @@
|
|||||||
export default function ApplesoftDump(mem)
|
|
||||||
{
|
|
||||||
var _mem = mem;
|
|
||||||
|
|
||||||
var LETTERS =
|
|
||||||
' ' +
|
|
||||||
' !"#$%&\'()*+,-./0123456789:;<=>?' +
|
|
||||||
'@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_' +
|
|
||||||
'`abcdefghijklmnopqrstuvwxyz{|}~ ';
|
|
||||||
var TOKENS = {
|
|
||||||
0x80: 'END',
|
|
||||||
0x81: 'FOR',
|
|
||||||
0x82: 'NEXT',
|
|
||||||
0x83: 'DATA',
|
|
||||||
0x84: 'INPUT',
|
|
||||||
0x85: 'DEL',
|
|
||||||
0x86: 'DIM',
|
|
||||||
0x87: 'READ',
|
|
||||||
0x88: 'GR',
|
|
||||||
0x89: 'TEXT',
|
|
||||||
0x8a: 'PR#',
|
|
||||||
0x8b: 'IN#',
|
|
||||||
0x8c: 'CALL',
|
|
||||||
0x8d: 'PLOT',
|
|
||||||
0x8e: 'HLIN',
|
|
||||||
0x8f: 'VLIN',
|
|
||||||
0x90: 'HGR2',
|
|
||||||
0x91: 'HGR',
|
|
||||||
0x92: 'HCOLOR=',
|
|
||||||
0x93: 'HPLOT',
|
|
||||||
0x94: 'DRAW',
|
|
||||||
0x95: 'XDRAW',
|
|
||||||
0x96: 'HTAB',
|
|
||||||
0x97: 'HOME',
|
|
||||||
0x98: 'ROT=',
|
|
||||||
0x99: 'SCALE=',
|
|
||||||
0x9a: 'SHLOAD',
|
|
||||||
0x9b: 'TRACE',
|
|
||||||
0x9c: 'NOTRACE',
|
|
||||||
0x9d: 'NORMAL',
|
|
||||||
0x9e: 'INVERSE',
|
|
||||||
0x9f: 'FLASH',
|
|
||||||
0xa0: 'COLOR=',
|
|
||||||
0xa1: 'POP=',
|
|
||||||
0xa2: 'VTAB',
|
|
||||||
0xa3: 'HIMEM:',
|
|
||||||
0xa4: 'LOMEM:',
|
|
||||||
0xa5: 'ONERR',
|
|
||||||
0xa6: 'RESUME',
|
|
||||||
0xa7: 'RECALL',
|
|
||||||
0xa8: 'STORE',
|
|
||||||
0xa9: 'SPEED=',
|
|
||||||
0xaa: 'LET',
|
|
||||||
0xab: 'GOTO',
|
|
||||||
0xac: 'RUN',
|
|
||||||
0xad: 'IF',
|
|
||||||
0xae: 'RESTORE',
|
|
||||||
0xaf: '&',
|
|
||||||
0xb0: 'GOSUB',
|
|
||||||
0xb1: 'RETURN',
|
|
||||||
0xb2: 'REM',
|
|
||||||
0xb3: 'STOP',
|
|
||||||
0xb4: 'ON',
|
|
||||||
0xb5: 'WAIT',
|
|
||||||
0xb6: 'LOAD',
|
|
||||||
0xb7: 'SAVE',
|
|
||||||
0xb8: 'DEF',
|
|
||||||
0xb9: 'POKE',
|
|
||||||
0xba: 'PRINT',
|
|
||||||
0xbb: 'CONT',
|
|
||||||
0xbc: 'LIST',
|
|
||||||
0xbd: 'CLEAR',
|
|
||||||
0xbe: 'GET',
|
|
||||||
0xbf: 'NEW',
|
|
||||||
0xc0: 'TAB(',
|
|
||||||
0xc1: 'TO',
|
|
||||||
0xc2: 'FN',
|
|
||||||
0xc3: 'SPC(',
|
|
||||||
0xc4: 'THEN',
|
|
||||||
0xc5: 'AT',
|
|
||||||
0xc6: 'NOT',
|
|
||||||
0xc7: 'STEP',
|
|
||||||
0xc8: '+',
|
|
||||||
0xc9: '-',
|
|
||||||
0xca: '*',
|
|
||||||
0xcb: '/',
|
|
||||||
0xcc: '^',
|
|
||||||
0xcd: 'AND',
|
|
||||||
0xce: 'OR',
|
|
||||||
0xcf: '>',
|
|
||||||
0xd0: '=',
|
|
||||||
0xd1: '<',
|
|
||||||
0xd2: 'SGN',
|
|
||||||
0xd3: 'INT',
|
|
||||||
0xd4: 'ABS',
|
|
||||||
0xd5: 'USR',
|
|
||||||
0xd6: 'FRE',
|
|
||||||
0xd7: 'SCRN(',
|
|
||||||
0xd8: 'PDL',
|
|
||||||
0xd9: 'POS',
|
|
||||||
0xda: 'SQR',
|
|
||||||
0xdb: 'RND',
|
|
||||||
0xdc: 'LOG',
|
|
||||||
0xdd: 'EXP',
|
|
||||||
0xde: 'COS',
|
|
||||||
0xdf: 'SIN',
|
|
||||||
0xe0: 'TAN',
|
|
||||||
0xe1: 'ATN',
|
|
||||||
0xe2: 'PEEK',
|
|
||||||
0xe3: 'LEN',
|
|
||||||
0xe4: 'STR$',
|
|
||||||
0xe5: 'VAL',
|
|
||||||
0xe6: 'ASC',
|
|
||||||
0xe7: 'CHR$',
|
|
||||||
0xe8: 'LEFT$',
|
|
||||||
0xe9: 'RIGHT$',
|
|
||||||
0xea: 'MID$'
|
|
||||||
};
|
|
||||||
|
|
||||||
function readByte(addr) {
|
|
||||||
var page = addr >> 8,
|
|
||||||
off = addr & 0xff;
|
|
||||||
|
|
||||||
return _mem.read(page, off);
|
|
||||||
}
|
|
||||||
|
|
||||||
function readWord(addr) {
|
|
||||||
var lsb, msb;
|
|
||||||
|
|
||||||
lsb = readByte(addr);
|
|
||||||
msb = readByte(addr + 1);
|
|
||||||
|
|
||||||
return (msb << 8) | lsb;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
toString: function () {
|
|
||||||
var str = '';
|
|
||||||
var start = readWord(0x67); // Start
|
|
||||||
var end = readWord(0xaf); // End of program
|
|
||||||
var addr = start;
|
|
||||||
do {
|
|
||||||
var line = '';
|
|
||||||
var next = readWord(addr);
|
|
||||||
addr += 2;
|
|
||||||
var lineno = readWord(addr);
|
|
||||||
addr += 2;
|
|
||||||
|
|
||||||
line += lineno;
|
|
||||||
line += ' ';
|
|
||||||
var val = false;
|
|
||||||
do {
|
|
||||||
if (addr < start || addr > end)
|
|
||||||
return str;
|
|
||||||
|
|
||||||
val = readByte(addr++);
|
|
||||||
if (val >= 0x80) {
|
|
||||||
line += ' ';
|
|
||||||
line += TOKENS[val];
|
|
||||||
line += ' ';
|
|
||||||
}
|
|
||||||
else
|
|
||||||
line += LETTERS[val];
|
|
||||||
} while (val);
|
|
||||||
line += '\n';
|
|
||||||
str += line;
|
|
||||||
addr = next;
|
|
||||||
} while (addr && addr >= start && addr < end);
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
171
js/applesoft/decompiler.ts
Normal file
171
js/applesoft/decompiler.ts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import { byte, KnownKeys, Memory, word } from '../types';
|
||||||
|
|
||||||
|
const LETTERS =
|
||||||
|
' ' +
|
||||||
|
' !"#$%&\'()*+,-./0123456789:;<=>?' +
|
||||||
|
'@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_' +
|
||||||
|
'`abcdefghijklmnopqrstuvwxyz{|}~ ';
|
||||||
|
|
||||||
|
const TOKENS = {
|
||||||
|
0x80: 'END',
|
||||||
|
0x81: 'FOR',
|
||||||
|
0x82: 'NEXT',
|
||||||
|
0x83: 'DATA',
|
||||||
|
0x84: 'INPUT',
|
||||||
|
0x85: 'DEL',
|
||||||
|
0x86: 'DIM',
|
||||||
|
0x87: 'READ',
|
||||||
|
0x88: 'GR',
|
||||||
|
0x89: 'TEXT',
|
||||||
|
0x8a: 'PR#',
|
||||||
|
0x8b: 'IN#',
|
||||||
|
0x8c: 'CALL',
|
||||||
|
0x8d: 'PLOT',
|
||||||
|
0x8e: 'HLIN',
|
||||||
|
0x8f: 'VLIN',
|
||||||
|
0x90: 'HGR2',
|
||||||
|
0x91: 'HGR',
|
||||||
|
0x92: 'HCOLOR=',
|
||||||
|
0x93: 'HPLOT',
|
||||||
|
0x94: 'DRAW',
|
||||||
|
0x95: 'XDRAW',
|
||||||
|
0x96: 'HTAB',
|
||||||
|
0x97: 'HOME',
|
||||||
|
0x98: 'ROT=',
|
||||||
|
0x99: 'SCALE=',
|
||||||
|
0x9a: 'SHLOAD',
|
||||||
|
0x9b: 'TRACE',
|
||||||
|
0x9c: 'NOTRACE',
|
||||||
|
0x9d: 'NORMAL',
|
||||||
|
0x9e: 'INVERSE',
|
||||||
|
0x9f: 'FLASH',
|
||||||
|
0xa0: 'COLOR=',
|
||||||
|
0xa1: 'POP=',
|
||||||
|
0xa2: 'VTAB',
|
||||||
|
0xa3: 'HIMEM:',
|
||||||
|
0xa4: 'LOMEM:',
|
||||||
|
0xa5: 'ONERR',
|
||||||
|
0xa6: 'RESUME',
|
||||||
|
0xa7: 'RECALL',
|
||||||
|
0xa8: 'STORE',
|
||||||
|
0xa9: 'SPEED=',
|
||||||
|
0xaa: 'LET',
|
||||||
|
0xab: 'GOTO',
|
||||||
|
0xac: 'RUN',
|
||||||
|
0xad: 'IF',
|
||||||
|
0xae: 'RESTORE',
|
||||||
|
0xaf: '&',
|
||||||
|
0xb0: 'GOSUB',
|
||||||
|
0xb1: 'RETURN',
|
||||||
|
0xb2: 'REM',
|
||||||
|
0xb3: 'STOP',
|
||||||
|
0xb4: 'ON',
|
||||||
|
0xb5: 'WAIT',
|
||||||
|
0xb6: 'LOAD',
|
||||||
|
0xb7: 'SAVE',
|
||||||
|
0xb8: 'DEF',
|
||||||
|
0xb9: 'POKE',
|
||||||
|
0xba: 'PRINT',
|
||||||
|
0xbb: 'CONT',
|
||||||
|
0xbc: 'LIST',
|
||||||
|
0xbd: 'CLEAR',
|
||||||
|
0xbe: 'GET',
|
||||||
|
0xbf: 'NEW',
|
||||||
|
0xc0: 'TAB(',
|
||||||
|
0xc1: 'TO',
|
||||||
|
0xc2: 'FN',
|
||||||
|
0xc3: 'SPC(',
|
||||||
|
0xc4: 'THEN',
|
||||||
|
0xc5: 'AT',
|
||||||
|
0xc6: 'NOT',
|
||||||
|
0xc7: 'STEP',
|
||||||
|
0xc8: '+',
|
||||||
|
0xc9: '-',
|
||||||
|
0xca: '*',
|
||||||
|
0xcb: '/',
|
||||||
|
0xcc: '^',
|
||||||
|
0xcd: 'AND',
|
||||||
|
0xce: 'OR',
|
||||||
|
0xcf: '>',
|
||||||
|
0xd0: '=',
|
||||||
|
0xd1: '<',
|
||||||
|
0xd2: 'SGN',
|
||||||
|
0xd3: 'INT',
|
||||||
|
0xd4: 'ABS',
|
||||||
|
0xd5: 'USR',
|
||||||
|
0xd6: 'FRE',
|
||||||
|
0xd7: 'SCRN(',
|
||||||
|
0xd8: 'PDL',
|
||||||
|
0xd9: 'POS',
|
||||||
|
0xda: 'SQR',
|
||||||
|
0xdb: 'RND',
|
||||||
|
0xdc: 'LOG',
|
||||||
|
0xdd: 'EXP',
|
||||||
|
0xde: 'COS',
|
||||||
|
0xdf: 'SIN',
|
||||||
|
0xe0: 'TAN',
|
||||||
|
0xe1: 'ATN',
|
||||||
|
0xe2: 'PEEK',
|
||||||
|
0xe3: 'LEN',
|
||||||
|
0xe4: 'STR$',
|
||||||
|
0xe5: 'VAL',
|
||||||
|
0xe6: 'ASC',
|
||||||
|
0xe7: 'CHR$',
|
||||||
|
0xe8: 'LEFT$',
|
||||||
|
0xe9: 'RIGHT$',
|
||||||
|
0xea: 'MID$'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default class ApplesoftDump {
|
||||||
|
constructor(private mem: Memory) { }
|
||||||
|
|
||||||
|
private readByte(addr: word): byte {
|
||||||
|
const page = addr >> 8;
|
||||||
|
const off = addr & 0xff;
|
||||||
|
|
||||||
|
return this.mem.read(page, off);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readWord(addr: word): word {
|
||||||
|
const lsb = this.readByte(addr);
|
||||||
|
const msb = this.readByte(addr + 1);
|
||||||
|
|
||||||
|
return (msb << 8) | lsb;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
let str = '';
|
||||||
|
const start = this.readWord(0x67); // Start
|
||||||
|
const end = this.readWord(0xaf); // End of program
|
||||||
|
let addr = start;
|
||||||
|
do {
|
||||||
|
let line = '';
|
||||||
|
const next = this.readWord(addr);
|
||||||
|
addr += 2;
|
||||||
|
const lineno = this.readWord(addr);
|
||||||
|
addr += 2;
|
||||||
|
|
||||||
|
line += lineno;
|
||||||
|
line += ' ';
|
||||||
|
let val = 0;
|
||||||
|
do {
|
||||||
|
if (addr < start || addr > end)
|
||||||
|
return str;
|
||||||
|
|
||||||
|
val = this.readByte(addr++);
|
||||||
|
if (val >= 0x80) {
|
||||||
|
line += ' ';
|
||||||
|
line += TOKENS[val as KnownKeys<typeof TOKENS>];
|
||||||
|
line += ' ';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
line += LETTERS[val];
|
||||||
|
} while (val);
|
||||||
|
line += '\n';
|
||||||
|
str += line;
|
||||||
|
addr = next;
|
||||||
|
} while (addr && addr >= start && addr < end);
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
@ -787,7 +787,7 @@ export default class DiskII implements Card {
|
|||||||
this.callbacks.label(drive, name);
|
this.callbacks.label(drive, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
getJSON(drive: DriveNumber, pretty: boolean) {
|
getJSON(drive: DriveNumber, pretty: boolean = false) {
|
||||||
const cur = this.drives[drive - 1];
|
const cur = this.drives[drive - 1];
|
||||||
if (!isNibbleDrive(cur)) {
|
if (!isNibbleDrive(cur)) {
|
||||||
throw new Error('Can\'t save WOZ disks to JSON');
|
throw new Error('Can\'t save WOZ disks to JSON');
|
||||||
@ -801,7 +801,7 @@ export default class DiskII implements Card {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setBinary(drive: DriveNumber, name: string, fmt: DiskFormat, rawData: memory) {
|
setBinary(drive: DriveNumber, name: string, fmt: DiskFormat, rawData: ArrayBuffer) {
|
||||||
let disk;
|
let disk;
|
||||||
const cur = this.drives[drive - 1];
|
const cur = this.drives[drive - 1];
|
||||||
const readOnly = false;
|
const readOnly = false;
|
||||||
|
@ -456,6 +456,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
|
|||||||
for (let idx = 0; idx < data.byteLength; idx += 512) {
|
for (let idx = 0; idx < data.byteLength; idx += 512) {
|
||||||
this.disks[drive].push(new Uint8Array(data.slice(idx, idx + 512)));
|
this.disks[drive].push(new Uint8Array(data.slice(idx, idx + 512)));
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setDisk(drive: number, json: BlockDevice) {
|
setDisk(drive: number, json: BlockDevice) {
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
import { byte, DiskFormat, memory } from '../types';
|
import { byte, DiskFormat, memory } from '../types';
|
||||||
import { base64_decode, base64_encode } from '../base64';
|
import { base64_decode, base64_encode } from '../base64';
|
||||||
import { bytify, debug, toHex } from '../util';
|
import { bytify, debug, toHex } from '../util';
|
||||||
|
import { GamepadConfiguration } from '../ui/gamepad';
|
||||||
|
|
||||||
export interface Disk {
|
export interface Disk {
|
||||||
format: DiskFormat
|
format: DiskFormat
|
||||||
@ -27,8 +28,12 @@ export interface Disk {
|
|||||||
export class JSONDiskBase {
|
export class JSONDiskBase {
|
||||||
type: DiskFormat
|
type: DiskFormat
|
||||||
name: string
|
name: string
|
||||||
|
disk?: number
|
||||||
|
category?: string
|
||||||
|
writeProtected?: boolean
|
||||||
volume: byte
|
volume: byte
|
||||||
readOnly: boolean
|
readOnly: boolean
|
||||||
|
gamepad?: GamepadConfiguration
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
2
js/gl.ts
2
js/gl.ts
@ -668,7 +668,7 @@ export class VideoModesGL implements VideoModes {
|
|||||||
private _displayConfig: screenEmu.DisplayConfiguration;
|
private _displayConfig: screenEmu.DisplayConfiguration;
|
||||||
private _monoMode: boolean = false;
|
private _monoMode: boolean = false;
|
||||||
|
|
||||||
ready: Promise<void>
|
public ready: Promise<void>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
gr: LoresPage,
|
gr: LoresPage,
|
||||||
|
19
js/types.ts
19
js/types.ts
@ -31,6 +31,25 @@ export type KnownKeys<T> = {
|
|||||||
[K in keyof T]: string extends K ? never : number extends K ? never : K
|
[K in keyof T]: string extends K ? never : number extends K ? never : K
|
||||||
} extends { [_ in keyof T]: infer U } ? U : never;
|
} extends { [_ in keyof T]: infer U } ? U : never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the declared values of a constant object.
|
||||||
|
*/
|
||||||
|
export type KnownValues<T> = T extends {
|
||||||
|
[_ in keyof T]: infer U } ? U : never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replacement for `includes` on constant types that is also a type assertion.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const SOME_VALUES = [1, 2, 'a'] as const;
|
||||||
|
* let n: number = 1;
|
||||||
|
* let r = includes(SOME_VALUES, n); // r === true, n is 1 | 2 | 'a'
|
||||||
|
* n = 5;
|
||||||
|
* r = includes(SOME_VALUES, n); // r === false, n is number
|
||||||
|
*/
|
||||||
|
export function includes<S extends T, T>(a: ReadonlyArray<S>, v: T): v is S {
|
||||||
|
return (a as ReadonlyArray<T>).includes(v);
|
||||||
|
}
|
||||||
|
|
||||||
/** A bit. */
|
/** A bit. */
|
||||||
export type bit = 0 | 1;
|
export type bit = 0 | 1;
|
||||||
|
937
js/ui/apple2.js
937
js/ui/apple2.js
@ -1,937 +0,0 @@
|
|||||||
import MicroModal from 'micromodal';
|
|
||||||
|
|
||||||
import { base64_json_parse, base64_json_stringify } from '../base64';
|
|
||||||
import Audio from './audio';
|
|
||||||
import DriveLights from './drive_lights';
|
|
||||||
import { DISK_FORMATS } from '../types';
|
|
||||||
import { gamepad, configGamepad, initGamepad } from './gamepad';
|
|
||||||
import KeyBoard from './keyboard';
|
|
||||||
import Tape, { TAPE_TYPES } from './tape';
|
|
||||||
|
|
||||||
import ApplesoftDump from '../applesoft/decompiler';
|
|
||||||
import ApplesoftCompiler from '../applesoft/compiler';
|
|
||||||
|
|
||||||
import { debug, gup, hup } from '../util';
|
|
||||||
import Prefs from '../prefs';
|
|
||||||
|
|
||||||
var paused = false;
|
|
||||||
|
|
||||||
var focused = false;
|
|
||||||
var startTime = Date.now();
|
|
||||||
var lastCycles = 0;
|
|
||||||
var lastFrames = 0;
|
|
||||||
var lastRenderedFrames = 0;
|
|
||||||
|
|
||||||
var hashtag = document.location.hash;
|
|
||||||
|
|
||||||
var disk_categories = {'Local Saves': []};
|
|
||||||
var disk_sets = {};
|
|
||||||
var disk_cur_name = [];
|
|
||||||
var disk_cur_cat = [];
|
|
||||||
|
|
||||||
var _apple2;
|
|
||||||
var cpu;
|
|
||||||
var stats;
|
|
||||||
var vm;
|
|
||||||
var tape;
|
|
||||||
var _disk2;
|
|
||||||
var _smartPort;
|
|
||||||
var _printer;
|
|
||||||
var audio;
|
|
||||||
var keyboard;
|
|
||||||
var io;
|
|
||||||
var _currentDrive = 1;
|
|
||||||
|
|
||||||
export const driveLights = new DriveLights();
|
|
||||||
|
|
||||||
export function dumpAppleSoftProgram() {
|
|
||||||
var dumper = new ApplesoftDump(cpu);
|
|
||||||
debug(dumper.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function compileAppleSoftProgram(program) {
|
|
||||||
var compiler = new ApplesoftCompiler(cpu);
|
|
||||||
compiler.compile(program);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openLoad(drive, event) {
|
|
||||||
_currentDrive = parseInt(drive, 10);
|
|
||||||
if (event.metaKey) {
|
|
||||||
openLoadHTTP(drive);
|
|
||||||
} else {
|
|
||||||
if (disk_cur_cat[drive]) {
|
|
||||||
document.querySelector('#category_select').value = disk_cur_cat[drive];
|
|
||||||
selectCategory();
|
|
||||||
}
|
|
||||||
MicroModal.show('load-modal');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openSave(drive, event) {
|
|
||||||
_currentDrive = parseInt(drive, 10);
|
|
||||||
|
|
||||||
var mimeType = 'application/octet-stream';
|
|
||||||
var data = _disk2.getBinary(drive);
|
|
||||||
var a = document.querySelector('#local_save_link');
|
|
||||||
|
|
||||||
var blob = new Blob([data], { 'type': mimeType });
|
|
||||||
a.href = window.URL.createObjectURL(blob);
|
|
||||||
a.download = driveLights.label(drive) + '.dsk';
|
|
||||||
|
|
||||||
if (event.metaKey) {
|
|
||||||
dumpDisk(drive);
|
|
||||||
} else {
|
|
||||||
document.querySelector('#save_name').value = driveLights.label(drive);
|
|
||||||
MicroModal.show('save-modal');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openAlert(msg) {
|
|
||||||
var el = document.querySelector('#alert-modal .message');
|
|
||||||
el.innerText = msg;
|
|
||||||
MicroModal.show('alert-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
/********************************************************************
|
|
||||||
*
|
|
||||||
* Drag and Drop
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function handleDragOver(drive, event) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.dataTransfer.dropEffect = 'copy';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleDragEnd(drive, event) {
|
|
||||||
var dt = event.dataTransfer;
|
|
||||||
if (dt.items) {
|
|
||||||
for (var i = 0; i < dt.items.length; i++) {
|
|
||||||
dt.items.remove(i);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
event.dataTransfer.clearData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleDrop(drive, event) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
if (drive < 1) {
|
|
||||||
if (!_disk2.getMetadata(1)) {
|
|
||||||
drive = 1;
|
|
||||||
} else if (!_disk2.getMetadata(2)) {
|
|
||||||
drive = 2;
|
|
||||||
} else {
|
|
||||||
drive = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var dt = event.dataTransfer;
|
|
||||||
if (dt.files.length == 1) {
|
|
||||||
var runOnLoad = event.shiftKey;
|
|
||||||
doLoadLocal(drive, dt.files[0], { runOnLoad });
|
|
||||||
} else if (dt.files.length == 2) {
|
|
||||||
doLoadLocal(1, dt.files[0]);
|
|
||||||
doLoadLocal(2, dt.files[1]);
|
|
||||||
} else {
|
|
||||||
for (var idx = 0; idx < dt.items.length; idx++) {
|
|
||||||
if (dt.items[idx].type === 'text/uri-list') {
|
|
||||||
dt.items[idx].getAsString(function(url) {
|
|
||||||
var parts = document.location.hash.split('|');
|
|
||||||
parts[drive - 1] = url;
|
|
||||||
document.location.hash = parts.join('|');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadingStart () {
|
|
||||||
var meter = document.querySelector('#loading-modal .meter');
|
|
||||||
meter.style.display = 'none';
|
|
||||||
MicroModal.show('loading-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadingProgress (current, total) {
|
|
||||||
if (total) {
|
|
||||||
var meter = document.querySelector('#loading-modal .meter');
|
|
||||||
var progress = document.querySelector('#loading-modal .progress');
|
|
||||||
meter.style.display = 'block';
|
|
||||||
progress.style.width = current / total * meter.clientWidth + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadingStop () {
|
|
||||||
MicroModal.close('loading-modal');
|
|
||||||
|
|
||||||
if (!paused) {
|
|
||||||
vm.ready.then(() => {
|
|
||||||
_apple2.run();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadAjax(drive, url) {
|
|
||||||
loadingStart();
|
|
||||||
|
|
||||||
fetch(url).then(function(response) {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
} else {
|
|
||||||
throw new Error('Error loading: ' + response.statusText);
|
|
||||||
}
|
|
||||||
}).then(function(data) {
|
|
||||||
if (data.type == 'binary') {
|
|
||||||
loadBinary(drive, data);
|
|
||||||
} else if (DISK_FORMATS.indexOf(data.type) > -1) {
|
|
||||||
loadDisk(drive, data);
|
|
||||||
}
|
|
||||||
initGamepad(data.gamepad);
|
|
||||||
loadingStop();
|
|
||||||
}).catch(function(error) {
|
|
||||||
loadingStop();
|
|
||||||
openAlert(error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function doLoad() {
|
|
||||||
MicroModal.close('load-modal');
|
|
||||||
var urls = document.querySelector('#disk_select').value, url;
|
|
||||||
if (urls && urls.length) {
|
|
||||||
if (typeof(urls) == 'string') {
|
|
||||||
url = urls;
|
|
||||||
} else {
|
|
||||||
url = urls[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var files = document.querySelector('#local_file').files;
|
|
||||||
if (files.length == 1) {
|
|
||||||
doLoadLocal(_currentDrive, files[0]);
|
|
||||||
} else if (url) {
|
|
||||||
var filename;
|
|
||||||
MicroModal.close('load-modal');
|
|
||||||
if (url.substr(0,6) == 'local:') {
|
|
||||||
filename = url.substr(6);
|
|
||||||
if (filename == '__manage') {
|
|
||||||
openManage();
|
|
||||||
} else {
|
|
||||||
loadLocalStorage(_currentDrive, filename);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var r1 = /json\/disks\/(.*).json$/.exec(url);
|
|
||||||
if (r1) {
|
|
||||||
filename = r1[1];
|
|
||||||
} else {
|
|
||||||
filename = url;
|
|
||||||
}
|
|
||||||
var parts = document.location.hash.split('|');
|
|
||||||
parts[_currentDrive - 1] = filename;
|
|
||||||
document.location.hash = parts.join('|');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function doSave() {
|
|
||||||
var name = document.querySelector('#save_name').value;
|
|
||||||
saveLocalStorage(_currentDrive, name);
|
|
||||||
MicroModal.close('save-modal');
|
|
||||||
window.setTimeout(() => openAlert('Saved'), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function doDelete(name) {
|
|
||||||
if (window.confirm('Delete ' + name + '?')) {
|
|
||||||
deleteLocalStorage(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const CIDERPRESS_EXTENSION = /#([0-9a-f]{2})([0-9a-f]{4})$/i;
|
|
||||||
const BIN_TYPES = ['bin'];
|
|
||||||
|
|
||||||
function doLoadLocal(drive, file, options = {}) {
|
|
||||||
var parts = file.name.split('.');
|
|
||||||
var ext = parts[parts.length - 1].toLowerCase();
|
|
||||||
var matches = file.name.match(CIDERPRESS_EXTENSION);
|
|
||||||
var type, aux;
|
|
||||||
if (matches && matches.length === 3) {
|
|
||||||
[, type, aux] = matches;
|
|
||||||
}
|
|
||||||
if (DISK_FORMATS.includes(ext)) {
|
|
||||||
doLoadLocalDisk(drive, file);
|
|
||||||
} else if (TAPE_TYPES.includes(ext)) {
|
|
||||||
tape.doLoadLocalTape(file);
|
|
||||||
} else if (BIN_TYPES.includes(ext) || type === '06' || options.address) {
|
|
||||||
doLoadBinary(file, { address: parseInt(aux || '2000', 16), ...options });
|
|
||||||
} else {
|
|
||||||
openAlert('Unknown file type: ' + ext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function doLoadBinary(file, options) {
|
|
||||||
loadingStart();
|
|
||||||
|
|
||||||
var fileReader = new FileReader();
|
|
||||||
fileReader.onload = function() {
|
|
||||||
let { address } = options;
|
|
||||||
const bytes = new Uint8Array(this.result);
|
|
||||||
for (let idx = 0; idx < this.result.byteLength; idx++) {
|
|
||||||
cpu.write(address >> 8, address & 0xff, bytes[idx]);
|
|
||||||
address++;
|
|
||||||
}
|
|
||||||
if (options.runOnLoad) {
|
|
||||||
cpu.reset();
|
|
||||||
cpu.setPC(options.address);
|
|
||||||
}
|
|
||||||
loadingStop();
|
|
||||||
};
|
|
||||||
fileReader.readAsArrayBuffer(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
function doLoadLocalDisk(drive, file) {
|
|
||||||
loadingStart();
|
|
||||||
var fileReader = new FileReader();
|
|
||||||
fileReader.onload = function() {
|
|
||||||
var parts = file.name.split('.');
|
|
||||||
var ext = parts.pop().toLowerCase();
|
|
||||||
var name = parts.join('.');
|
|
||||||
|
|
||||||
// Remove any json file reference
|
|
||||||
var files = document.location.hash.split('|');
|
|
||||||
files[drive - 1] = '';
|
|
||||||
document.location.hash = files.join('|');
|
|
||||||
|
|
||||||
if (this.result.byteLength >= 800 * 1024) {
|
|
||||||
if (_smartPort.setBinary(drive, name, ext, this.result)) {
|
|
||||||
focused = false;
|
|
||||||
initGamepad();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (_disk2.setBinary(drive, name, ext, this.result)) {
|
|
||||||
focused = false;
|
|
||||||
initGamepad();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loadingStop();
|
|
||||||
};
|
|
||||||
fileReader.readAsArrayBuffer(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function doLoadHTTP(drive, _url) {
|
|
||||||
if (!_url) {
|
|
||||||
MicroModal.close('http-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
loadingStart();
|
|
||||||
var url = _url || document.querySelector('#http_url').value;
|
|
||||||
if (url) {
|
|
||||||
fetch(url).then(function(response) {
|
|
||||||
if (response.ok) {
|
|
||||||
var reader = response.body.getReader();
|
|
||||||
var received = 0;
|
|
||||||
var chunks = [];
|
|
||||||
var contentLength = parseInt(response.headers.get('content-length'), 10);
|
|
||||||
|
|
||||||
return reader.read().then(function readChunk(result) {
|
|
||||||
if (result.done) {
|
|
||||||
var data = new Uint8Array(received);
|
|
||||||
var offset = 0;
|
|
||||||
for (var idx = 0; idx < chunks.length; idx++) {
|
|
||||||
data.set(chunks[idx], offset);
|
|
||||||
offset += chunks[idx].length;
|
|
||||||
}
|
|
||||||
return data.buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
received += result.value.length;
|
|
||||||
if (contentLength) {
|
|
||||||
loadingProgress(received, contentLength);
|
|
||||||
}
|
|
||||||
chunks.push(result.value);
|
|
||||||
|
|
||||||
return reader.read().then(readChunk);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error('Error loading: ' + response.statusText);
|
|
||||||
}
|
|
||||||
}).then(function(data) {
|
|
||||||
var urlParts = url.split('/');
|
|
||||||
var file = urlParts.pop();
|
|
||||||
var fileParts = file.split('.');
|
|
||||||
var ext = fileParts.pop().toLowerCase();
|
|
||||||
var name = decodeURIComponent(fileParts.join('.'));
|
|
||||||
if (data.byteLength >= 800 * 1024) {
|
|
||||||
if (_smartPort.setBinary(drive, name, ext, data)) {
|
|
||||||
initGamepad();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (_disk2.setBinary(drive, name, ext, data)) {
|
|
||||||
initGamepad();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loadingStop();
|
|
||||||
}).catch(function(error) {
|
|
||||||
loadingStop();
|
|
||||||
openAlert(error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openLoadHTTP(drive) {
|
|
||||||
_currentDrive = parseInt(drive, 10);
|
|
||||||
MicroModal.show('http-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
function openManage() {
|
|
||||||
MicroModal.show('manage-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
var prefs = new Prefs();
|
|
||||||
var showStats = 0;
|
|
||||||
|
|
||||||
export function updateKHz() {
|
|
||||||
var now = Date.now();
|
|
||||||
var ms = now - startTime;
|
|
||||||
var cycles = cpu.getCycles();
|
|
||||||
var delta;
|
|
||||||
var fps;
|
|
||||||
var khz;
|
|
||||||
|
|
||||||
switch (showStats) {
|
|
||||||
case 0: {
|
|
||||||
delta = cycles - lastCycles;
|
|
||||||
khz = parseInt(delta/ms);
|
|
||||||
document.querySelector('#khz').innerText = khz + ' kHz';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 1: {
|
|
||||||
delta = stats.renderedFrames - lastRenderedFrames;
|
|
||||||
fps = parseInt(delta/(ms/1000), 10);
|
|
||||||
document.querySelector('#khz').innerText = fps + ' rps';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
delta = stats.frames - lastFrames;
|
|
||||||
fps = parseInt(delta/(ms/1000), 10);
|
|
||||||
document.querySelector('#khz').innerText = fps + ' fps';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startTime = now;
|
|
||||||
lastCycles = cycles;
|
|
||||||
lastRenderedFrames = stats.renderedFrames;
|
|
||||||
lastFrames = stats.frames;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toggleShowFPS() {
|
|
||||||
showStats = ++showStats % 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateSound() {
|
|
||||||
var on = document.querySelector('#enable_sound').checked;
|
|
||||||
var label = document.querySelector('#toggle-sound i');
|
|
||||||
audio.enable(on);
|
|
||||||
if (on) {
|
|
||||||
label.classList.remove('fa-volume-off');
|
|
||||||
label.classList.add('fa-volume-up');
|
|
||||||
} else {
|
|
||||||
label.classList.remove('fa-volume-up');
|
|
||||||
label.classList.add('fa-volume-off');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dumpDisk(drive) {
|
|
||||||
var wind = window.open('', '_blank');
|
|
||||||
wind.document.title = driveLights.label(drive);
|
|
||||||
wind.document.write('<pre>');
|
|
||||||
wind.document.write(_disk2.getJSON(drive, true));
|
|
||||||
wind.document.write('</pre>');
|
|
||||||
wind.document.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function reset() {
|
|
||||||
_apple2.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadBinary(bin) {
|
|
||||||
for (var idx = 0; idx < bin.length; idx++) {
|
|
||||||
var pos = bin.start + idx;
|
|
||||||
cpu.write(pos >> 8, pos & 0xff, bin.data[idx]);
|
|
||||||
}
|
|
||||||
cpu.reset();
|
|
||||||
cpu.setPC(bin.start);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function selectCategory() {
|
|
||||||
document.querySelector('#disk_select').innerHTML = '';
|
|
||||||
var cat = disk_categories[document.querySelector('#category_select').value];
|
|
||||||
if (cat) {
|
|
||||||
for (var idx = 0; idx < cat.length; idx++) {
|
|
||||||
var file = cat[idx], name = file.name;
|
|
||||||
if (file.disk) {
|
|
||||||
name += ' - ' + file.disk;
|
|
||||||
}
|
|
||||||
var option = document.createElement('option');
|
|
||||||
option.value = file.filename;
|
|
||||||
option.innerText = name;
|
|
||||||
document.querySelector('#disk_select').append(option);
|
|
||||||
if (disk_cur_name[_currentDrive] == name) {
|
|
||||||
option.selected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function selectDisk() {
|
|
||||||
document.querySelector('#local_file').value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clickDisk() {
|
|
||||||
doLoad();
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadDisk(drive, disk) {
|
|
||||||
var name = disk.name;
|
|
||||||
var category = disk.category;
|
|
||||||
|
|
||||||
if (disk.disk) {
|
|
||||||
name += ' - ' + disk.disk;
|
|
||||||
}
|
|
||||||
|
|
||||||
disk_cur_cat[drive] = category;
|
|
||||||
disk_cur_name[drive] = name;
|
|
||||||
|
|
||||||
_disk2.setDisk(drive, disk);
|
|
||||||
initGamepad(disk.gamepad);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* LocalStorage Disk Storage
|
|
||||||
*/
|
|
||||||
|
|
||||||
function updateLocalStorage() {
|
|
||||||
var diskIndex = JSON.parse(window.localStorage.diskIndex || '{}');
|
|
||||||
var names = Object.keys(diskIndex), cat;
|
|
||||||
|
|
||||||
cat = disk_categories['Local Saves'] = [];
|
|
||||||
document.querySelector('#manage-modal-content').innerHTML = '';
|
|
||||||
|
|
||||||
names.forEach(function(name) {
|
|
||||||
cat.push({
|
|
||||||
'category': 'Local Saves',
|
|
||||||
'name': name,
|
|
||||||
'filename': 'local:' + name
|
|
||||||
});
|
|
||||||
document.querySelector('#manage-modal-content').innerHTML =
|
|
||||||
'<span class="local_save">' +
|
|
||||||
name +
|
|
||||||
' <a href="#" onclick="Apple2.doDelete(\'' +
|
|
||||||
name +
|
|
||||||
'\')">Delete</a><br /></span>';
|
|
||||||
});
|
|
||||||
cat.push({
|
|
||||||
'category': 'Local Saves',
|
|
||||||
'name': 'Manage Saves...',
|
|
||||||
'filename': 'local:__manage'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveLocalStorage(drive, name) {
|
|
||||||
var diskIndex = JSON.parse(window.localStorage.diskIndex || '{}');
|
|
||||||
|
|
||||||
var json = _disk2.getJSON(drive);
|
|
||||||
diskIndex[name] = json;
|
|
||||||
|
|
||||||
window.localStorage.diskIndex = JSON.stringify(diskIndex);
|
|
||||||
|
|
||||||
driveLights.label(drive, name);
|
|
||||||
driveLights.dirty(drive, false);
|
|
||||||
updateLocalStorage();
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteLocalStorage(name) {
|
|
||||||
var diskIndex = JSON.parse(window.localStorage.diskIndex || '{}');
|
|
||||||
if (diskIndex[name]) {
|
|
||||||
delete diskIndex[name];
|
|
||||||
openAlert('Deleted');
|
|
||||||
}
|
|
||||||
window.localStorage.diskIndex = JSON.stringify(diskIndex);
|
|
||||||
updateLocalStorage();
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadLocalStorage(drive, name) {
|
|
||||||
var diskIndex = JSON.parse(window.localStorage.diskIndex || '{}');
|
|
||||||
if (diskIndex[name]) {
|
|
||||||
_disk2.setJSON(drive, diskIndex[name]);
|
|
||||||
driveLights.label(drive, name);
|
|
||||||
driveLights.dirty(drive, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.localStorage !== undefined) {
|
|
||||||
document.querySelectorAll('.disksave').forEach(function (el) { el.style.display = 'inline-block';});
|
|
||||||
}
|
|
||||||
|
|
||||||
var oldcat = '';
|
|
||||||
var option;
|
|
||||||
for (var idx = 0; idx < window.disk_index.length; idx++) {
|
|
||||||
var file = window.disk_index[idx];
|
|
||||||
var cat = file.category;
|
|
||||||
var name = file.name, disk = file.disk;
|
|
||||||
if (file.e && !window.e) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (cat != oldcat) {
|
|
||||||
option = document.createElement('option');
|
|
||||||
option.value = cat;
|
|
||||||
option.innerText = cat;
|
|
||||||
document.querySelector('#category_select').append(option);
|
|
||||||
|
|
||||||
disk_categories[cat] = [];
|
|
||||||
oldcat = cat;
|
|
||||||
}
|
|
||||||
disk_categories[cat].push(file);
|
|
||||||
if (disk) {
|
|
||||||
if (!disk_sets[name]) {
|
|
||||||
disk_sets[name] = [];
|
|
||||||
}
|
|
||||||
disk_sets[name].push(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
option = document.createElement('option');
|
|
||||||
option.innerText = 'Local Saves';
|
|
||||||
document.querySelector('#category_select').append(option);
|
|
||||||
|
|
||||||
updateLocalStorage();
|
|
||||||
|
|
||||||
function processHash(hash) {
|
|
||||||
var files = hash.split('|');
|
|
||||||
for (var idx = 0; idx < files.length; idx++) {
|
|
||||||
var file = files[idx];
|
|
||||||
if (file.indexOf('://') > 0) {
|
|
||||||
var parts = file.split('.');
|
|
||||||
var ext = parts[parts.length - 1].toLowerCase();
|
|
||||||
if (ext == 'json') {
|
|
||||||
loadAjax(idx + 1, file);
|
|
||||||
} else {
|
|
||||||
doLoadHTTP(idx + 1, file);
|
|
||||||
}
|
|
||||||
} else if (file) {
|
|
||||||
loadAjax(idx + 1, 'json/disks/' + file + '.json');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Keyboard/Gamepad routines
|
|
||||||
*/
|
|
||||||
|
|
||||||
function _keydown(evt) {
|
|
||||||
if (!focused && (!evt.metaKey || evt.ctrlKey || window.e)) {
|
|
||||||
evt.preventDefault();
|
|
||||||
|
|
||||||
var key = keyboard.mapKeyEvent(evt);
|
|
||||||
if (key !== 0xff) {
|
|
||||||
io.keyDown(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (evt.keyCode === 112) { // F1 - Reset
|
|
||||||
cpu.reset();
|
|
||||||
evt.preventDefault(); // prevent launching help
|
|
||||||
} else if (evt.keyCode === 113) { // F2 - Full Screen
|
|
||||||
var elem = document.getElementById('screen');
|
|
||||||
if (evt.shiftKey) { // Full window, but not full screen
|
|
||||||
document.body.classList.toggle('full-page');
|
|
||||||
} else if (document.webkitCancelFullScreen) {
|
|
||||||
if (document.webkitIsFullScreen) {
|
|
||||||
document.webkitCancelFullScreen();
|
|
||||||
} else {
|
|
||||||
if (Element.ALLOW_KEYBOARD_INPUT) {
|
|
||||||
elem.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
|
|
||||||
} else {
|
|
||||||
elem.webkitRequestFullScreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (document.mozCancelFullScreen) {
|
|
||||||
if (document.mozIsFullScreen) {
|
|
||||||
document.mozCancelFullScreen();
|
|
||||||
} else {
|
|
||||||
elem.mozRequestFullScreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (evt.keyCode === 114) { // F3
|
|
||||||
io.keyDown(0x1b);
|
|
||||||
} else if (evt.keyCode === 117) { // F6 Quick Save
|
|
||||||
window.localStorage.state = base64_json_stringify(_apple2.getState());
|
|
||||||
} else if (evt.keyCode === 120) { // F9 Quick Restore
|
|
||||||
if (window.localStorage.state) {
|
|
||||||
_apple2.setState(base64_json_parse(window.localStorage.state));
|
|
||||||
}
|
|
||||||
} else if (evt.keyCode == 16) { // Shift
|
|
||||||
keyboard.shiftKey(true);
|
|
||||||
} else if (evt.keyCode == 20) { // Caps lock
|
|
||||||
keyboard.capslockKey();
|
|
||||||
} else if (evt.keyCode == 17) { // Control
|
|
||||||
keyboard.controlKey(true);
|
|
||||||
} else if (evt.keyCode == 91 || evt.keyCode == 93) { // Command
|
|
||||||
keyboard.commandKey(true);
|
|
||||||
} else if (evt.keyCode == 18) { // Alt
|
|
||||||
if (evt.location == 1) {
|
|
||||||
keyboard.commandKey(true);
|
|
||||||
} else {
|
|
||||||
keyboard.optionKey(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _keyup(evt) {
|
|
||||||
if (!focused)
|
|
||||||
io.keyUp();
|
|
||||||
|
|
||||||
if (evt.keyCode == 16) { // Shift
|
|
||||||
keyboard.shiftKey(false);
|
|
||||||
} else if (evt.keyCode == 17) { // Control
|
|
||||||
keyboard.controlKey(false);
|
|
||||||
} else if (evt.keyCode == 91 || evt.keyCode == 93) { // Command
|
|
||||||
keyboard.commandKey(false);
|
|
||||||
} else if (evt.keyCode == 18) { // Alt
|
|
||||||
if (evt.location == 1) {
|
|
||||||
keyboard.commandKey(false);
|
|
||||||
} else {
|
|
||||||
keyboard.optionKey(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateScreen() {
|
|
||||||
var mono = document.querySelector('#mono_screen').checked;
|
|
||||||
var scanlines = document.querySelector('#show_scanlines').checked;
|
|
||||||
var gl = document.querySelector('#gl_canvas').checked;
|
|
||||||
|
|
||||||
var screen = document.querySelector('#screen');
|
|
||||||
var overscan = document.querySelector('.overscan');
|
|
||||||
if (scanlines && !gl) {
|
|
||||||
overscan.classList.add('scanlines');
|
|
||||||
} else {
|
|
||||||
overscan.classList.remove('scanlines');
|
|
||||||
}
|
|
||||||
if (mono && !gl) {
|
|
||||||
screen.classList.add('mono');
|
|
||||||
} else {
|
|
||||||
screen.classList.remove('mono');
|
|
||||||
}
|
|
||||||
vm.mono(mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateCPU() {
|
|
||||||
var accelerated = document.querySelector('#accelerator_toggle').checked;
|
|
||||||
var kHz = accelerated ? 4092 : 1023;
|
|
||||||
io.updateKHz(kHz);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateUI() {
|
|
||||||
if (document.location.hash != hashtag) {
|
|
||||||
hashtag = document.location.hash;
|
|
||||||
var hash = hup();
|
|
||||||
if (hash) {
|
|
||||||
processHash(hash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var disableMouseJoystick = false;
|
|
||||||
var flipX = false;
|
|
||||||
var flipY = false;
|
|
||||||
var swapXY = false;
|
|
||||||
|
|
||||||
export function updateJoystick() {
|
|
||||||
disableMouseJoystick = document.querySelector('#disable_mouse').checked;
|
|
||||||
flipX = document.querySelector('#flip_x').checked;
|
|
||||||
flipY = document.querySelector('#flip_y').checked;
|
|
||||||
swapXY = document.querySelector('#swap_x_y').checked;
|
|
||||||
configGamepad(flipX, flipY);
|
|
||||||
|
|
||||||
if (disableMouseJoystick) {
|
|
||||||
io.paddle(0, 0.5);
|
|
||||||
io.paddle(1, 0.5);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _mousemove(evt) {
|
|
||||||
if (gamepad || disableMouseJoystick) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var s = document.querySelector('#screen');
|
|
||||||
var offset = s.getBoundingClientRect();
|
|
||||||
var x = (evt.pageX - offset.left) / s.clientWidth,
|
|
||||||
y = (evt.pageY - offset.top) / s.clientHeight,
|
|
||||||
z = x;
|
|
||||||
|
|
||||||
if (swapXY) {
|
|
||||||
x = y;
|
|
||||||
y = z;
|
|
||||||
}
|
|
||||||
|
|
||||||
io.paddle(0, flipX ? 1 - x : x);
|
|
||||||
io.paddle(1, flipY ? 1 - y : y);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pauseRun() {
|
|
||||||
var label = document.querySelector('#pause-run i');
|
|
||||||
if (paused) {
|
|
||||||
vm.ready.then(() => {
|
|
||||||
_apple2.run();
|
|
||||||
});
|
|
||||||
label.classList.remove('fa-play');
|
|
||||||
label.classList.add('fa-pause');
|
|
||||||
} else {
|
|
||||||
_apple2.stop();
|
|
||||||
label.classList.remove('fa-pause');
|
|
||||||
label.classList.add('fa-play');
|
|
||||||
}
|
|
||||||
paused = !paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toggleSound() {
|
|
||||||
var enableSound = document.querySelector('#enable_sound');
|
|
||||||
enableSound.checked = !enableSound.checked;
|
|
||||||
updateSound();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openOptions() {
|
|
||||||
MicroModal.show('options-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openPrinterModal() {
|
|
||||||
const mimeType = 'application/octet-stream';
|
|
||||||
const data = _printer.getRawOutput();
|
|
||||||
const a = document.querySelector('#raw_printer_output');
|
|
||||||
|
|
||||||
const blob = new Blob([data], { 'type': mimeType});
|
|
||||||
a.href = window.URL.createObjectURL(blob);
|
|
||||||
a.download = 'raw_printer_output.bin';
|
|
||||||
MicroModal.show('printer-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearPrinterPaper() {
|
|
||||||
_printer.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLoaded(apple2, disk2, smartPort, printer, e) {
|
|
||||||
_apple2 = apple2;
|
|
||||||
cpu = _apple2.getCPU();
|
|
||||||
io = _apple2.getIO();
|
|
||||||
stats = apple2.getStats();
|
|
||||||
vm = apple2.getVideoModes();
|
|
||||||
tape = new Tape(io);
|
|
||||||
_disk2 = disk2;
|
|
||||||
_smartPort = smartPort;
|
|
||||||
_printer = printer;
|
|
||||||
|
|
||||||
keyboard = new KeyBoard(cpu, io, e);
|
|
||||||
keyboard.create('#keyboard');
|
|
||||||
audio = new Audio(io);
|
|
||||||
|
|
||||||
MicroModal.init();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Input Handling
|
|
||||||
*/
|
|
||||||
|
|
||||||
window.addEventListener('keydown', _keydown);
|
|
||||||
window.addEventListener('keyup', _keyup);
|
|
||||||
|
|
||||||
window.addEventListener('keydown', audio.autoStart);
|
|
||||||
if (window.ontouchstart !== undefined) {
|
|
||||||
window.addEventListener('touchstart', audio.autoStart);
|
|
||||||
}
|
|
||||||
window.addEventListener('mousedown', audio.autoStart);
|
|
||||||
|
|
||||||
window.addEventListener('paste', (event) => {
|
|
||||||
var paste = (event.clipboardData || window.clipboardData).getData('text');
|
|
||||||
io.setKeyBuffer(paste);
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('copy', (event) => {
|
|
||||||
event.clipboardData.setData('text/plain', vm.getText());
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
document.querySelectorAll('canvas').forEach(function(canvas) {
|
|
||||||
canvas.addEventListener('mousedown', function(evt) {
|
|
||||||
if (!gamepad) {
|
|
||||||
io.buttonDown(evt.which == 1 ? 0 : 1);
|
|
||||||
}
|
|
||||||
evt.preventDefault();
|
|
||||||
});
|
|
||||||
canvas.addEventListener('mouseup', function(evt) {
|
|
||||||
if (!gamepad) {
|
|
||||||
io.buttonUp(evt.which == 1 ? 0 : 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
canvas.addEventListener('contextmenu', function(evt) {
|
|
||||||
evt.preventDefault();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.addEventListener('mousemove', _mousemove);
|
|
||||||
|
|
||||||
document.querySelectorAll('input,textarea').forEach(function(input) {
|
|
||||||
input.addEventListener('focus', function() { focused = true; });
|
|
||||||
input.addEventListener('blur', function() { focused = false; });
|
|
||||||
});
|
|
||||||
|
|
||||||
if (prefs.havePrefs()) {
|
|
||||||
document.querySelectorAll('#options-modal input[type=checkbox]').forEach(function(el) {
|
|
||||||
var val = prefs.readPref(el.id);
|
|
||||||
if (val) {
|
|
||||||
el.checked = JSON.parse(val);
|
|
||||||
}
|
|
||||||
el.addEventListener('change', function() {
|
|
||||||
prefs.writePref(el.id, JSON.stringify(el.checked));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
document.querySelectorAll('#options-modal select').forEach(function(el) {
|
|
||||||
var val = prefs.readPref(el.id);
|
|
||||||
if (val) {
|
|
||||||
el.value = val;
|
|
||||||
}
|
|
||||||
el.addEventListener('change', function() {
|
|
||||||
prefs.writePref(el.id, el.value);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (navigator.standalone) {
|
|
||||||
document.body.classList.add('standalone');
|
|
||||||
}
|
|
||||||
|
|
||||||
cpu.reset();
|
|
||||||
setInterval(updateKHz, 1000);
|
|
||||||
updateSound();
|
|
||||||
updateScreen();
|
|
||||||
updateCPU();
|
|
||||||
initGamepad();
|
|
||||||
|
|
||||||
// Check for disks in hashtag
|
|
||||||
|
|
||||||
var hash = gup('disk') || hup();
|
|
||||||
if (hash) {
|
|
||||||
_apple2.stop();
|
|
||||||
processHash(hash);
|
|
||||||
} else {
|
|
||||||
vm.ready.then(() => {
|
|
||||||
_apple2.run();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initUI(apple2, disk2, smartPort, printer, e) {
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
onLoaded(apple2, disk2, smartPort, printer, e);
|
|
||||||
});
|
|
||||||
}
|
|
1053
js/ui/apple2.ts
Normal file
1053
js/ui/apple2.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -9,11 +9,11 @@ export default class DriveLights implements Callbacks {
|
|||||||
'url(css/red-off-16.png)';
|
'url(css/red-off-16.png)';
|
||||||
}
|
}
|
||||||
|
|
||||||
public dirty() {
|
public dirty(_drive: DriveNumber, _dirty: boolean) {
|
||||||
// document.querySelector('#disksave' + drive).disabled = !dirty;
|
// document.querySelector('#disksave' + drive).disabled = !dirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public label(drive: DriveNumber, label: string) {
|
public label(drive: DriveNumber, label?: string) {
|
||||||
const labelElement =
|
const labelElement =
|
||||||
document.querySelector('#disk-label' + drive)! as HTMLElement;
|
document.querySelector('#disk-label' + drive)! as HTMLElement;
|
||||||
if (label) {
|
if (label) {
|
||||||
|
@ -19,7 +19,7 @@ export const TAPE_TYPES = ['wav', 'aiff', 'aif', 'mp3', 'm4a'] as const;
|
|||||||
export default class Tape {
|
export default class Tape {
|
||||||
constructor(private readonly io: Apple2IO) {}
|
constructor(private readonly io: Apple2IO) {}
|
||||||
|
|
||||||
public doLoadLocalTape(file: File, done: () => void) {
|
public doLoadLocalTape(file: File, done?: () => void) {
|
||||||
const kHz = this.io.getKHz();
|
const kHz = this.io.getKHz();
|
||||||
|
|
||||||
// Audio Buffer Source
|
// Audio Buffer Source
|
||||||
@ -28,7 +28,7 @@ export default class Tape {
|
|||||||
context = new AudioContext();
|
context = new AudioContext();
|
||||||
} else {
|
} else {
|
||||||
window.alert('Not supported by your browser');
|
window.alert('Not supported by your browser');
|
||||||
done();
|
done && done();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,4 +69,10 @@ export interface VideoModes extends Restorable<VideoModesState> {
|
|||||||
isMixed(): boolean
|
isMixed(): boolean
|
||||||
isPage2(): boolean
|
isPage2(): boolean
|
||||||
isText(): boolean
|
isText(): boolean
|
||||||
|
|
||||||
|
mono(on: boolean): void
|
||||||
|
|
||||||
|
getText(): string
|
||||||
|
|
||||||
|
ready: Promise<void>
|
||||||
}
|
}
|
||||||
|
13
package-lock.json
generated
13
package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"@babel/core": "^7.9.0",
|
"@babel/core": "^7.9.0",
|
||||||
"@babel/preset-env": "^7.9.0",
|
"@babel/preset-env": "^7.9.0",
|
||||||
"@types/jest": "^26.0.14",
|
"@types/jest": "^26.0.14",
|
||||||
|
"@types/micromodal": "^0.3.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.6.1",
|
"@typescript-eslint/eslint-plugin": "^4.6.1",
|
||||||
"@typescript-eslint/parser": "^4.6.1",
|
"@typescript-eslint/parser": "^4.6.1",
|
||||||
"ajv": "^6.12.0",
|
"ajv": "^6.12.0",
|
||||||
@ -2163,6 +2164,12 @@
|
|||||||
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
|
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/micromodal": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/micromodal/-/micromodal-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-rQAprHsGUqtbngygYKWKpgbmWCbdR6injIeLELVjapsI7xXCjKlbaet62haRG+CLYOacXzlUlS0me8H+3RG1UQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "14.14.35",
|
"version": "14.14.35",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz",
|
||||||
@ -14938,6 +14945,12 @@
|
|||||||
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
|
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/micromodal": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/micromodal/-/micromodal-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-rQAprHsGUqtbngygYKWKpgbmWCbdR6injIeLELVjapsI7xXCjKlbaet62haRG+CLYOacXzlUlS0me8H+3RG1UQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "14.14.35",
|
"version": "14.14.35",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz",
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
"@babel/core": "^7.9.0",
|
"@babel/core": "^7.9.0",
|
||||||
"@babel/preset-env": "^7.9.0",
|
"@babel/preset-env": "^7.9.0",
|
||||||
"@types/jest": "^26.0.14",
|
"@types/jest": "^26.0.14",
|
||||||
|
"@types/micromodal": "^0.3.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.6.1",
|
"@typescript-eslint/eslint-plugin": "^4.6.1",
|
||||||
"@typescript-eslint/parser": "^4.6.1",
|
"@typescript-eslint/parser": "^4.6.1",
|
||||||
"ajv": "^6.12.0",
|
"ajv": "^6.12.0",
|
||||||
|
Loading…
Reference in New Issue
Block a user