Compare commits
5 Commits
59b40b3ac8
...
0216e75e7f
Author | SHA1 | Date |
---|---|---|
Will Scullin | 0216e75e7f | |
Will Scullin | 88bd7836bc | |
Will Scullin | ca3f255a87 | |
Will Scullin | 3f6cfd67b7 | |
Will Scullin | f2dd9b96a3 |
|
@ -10,6 +10,10 @@ trim_trailing_whitespace = true
|
||||||
[*.js]
|
[*.js]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
indent_size = 2
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
[*.html]
|
[*.html]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
|
211
.eslintrc.json
211
.eslintrc.json
|
@ -1,20 +1,60 @@
|
||||||
{
|
{
|
||||||
|
// Global
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:jest/recommended"
|
||||||
|
],
|
||||||
|
"globals": {
|
||||||
|
"tapes": "writable"
|
||||||
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"indent": [
|
"indent": [
|
||||||
2,
|
"error",
|
||||||
4
|
4,
|
||||||
|
{
|
||||||
|
"SwitchCase": 1
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"quotes": [
|
"quotes": [
|
||||||
2,
|
"error",
|
||||||
"single"
|
"single",
|
||||||
|
{ "avoidEscape": true }
|
||||||
],
|
],
|
||||||
"linebreak-style": [
|
"linebreak-style": [
|
||||||
2,
|
"error",
|
||||||
"unix"
|
"unix"
|
||||||
],
|
],
|
||||||
"semi": [
|
"eqeqeq": [
|
||||||
2,
|
"error",
|
||||||
"always"
|
"smart"
|
||||||
|
],
|
||||||
|
"prefer-const": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"no-var": "error",
|
||||||
|
"no-use-before-define": "off",
|
||||||
|
"no-console": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allow": [
|
||||||
|
"info",
|
||||||
|
"warn",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// Jest configuration
|
||||||
|
"jest/expect-expect": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"assertFunctionNames": [
|
||||||
|
"expect*",
|
||||||
|
"checkImageData",
|
||||||
|
"testCode"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
|
@ -22,16 +62,109 @@
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"es6": true
|
"es6": true
|
||||||
},
|
},
|
||||||
"globals": {
|
|
||||||
"tapes": true
|
|
||||||
},
|
|
||||||
"parserOptions": {
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"extends": "eslint:recommended",
|
|
||||||
"overrides": [
|
"overrides": [
|
||||||
|
// All overrides matching a file are applied in-order, with the last
|
||||||
|
// taking precedence.
|
||||||
|
//
|
||||||
|
// TypeScript/TSX-specific configuration
|
||||||
{
|
{
|
||||||
"files": [ "bin/*", "babel.config.js", "webpack.config.js" ],
|
"files": [
|
||||||
|
"*.ts",
|
||||||
|
"*.tsx"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint/eslint-plugin"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended-requiring-type-checking"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"indent": [
|
||||||
|
"error",
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"SwitchCase": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// recommended is just "warn"
|
||||||
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
|
// enforce semicolons at ends of statements
|
||||||
|
"semi": "off",
|
||||||
|
"@typescript-eslint/semi": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
// enforce semicolons to separate members
|
||||||
|
"@typescript-eslint/member-delimiter-style": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"multiline": {
|
||||||
|
"delimiter": "semi",
|
||||||
|
"requireLast": true
|
||||||
|
},
|
||||||
|
"singleline": {
|
||||||
|
"delimiter": "semi",
|
||||||
|
"requireLast": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// definitions must come before uses for variables
|
||||||
|
"@typescript-eslint/no-use-before-define": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"functions": false,
|
||||||
|
"classes": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// no used variables
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// no redeclaration of classes, members or variables
|
||||||
|
"no-redeclare": "off",
|
||||||
|
"@typescript-eslint/no-redeclare": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
// allow empty interface definitions and empty extends
|
||||||
|
"@typescript-eslint/no-empty-interface": "off",
|
||||||
|
// allow explicit type declaration
|
||||||
|
"@typescript-eslint/no-inferrable-types": "off",
|
||||||
|
// allow some non-string types in templates
|
||||||
|
"@typescript-eslint/restrict-template-expressions": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowNumber": true,
|
||||||
|
"allowBoolean": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module",
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// UI elements
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"js/ui/**.ts"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
// allow non-null assertions since these classes reference the DOM
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// JS Node configuration
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"bin/*",
|
||||||
|
"babel.config.js",
|
||||||
|
"webpack.config.js"
|
||||||
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-console": 0
|
"no-console": 0
|
||||||
},
|
},
|
||||||
|
@ -40,17 +173,49 @@
|
||||||
"jquery": false,
|
"jquery": false,
|
||||||
"browser": false
|
"browser": false
|
||||||
}
|
}
|
||||||
}, {
|
},
|
||||||
"files": [ "test/*"],
|
// Test configuration
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"test/**/*"
|
||||||
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"node": true,
|
"jest": true,
|
||||||
"jest": true
|
"jasmine": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-console": 0
|
||||||
}
|
}
|
||||||
}, {
|
},
|
||||||
"files": [ "js/entry1.js"],
|
// Entry point configuration
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"js/entry2.ts",
|
||||||
|
"js/entry2e.ts",
|
||||||
|
"jest.config.js"
|
||||||
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"commonjs": true
|
"commonjs": true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// Worker configuration
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"workers/*"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "workers/tsconfig.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"ignorePatterns": [
|
||||||
|
"coverage/**/*"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"pragma": "h",
|
||||||
|
"version": "16"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,23 +4,22 @@ on: [push]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x]
|
node-version: [16.x, 18.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- name: npm install, build, and test
|
- name: npm install, build, and test
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm run build --if-present
|
npm run build --if-present
|
||||||
npm test
|
npm test
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
module.exports = {
|
||||||
|
'moduleNameMapper': {
|
||||||
|
'^js/(.*)': '<rootDir>/js/$1',
|
||||||
|
'^test/(.*)': '<rootDir>/test/$1',
|
||||||
|
'\\.css$': 'identity-obj-proxy',
|
||||||
|
'\\.scss$': 'identity-obj-proxy',
|
||||||
|
},
|
||||||
|
'roots': [
|
||||||
|
'js/',
|
||||||
|
'test/',
|
||||||
|
],
|
||||||
|
'testMatch': [
|
||||||
|
'**/?(*.)+(spec|test).+(ts|js|tsx)'
|
||||||
|
],
|
||||||
|
'transform': {
|
||||||
|
'^.+\\.js$': 'babel-jest',
|
||||||
|
'^.+\\.ts$': 'ts-jest',
|
||||||
|
'^.*\\.tsx$': 'ts-jest',
|
||||||
|
},
|
||||||
|
'transformIgnorePatterns': [
|
||||||
|
'/node_modules/(?!(@testing-library/preact/dist/esm)/)',
|
||||||
|
],
|
||||||
|
'coveragePathIgnorePatterns': [
|
||||||
|
'/node_modules/',
|
||||||
|
'/js/roms/',
|
||||||
|
'/test/',
|
||||||
|
],
|
||||||
|
'preset': 'ts-jest',
|
||||||
|
};
|
342
js/apple1.ts
342
js/apple1.ts
|
@ -1,22 +1,35 @@
|
||||||
import MicroModal from "micromodal";
|
/* Copyright 2010-2023 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 Apple1IO from "./apple1io";
|
import MicroModal from 'micromodal';
|
||||||
import CPU6502 from "./cpu6502";
|
|
||||||
import Prefs from "./prefs";
|
|
||||||
import RAM from "./ram";
|
|
||||||
import { TextPage } from "./canvas1";
|
|
||||||
import { debug, hup } from "./util";
|
|
||||||
|
|
||||||
import Basic from "./roms/basic";
|
import Apple1IO from './apple1io';
|
||||||
import Bios from "./roms/bios";
|
import CPU6502 from './cpu6502';
|
||||||
import Krusader from "./roms/krusader";
|
import Prefs from './prefs';
|
||||||
|
import RAM from './ram';
|
||||||
|
import { TextPage } from './canvas1';
|
||||||
|
import { debug, hup } from './util';
|
||||||
|
|
||||||
import ACI from "./cards/aci";
|
import Basic from './roms/basic';
|
||||||
|
import Bios from './roms/bios';
|
||||||
|
import Krusader from './roms/krusader';
|
||||||
|
|
||||||
import { mapKeyEvent, KeyBoard } from "./ui/keyboard";
|
import ACI from './cards/aci';
|
||||||
import { address, byte } from "./types";
|
|
||||||
|
|
||||||
|
import { mapKeyEvent, KeyBoard } from './ui/keyboard';
|
||||||
|
import { address, byte } from './types';
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
let DEBUG = false;
|
let DEBUG = false;
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
let TRACE = true;
|
let TRACE = true;
|
||||||
const skidmarks: string[] = [];
|
const skidmarks: string[] = [];
|
||||||
|
|
||||||
|
@ -32,22 +45,22 @@ const prefs = new Prefs();
|
||||||
let runTimer: ReturnType<typeof setInterval> | null = null;
|
let runTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
const cpu = new CPU6502();
|
const cpu = new CPU6502();
|
||||||
|
|
||||||
var krusader = window.location.hash == "#krusader";
|
const krusader = window.location.hash === '#krusader';
|
||||||
|
|
||||||
var raml, ramh, rom, aci: ACI, io: Apple1IO, text: TextPage, keyboard: KeyBoard;
|
let ramh, rom;
|
||||||
|
|
||||||
// 32K base memory. Should be 0x0f for 4K, 0x1f for 8K, 0x3f for 16K
|
// 32K base memory. Should be 0x0f for 4K, 0x1f for 8K, 0x3f for 16K
|
||||||
raml = new RAM(0x00, 0x7f);
|
const raml = new RAM(0x00, 0x7f);
|
||||||
text = new TextPage();
|
const text = new TextPage();
|
||||||
text.init();
|
text.init();
|
||||||
|
|
||||||
aci = new ACI(cpu, {
|
const aci = new ACI(cpu, {
|
||||||
progress: function (val) {
|
progress: function (val) {
|
||||||
document.querySelector<HTMLElement>("#tape")!.style.width =
|
document.querySelector<HTMLElement>('#tape')!.style.width =
|
||||||
val * 100 + "px";
|
val * 100 + 'px';
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
io = new Apple1IO(text);
|
const io = new Apple1IO(text);
|
||||||
|
|
||||||
if (krusader) {
|
if (krusader) {
|
||||||
ramh = null;
|
ramh = null;
|
||||||
|
@ -57,7 +70,7 @@ if (krusader) {
|
||||||
ramh = new Basic();
|
ramh = new Basic();
|
||||||
rom = new Bios();
|
rom = new Bios();
|
||||||
}
|
}
|
||||||
keyboard = new KeyBoard("#keyboard", cpu, io, text);
|
const keyboard = new KeyBoard('#keyboard', cpu, io, text);
|
||||||
|
|
||||||
cpu.addPageHandler(raml);
|
cpu.addPageHandler(raml);
|
||||||
if (ramh) {
|
if (ramh) {
|
||||||
|
@ -68,7 +81,7 @@ cpu.addPageHandler(rom);
|
||||||
cpu.addPageHandler(aci);
|
cpu.addPageHandler(aci);
|
||||||
cpu.addPageHandler(io);
|
cpu.addPageHandler(io);
|
||||||
|
|
||||||
var showFPS = false;
|
let showFPS = false;
|
||||||
|
|
||||||
interface Tape {
|
interface Tape {
|
||||||
script: string;
|
script: string;
|
||||||
|
@ -86,7 +99,7 @@ declare global {
|
||||||
//aci.setData([0x01,0x23,0x45,0x67,0x89,0xab,0xcd,0xef])
|
//aci.setData([0x01,0x23,0x45,0x67,0x89,0xab,0xcd,0xef])
|
||||||
|
|
||||||
//aci.setData(tapes['BASIC']);
|
//aci.setData(tapes['BASIC']);
|
||||||
aci.setData(window.tapes["Microchess"].tracks);
|
aci.setData(window.tapes['Microchess'].tracks);
|
||||||
|
|
||||||
// Audio Buffer Source
|
// Audio Buffer Source
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -99,55 +112,61 @@ const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||||
const context = new AudioContext();
|
const context = new AudioContext();
|
||||||
|
|
||||||
export function doLoadLocal(files: FileList) {
|
export function doLoadLocal(files: FileList) {
|
||||||
context.resume();
|
context
|
||||||
files =
|
.resume()
|
||||||
files || document.querySelector<HTMLInputElement>("#local_file")!.files;
|
.then(() => {
|
||||||
if (files.length == 1) {
|
files =
|
||||||
var file = files[0];
|
files || document.querySelector<HTMLInputElement>('#local_file')!.files;
|
||||||
var fileReader = new FileReader();
|
if (files.length === 1) {
|
||||||
fileReader.onload = function (ev) {
|
const file = files[0];
|
||||||
context.decodeAudioData(
|
const fileReader = new FileReader();
|
||||||
ev.target!.result as ArrayBuffer,
|
fileReader.onload = function (ev) {
|
||||||
function (buffer) {
|
context
|
||||||
var buf = [];
|
.decodeAudioData(
|
||||||
var data = buffer.getChannelData(0);
|
ev.target!.result as ArrayBuffer,
|
||||||
var old = data[0] > 0.25;
|
function (buffer) {
|
||||||
var last = 0;
|
const buf = [];
|
||||||
for (var idx = 1; idx < data.length; idx++) {
|
const data = buffer.getChannelData(0);
|
||||||
var current = data[idx] > 0.25;
|
let old = data[0] > 0.25;
|
||||||
if (current != old) {
|
let last = 0;
|
||||||
var delta = idx - last;
|
for (let idx = 1; idx < data.length; idx++) {
|
||||||
buf.push(Math.floor((delta / buffer.sampleRate) * 1023000));
|
const current = data[idx] > 0.25;
|
||||||
old = current;
|
if (current !== old) {
|
||||||
last = idx;
|
const delta = idx - last;
|
||||||
}
|
buf.push(Math.floor((delta / buffer.sampleRate) * 1023000));
|
||||||
}
|
old = current;
|
||||||
aci.buffer = buf;
|
last = idx;
|
||||||
MicroModal.close("local-modal");
|
}
|
||||||
},
|
}
|
||||||
function () {
|
aci.buffer = buf;
|
||||||
window.alert("Unable to read tape file: " + file.name);
|
MicroModal.close('local-modal');
|
||||||
}
|
},
|
||||||
);
|
function () {
|
||||||
};
|
window.alert('Unable to read tape file: ' + file.name);
|
||||||
fileReader.readAsArrayBuffer(file);
|
}
|
||||||
}
|
)
|
||||||
|
.catch(console.error);
|
||||||
|
};
|
||||||
|
fileReader.readAsArrayBuffer(file);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateKHz() {
|
function updateKHz() {
|
||||||
let now = Date.now();
|
const now = Date.now();
|
||||||
let ms = now - startTime;
|
const ms = now - startTime;
|
||||||
let cycles = cpu.getCycles();
|
const cycles = cpu.getCycles();
|
||||||
let delta: number;
|
let delta: number;
|
||||||
|
|
||||||
if (showFPS) {
|
if (showFPS) {
|
||||||
delta = renderedFrames - lastFrames;
|
delta = renderedFrames - lastFrames;
|
||||||
var fps = Math.floor(delta / (ms / 1000));
|
const fps = Math.floor(delta / (ms / 1000));
|
||||||
document.querySelector("#khz")!.innerHTML = fps + "fps";
|
document.querySelector('#khz')!.innerHTML = fps + 'fps';
|
||||||
} else {
|
} else {
|
||||||
delta = cycles - lastCycles;
|
delta = cycles - lastCycles;
|
||||||
var khz = Math.floor(delta / ms);
|
const khz = Math.floor(delta / ms);
|
||||||
document.querySelector("#khz")!.innerHTML = khz + "KHz";
|
document.querySelector('#khz')!.innerHTML = khz + 'KHz';
|
||||||
}
|
}
|
||||||
|
|
||||||
startTime = now;
|
startTime = now;
|
||||||
|
@ -155,9 +174,8 @@ function updateKHz() {
|
||||||
lastFrames = renderedFrames;
|
lastFrames = renderedFrames;
|
||||||
}
|
}
|
||||||
|
|
||||||
var loading = false;
|
let throttling = true;
|
||||||
var throttling = true;
|
let turbotape = false;
|
||||||
var turbotape = false;
|
|
||||||
|
|
||||||
export function toggleFPS() {
|
export function toggleFPS() {
|
||||||
showFPS = !showFPS;
|
showFPS = !showFPS;
|
||||||
|
@ -165,7 +183,7 @@ export function toggleFPS() {
|
||||||
|
|
||||||
export function toggleSpeed() {
|
export function toggleSpeed() {
|
||||||
throttling =
|
throttling =
|
||||||
document.querySelector<HTMLInputElement>("#speed_toggle")!.checked;
|
document.querySelector<HTMLInputElement>('#speed_toggle')!.checked;
|
||||||
if (runTimer) {
|
if (runTimer) {
|
||||||
run();
|
run();
|
||||||
}
|
}
|
||||||
|
@ -188,17 +206,17 @@ function run(pc?: address) {
|
||||||
cpu.setPC(pc);
|
cpu.setPC(pc);
|
||||||
}
|
}
|
||||||
|
|
||||||
var ival = 30,
|
let ival = 30;
|
||||||
step = 1023 * ival,
|
let step = 1023 * ival;
|
||||||
stepMax = step;
|
const stepMax = step;
|
||||||
|
|
||||||
if (!throttling) {
|
if (!throttling) {
|
||||||
ival = 1;
|
ival = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var now,
|
let now;
|
||||||
last = Date.now();
|
let last = Date.now();
|
||||||
var runFn = function () {
|
const runFn = function () {
|
||||||
now = Date.now();
|
now = Date.now();
|
||||||
renderedFrames++;
|
renderedFrames++;
|
||||||
step = (now - last) * 1023;
|
step = (now - last) * 1023;
|
||||||
|
@ -206,27 +224,25 @@ function run(pc?: address) {
|
||||||
if (step > stepMax) {
|
if (step > stepMax) {
|
||||||
step = stepMax;
|
step = stepMax;
|
||||||
}
|
}
|
||||||
if (document.location.hash != hashtag) {
|
if (document.location.hash !== hashtag) {
|
||||||
hashtag = document.location.hash;
|
hashtag = document.location.hash;
|
||||||
}
|
}
|
||||||
if (!loading) {
|
if (DEBUG) {
|
||||||
if (DEBUG) {
|
cpu.stepCyclesDebug(TRACE ? 1 : step, function () {
|
||||||
cpu.stepCyclesDebug(TRACE ? 1 : step, function () {
|
const line = JSON.stringify(cpu.getState());
|
||||||
var line = JSON.stringify(cpu.getState());
|
if (TRACE) {
|
||||||
if (TRACE) {
|
debug(line);
|
||||||
debug(line);
|
} else {
|
||||||
} else {
|
skidmarks.push(line);
|
||||||
skidmarks.push(line);
|
if (skidmarks.length > 256) {
|
||||||
if (skidmarks.length > 256) {
|
skidmarks.shift();
|
||||||
skidmarks.shift();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
} else {
|
});
|
||||||
cpu.stepCycles(step);
|
} else {
|
||||||
}
|
cpu.stepCycles(step);
|
||||||
text.blit();
|
|
||||||
}
|
}
|
||||||
|
text.blit();
|
||||||
if (!paused) {
|
if (!paused) {
|
||||||
requestAnimationFrame(runFn);
|
requestAnimationFrame(runFn);
|
||||||
}
|
}
|
||||||
|
@ -255,7 +271,7 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _key: byte;
|
let _key: byte;
|
||||||
function _keydown(evt: KeyboardEvent) {
|
function _keydown(evt: KeyboardEvent) {
|
||||||
if (evt.keyCode === 112) {
|
if (evt.keyCode === 112) {
|
||||||
cpu.reset();
|
cpu.reset();
|
||||||
|
@ -263,19 +279,19 @@ function _keydown(evt: KeyboardEvent) {
|
||||||
if (document.webkitIsFullScreen) {
|
if (document.webkitIsFullScreen) {
|
||||||
document.webkitCancelFullScreen();
|
document.webkitCancelFullScreen();
|
||||||
} else {
|
} else {
|
||||||
var elem = document.getElementById("display");
|
const elem = document.getElementById('display');
|
||||||
elem!.webkitRequestFullScreen();
|
elem!.webkitRequestFullScreen();
|
||||||
}
|
}
|
||||||
} else if (evt.key === "Shift") {
|
} else if (evt.key === 'Shift') {
|
||||||
keyboard.shiftKey(true);
|
keyboard.shiftKey(true);
|
||||||
} else if (evt.key == "Control") {
|
} else if (evt.key === 'Control') {
|
||||||
keyboard.controlKey(true);
|
keyboard.controlKey(true);
|
||||||
} else if (!focused && (!evt.metaKey || evt.ctrlKey)) {
|
} else if (!focused && (!evt.metaKey || evt.ctrlKey)) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
||||||
var key = mapKeyEvent(evt);
|
const key = mapKeyEvent(evt);
|
||||||
if (key != 0xff) {
|
if (key !== 0xff) {
|
||||||
if (_key != 0xff) io.keyUp();
|
if (_key !== 0xff) io.keyUp();
|
||||||
io.keyDown(key);
|
io.keyDown(key);
|
||||||
_key = key;
|
_key = key;
|
||||||
}
|
}
|
||||||
|
@ -285,9 +301,9 @@ function _keydown(evt: KeyboardEvent) {
|
||||||
function _keyup(evt: KeyboardEvent) {
|
function _keyup(evt: KeyboardEvent) {
|
||||||
_key = 0xff;
|
_key = 0xff;
|
||||||
|
|
||||||
if (evt.key === "Shift") {
|
if (evt.key === 'Shift') {
|
||||||
keyboard.shiftKey(false);
|
keyboard.shiftKey(false);
|
||||||
} else if (evt.key === "Control") {
|
} else if (evt.key === 'Control') {
|
||||||
keyboard.controlKey(false);
|
keyboard.controlKey(false);
|
||||||
} else {
|
} else {
|
||||||
if (!focused) {
|
if (!focused) {
|
||||||
|
@ -300,9 +316,9 @@ let _updateScreenTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
export function updateScreen() {
|
export function updateScreen() {
|
||||||
const green =
|
const green =
|
||||||
document.querySelector<HTMLInputElement>("#green_screen")!.checked;
|
document.querySelector<HTMLInputElement>('#green_screen')!.checked;
|
||||||
const scanlines =
|
const scanlines =
|
||||||
document.querySelector<HTMLInputElement>("#show_scanlines")!.checked;
|
document.querySelector<HTMLInputElement>('#show_scanlines')!.checked;
|
||||||
|
|
||||||
text.green(green);
|
text.green(green);
|
||||||
text.scanlines(scanlines);
|
text.scanlines(scanlines);
|
||||||
|
@ -321,39 +337,39 @@ paused = false;
|
||||||
export function pauseRun(b: HTMLButtonElement) {
|
export function pauseRun(b: HTMLButtonElement) {
|
||||||
if (paused) {
|
if (paused) {
|
||||||
run();
|
run();
|
||||||
b.value = "Pause";
|
b.value = 'Pause';
|
||||||
} else {
|
} else {
|
||||||
stop();
|
stop();
|
||||||
b.value = "Run";
|
b.value = 'Run';
|
||||||
}
|
}
|
||||||
paused = !paused;
|
paused = !paused;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openOptions() {
|
export function openOptions() {
|
||||||
MicroModal.show("options-modal");
|
MicroModal.show('options-modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openLoadText(event?: MouseEvent) {
|
export function openLoadText(event?: MouseEvent) {
|
||||||
if (event && event.altKey) {
|
if (event && event.altKey) {
|
||||||
MicroModal.show("local-modal");
|
MicroModal.show('local-modal');
|
||||||
} else {
|
} else {
|
||||||
MicroModal.show("input-modal");
|
MicroModal.show('input-modal');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doLoadText() {
|
export function doLoadText() {
|
||||||
var text = document.querySelector<HTMLInputElement>("#text_input")!.value;
|
const text = document.querySelector<HTMLInputElement>('#text_input')!.value;
|
||||||
if (!text.indexOf("//Binary")) {
|
if (!text.indexOf('//Binary')) {
|
||||||
var lines = text.split("\n");
|
const lines = text.split('\n');
|
||||||
lines.forEach(function (line) {
|
lines.forEach(function (line) {
|
||||||
var parts = line.split(": ");
|
const parts = line.split(': ');
|
||||||
if (parts.length == 2) {
|
if (parts.length === 2) {
|
||||||
let addr: address = 0;
|
let addr: address = 0;
|
||||||
if (parts[0].length > 0) {
|
if (parts[0].length > 0) {
|
||||||
addr = parseInt(parts[0], 16);
|
addr = parseInt(parts[0], 16);
|
||||||
}
|
}
|
||||||
var data = parts[1].split(" ");
|
const data = parts[1].split(' ');
|
||||||
for (var idx = 0; idx < data.length; idx++) {
|
for (let idx = 0; idx < data.length; idx++) {
|
||||||
cpu.write(addr >> 8, addr & 0xff, parseInt(data[idx], 16));
|
cpu.write(addr >> 8, addr & 0xff, parseInt(data[idx], 16));
|
||||||
addr++;
|
addr++;
|
||||||
}
|
}
|
||||||
|
@ -362,28 +378,28 @@ export function doLoadText() {
|
||||||
} else {
|
} else {
|
||||||
io.paste(text);
|
io.paste(text);
|
||||||
}
|
}
|
||||||
MicroModal.close("input-modal");
|
MicroModal.close('input-modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleDragOver(event: DragEvent) {
|
export function handleDragOver(event: DragEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.dataTransfer!.dropEffect = "copy";
|
event.dataTransfer!.dropEffect = 'copy';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleDrop(event: DragEvent) {
|
export function handleDrop(event: DragEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
var dt = event.dataTransfer;
|
const dt = event.dataTransfer;
|
||||||
if (dt?.files && dt.files.length > 0) {
|
if (dt?.files && dt.files.length > 0) {
|
||||||
doLoadLocal(dt.files);
|
doLoadLocal(dt.files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleDragEnd(event: DragEvent) {
|
export function handleDragEnd(event: DragEvent) {
|
||||||
var dt = event.dataTransfer;
|
const dt = event.dataTransfer;
|
||||||
if (dt?.items) {
|
if (dt?.items) {
|
||||||
for (var i = 0; i < dt.items.length; i++) {
|
for (let i = 0; i < dt.items.length; i++) {
|
||||||
dt.items.remove(i);
|
dt.items.remove(i);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -393,39 +409,39 @@ export function handleDragEnd(event: DragEvent) {
|
||||||
|
|
||||||
MicroModal.init();
|
MicroModal.init();
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
hashtag = document.location.hash;
|
hashtag = document.location.hash;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Input Handling
|
* Input Handling
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const canvas = document.querySelector<HTMLCanvasElement>("#text")!;
|
const canvas = document.querySelector<HTMLCanvasElement>('#text')!;
|
||||||
const context = canvas.getContext("2d")!;
|
const context = canvas.getContext('2d')!;
|
||||||
|
|
||||||
text.setContext(context);
|
text.setContext(context);
|
||||||
|
|
||||||
window.addEventListener("keydown", _keydown);
|
window.addEventListener('keydown', _keydown);
|
||||||
window.addEventListener("keyup", _keyup);
|
window.addEventListener('keyup', _keyup);
|
||||||
|
|
||||||
window.addEventListener("paste", (event) => {
|
window.addEventListener('paste', (event) => {
|
||||||
var paste = event.clipboardData!.getData("text/plain");
|
const paste = event.clipboardData!.getData('text/plain');
|
||||||
setKeyBuffer(paste);
|
setKeyBuffer(paste);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("copy", (event) => {
|
window.addEventListener('copy', (event) => {
|
||||||
event.clipboardData?.setData("text/plain", text.getText());
|
event.clipboardData?.setData('text/plain', text.getText());
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll("input,textarea").forEach(function (el) {
|
document.querySelectorAll('input,textarea').forEach(function (el) {
|
||||||
el.addEventListener("focus", function () {
|
el.addEventListener('focus', function () {
|
||||||
focused = true;
|
focused = true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
document.querySelectorAll("input,textarea").forEach(function (el) {
|
document.querySelectorAll('input,textarea').forEach(function (el) {
|
||||||
el.addEventListener("blur", function () {
|
el.addEventListener('blur', function () {
|
||||||
focused = false;
|
focused = false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -433,58 +449,58 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
|
||||||
if (prefs.havePrefs()) {
|
if (prefs.havePrefs()) {
|
||||||
document
|
document
|
||||||
.querySelectorAll<HTMLInputElement>("input[type=checkbox]")
|
.querySelectorAll<HTMLInputElement>('input[type=checkbox]')
|
||||||
.forEach(function (el) {
|
.forEach(function (el) {
|
||||||
var val = prefs.readPref(el.id);
|
const val = prefs.readPref(el.id);
|
||||||
if (val != null) el.checked = JSON.parse(val);
|
if (val != null) el.checked = !!JSON.parse(val);
|
||||||
});
|
});
|
||||||
document
|
document
|
||||||
.querySelectorAll<HTMLInputElement>("input[type=checkbox]")
|
.querySelectorAll<HTMLInputElement>('input[type=checkbox]')
|
||||||
.forEach(function (el) {
|
.forEach(function (el) {
|
||||||
el.addEventListener("change", function () {
|
el.addEventListener('change', function () {
|
||||||
prefs.writePref(el.id, JSON.stringify(el.checked));
|
prefs.writePref(el.id, JSON.stringify(el.checked));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
turbotape = document.querySelector<HTMLInputElement>("#turbo_tape")!.checked;
|
turbotape = document.querySelector<HTMLInputElement>('#turbo_tape')!.checked;
|
||||||
|
|
||||||
Object.keys(window.tapes)
|
Object.keys(window.tapes)
|
||||||
.sort()
|
.sort()
|
||||||
.forEach(function (key) {
|
.forEach(function (key) {
|
||||||
var option = document.createElement("option");
|
const option = document.createElement('option');
|
||||||
option.value = key;
|
option.value = key;
|
||||||
option.text = key;
|
option.text = key;
|
||||||
document.querySelector("#tape_select")!.append(option);
|
document.querySelector('#tape_select')!.append(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
function doTapeSelect() {
|
function doTapeSelect() {
|
||||||
var tapeId =
|
const tapeId =
|
||||||
document.querySelector<HTMLInputElement>("#tape_select")!.value;
|
document.querySelector<HTMLInputElement>('#tape_select')!.value;
|
||||||
var tape = window.tapes[tapeId];
|
const tape = window.tapes[tapeId];
|
||||||
if (!tape) {
|
if (!tape) {
|
||||||
document.querySelector<HTMLInputElement>("#text_input")!.value = "";
|
document.querySelector<HTMLInputElement>('#text_input')!.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
debug("Loading", tapeId);
|
debug('Loading', tapeId);
|
||||||
|
|
||||||
window.location.hash = tapeId;
|
window.location.hash = tapeId;
|
||||||
reset();
|
reset();
|
||||||
if (turbotape) {
|
if (turbotape) {
|
||||||
var trackIdx = 0,
|
let trackIdx = 0,
|
||||||
script = "";
|
script = '';
|
||||||
var parts = tape.script.split("\n");
|
const parts = tape.script.split('\n');
|
||||||
// Ignore part 0 (C100R)
|
// Ignore part 0 (C100R)
|
||||||
// Split part 1 into ranges
|
// Split part 1 into ranges
|
||||||
var ranges = parts[1].split(" ");
|
const ranges = parts[1].split(' ');
|
||||||
var idx;
|
let idx;
|
||||||
for (idx = 0; idx < ranges.length; idx++) {
|
for (idx = 0; idx < ranges.length; idx++) {
|
||||||
var range = ranges[idx].split(".");
|
const range = ranges[idx].split('.');
|
||||||
var start = parseInt(range[0], 16);
|
const start = parseInt(range[0], 16);
|
||||||
var end = parseInt(range[1], 16);
|
const end = parseInt(range[1], 16);
|
||||||
var track = tape.tracks[trackIdx];
|
const track = tape.tracks[trackIdx];
|
||||||
var kdx = 0;
|
let kdx = 0;
|
||||||
for (var jdx = start; jdx <= end; jdx++) {
|
for (let jdx = start; jdx <= end; jdx++) {
|
||||||
cpu.write(jdx >> 8, jdx & 0xff, track[kdx++]);
|
cpu.write(jdx >> 8, jdx & 0xff, track[kdx++]);
|
||||||
}
|
}
|
||||||
trackIdx++;
|
trackIdx++;
|
||||||
|
@ -492,29 +508,29 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Execute parts 2-n
|
// Execute parts 2-n
|
||||||
for (idx = 2; idx < parts.length; idx++) {
|
for (idx = 2; idx < parts.length; idx++) {
|
||||||
if (parts[idx]) {
|
if (parts[idx]) {
|
||||||
script += parts[idx] + "\n";
|
script += parts[idx] + '\n';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.querySelector<HTMLInputElement>("#text_input")!.value = script;
|
document.querySelector<HTMLInputElement>('#text_input')!.value = script;
|
||||||
} else {
|
} else {
|
||||||
aci.setData(tape.tracks);
|
aci.setData(tape.tracks);
|
||||||
document.querySelector<HTMLInputElement>("#text_input")!.value =
|
document.querySelector<HTMLInputElement>('#text_input')!.value =
|
||||||
tape.script;
|
tape.script;
|
||||||
}
|
}
|
||||||
doLoadText();
|
doLoadText();
|
||||||
}
|
}
|
||||||
document
|
document
|
||||||
.querySelector("#tape_select")!
|
.querySelector('#tape_select')!
|
||||||
.addEventListener("change", doTapeSelect);
|
.addEventListener('change', doTapeSelect);
|
||||||
|
|
||||||
run();
|
run();
|
||||||
setInterval(updateKHz, 1000);
|
setInterval(updateKHz, 1000);
|
||||||
updateScreen();
|
updateScreen();
|
||||||
|
|
||||||
var tape = hup();
|
const tape = hup();
|
||||||
if (tape) {
|
if (tape) {
|
||||||
openLoadText();
|
openLoadText();
|
||||||
document.querySelector<HTMLInputElement>("#tape_select")!.value = tape;
|
document.querySelector<HTMLInputElement>('#tape_select')!.value = tape;
|
||||||
doTapeSelect();
|
doTapeSelect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
* implied warranty.
|
* implied warranty.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TextPage } from "./canvas1";
|
import { TextPage } from './canvas1';
|
||||||
import type { byte } from "./types";
|
import type { byte } from './types';
|
||||||
|
|
||||||
const LOC = {
|
const LOC = {
|
||||||
KBD: 0x10,
|
KBD: 0x10,
|
||||||
|
@ -36,20 +36,22 @@ export default class Apple1IO {
|
||||||
return 0xd0;
|
return 0xd0;
|
||||||
}
|
}
|
||||||
read(_page: byte, off: byte): byte {
|
read(_page: byte, off: byte): byte {
|
||||||
var result = 0;
|
let result = 0;
|
||||||
off &= 0x13;
|
off &= 0x13;
|
||||||
switch (off) {
|
switch (off) {
|
||||||
case LOC.KBD:
|
case LOC.KBD:
|
||||||
// Keyboard
|
{
|
||||||
const key = this._buffer.shift();
|
// Keyboard
|
||||||
if (key != null) {
|
const key = this._buffer.shift();
|
||||||
result = key.toUpperCase().charCodeAt(0) & 0x7f;
|
if (key != null) {
|
||||||
this._keyReady = this._buffer.length > 0;
|
result = key.toUpperCase().charCodeAt(0) & 0x7f;
|
||||||
} else {
|
this._keyReady = this._buffer.length > 0;
|
||||||
result = this._key;
|
} else {
|
||||||
this._keyReady = false;
|
result = this._key;
|
||||||
|
this._keyReady = false;
|
||||||
|
}
|
||||||
|
result |= 0x80;
|
||||||
}
|
}
|
||||||
result |= 0x80;
|
|
||||||
break;
|
break;
|
||||||
case LOC.KBDRDY:
|
case LOC.KBDRDY:
|
||||||
result = this._keyReady ? 0x80 : 0x00;
|
result = this._keyReady ? 0x80 : 0x00;
|
||||||
|
@ -96,9 +98,9 @@ export default class Apple1IO {
|
||||||
this._keyReady = true;
|
this._keyReady = true;
|
||||||
}
|
}
|
||||||
paste(buffer: string) {
|
paste(buffer: string) {
|
||||||
buffer = buffer.replace(/\/\/.*\n/g, "");
|
buffer = buffer.replace(/\/\/.*\n/g, '');
|
||||||
buffer = buffer.replace(/\n/g, "\r");
|
buffer = buffer.replace(/\n/g, '\r');
|
||||||
this._buffer = buffer.split("");
|
this._buffer = buffer.split('');
|
||||||
this._keyReady = true;
|
this._keyReady = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
16
js/base64.ts
16
js/base64.ts
|
@ -1,6 +1,6 @@
|
||||||
import { memory } from "./types";
|
import { memory } from './types';
|
||||||
|
|
||||||
const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||||
|
|
||||||
/** Encode an array of bytes in base64. */
|
/** Encode an array of bytes in base64. */
|
||||||
export function base64_encode(data: null | undefined): undefined;
|
export function base64_encode(data: null | undefined): undefined;
|
||||||
|
@ -37,7 +37,7 @@ export function base64_encode(
|
||||||
bits,
|
bits,
|
||||||
i = 0,
|
i = 0,
|
||||||
ac = 0,
|
ac = 0,
|
||||||
enc = "";
|
enc = '';
|
||||||
const tmp_arr = [];
|
const tmp_arr = [];
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
@ -62,14 +62,14 @@ export function base64_encode(
|
||||||
B64.charAt(h1) + B64.charAt(h2) + B64.charAt(h3) + B64.charAt(h4);
|
B64.charAt(h1) + B64.charAt(h2) + B64.charAt(h3) + B64.charAt(h4);
|
||||||
} while (i < data.length);
|
} while (i < data.length);
|
||||||
|
|
||||||
enc = tmp_arr.join("");
|
enc = tmp_arr.join('');
|
||||||
|
|
||||||
switch (data.length % 3) {
|
switch (data.length % 3) {
|
||||||
case 1:
|
case 1:
|
||||||
enc = enc.slice(0, -2) + "==";
|
enc = enc.slice(0, -2) + '==';
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
enc = enc.slice(0, -1) + "=";
|
enc = enc.slice(0, -1) + '=';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,11 +147,11 @@ export function base64_decode(
|
||||||
return new Uint8Array(tmp_arr);
|
return new Uint8Array(tmp_arr);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DATA_URL_PREFIX = "data:application/octet-stream;base64,";
|
const DATA_URL_PREFIX = 'data:application/octet-stream;base64,';
|
||||||
|
|
||||||
export function base64_json_parse(json: string): unknown {
|
export function base64_json_parse(json: string): unknown {
|
||||||
const reviver = (_key: string, value: unknown) => {
|
const reviver = (_key: string, value: unknown) => {
|
||||||
if (typeof value === "string" && value.startsWith(DATA_URL_PREFIX)) {
|
if (typeof value === 'string' && value.startsWith(DATA_URL_PREFIX)) {
|
||||||
return base64_decode(value.slice(DATA_URL_PREFIX.length));
|
return base64_decode(value.slice(DATA_URL_PREFIX.length));
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
* implied warranty.
|
* implied warranty.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { charset } from "./roms/apple1char";
|
import { charset } from './roms/apple1char';
|
||||||
import type { byte } from "./types";
|
import type { byte } from './types';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
0: A9 9 AA 20 EF FF E8 8A 4C 2 0
|
0: A9 9 AA 20 EF FF E8 8A 4C 2 0
|
||||||
|
@ -22,8 +22,8 @@ import type { byte } from "./types";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class TextPage {
|
export class TextPage {
|
||||||
_page: ImageData;
|
_page: ImageData | undefined;
|
||||||
_context: CanvasRenderingContext2D;
|
_context: CanvasRenderingContext2D | undefined;
|
||||||
|
|
||||||
_buffer: byte[][] = [];
|
_buffer: byte[][] = [];
|
||||||
_greenMode = false;
|
_greenMode = false;
|
||||||
|
@ -40,9 +40,9 @@ export class TextPage {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._buffer = [];
|
this._buffer = [];
|
||||||
for (var row = 0; row < 24; row++) {
|
for (let row = 0; row < 24; row++) {
|
||||||
this._buffer[row] = [];
|
this._buffer[row] = [];
|
||||||
for (var col = 0; col < 40; col++) {
|
for (let col = 0; col < 40; col++) {
|
||||||
this._buffer[row][col] = col % 2 ? 0x00 : 0xff;
|
this._buffer[row][col] = col % 2 ? 0x00 : 0xff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,12 +58,12 @@ export class TextPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
write(val: byte): void {
|
write(val: byte): void {
|
||||||
var col;
|
let col;
|
||||||
val &= 0x7f;
|
val &= 0x7f;
|
||||||
|
|
||||||
if (this.transcript) {
|
if (this.transcript) {
|
||||||
if (val == 0xd) {
|
if (val === 0xd) {
|
||||||
this.transcript += "\n";
|
this.transcript += '\n';
|
||||||
} else if (val >= 0x20) {
|
} else if (val >= 0x20) {
|
||||||
if (val >= 0x60) {
|
if (val >= 0x60) {
|
||||||
val &= 0x5f;
|
val &= 0x5f;
|
||||||
|
@ -72,7 +72,7 @@ export class TextPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (val == 0x0d) {
|
if (val === 0x0d) {
|
||||||
for (col = this._col; col < 40; col++) {
|
for (col = this._col; col < 40; col++) {
|
||||||
this._buffer[this._row][col] = 0x20;
|
this._buffer[this._row][col] = 0x20;
|
||||||
}
|
}
|
||||||
|
@ -98,16 +98,17 @@ export class TextPage {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
writeAt(row: byte, col: byte, val: byte): void {
|
writeAt(row: byte, col: byte, val: byte): void {
|
||||||
|
if (!this._page) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._buffer[row][col] = val;
|
this._buffer[row][col] = val;
|
||||||
var data = this._page.data,
|
const data = this._page.data;
|
||||||
fore,
|
let color;
|
||||||
back,
|
let off = (col * 14 + row * 560 * 8 * 2) * 4;
|
||||||
color;
|
|
||||||
var off = (col * 14 + row * 560 * 8 * 2) * 4;
|
|
||||||
|
|
||||||
fore = this._greenMode ? this._green : this._white;
|
let fore = this._greenMode ? this._green : this._white;
|
||||||
back = this._black;
|
const back = this._black;
|
||||||
var char = 0;
|
let char = 0;
|
||||||
|
|
||||||
if (!val) {
|
if (!val) {
|
||||||
if (this._blinking) {
|
if (this._blinking) {
|
||||||
|
@ -118,12 +119,12 @@ export class TextPage {
|
||||||
char |= val & 0x40 ? 0 : 0x20;
|
char |= val & 0x40 ? 0 : 0x20;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var jdx = 0; jdx < 8; jdx++) {
|
for (let jdx = 0; jdx < 8; jdx++) {
|
||||||
var b = charset[char * 8 + jdx];
|
let b = charset[char * 8 + jdx];
|
||||||
for (var idx = 0; idx < 7; idx += 1) {
|
for (let idx = 0; idx < 7; idx += 1) {
|
||||||
b <<= 1;
|
b <<= 1;
|
||||||
color = b & 0x80 ? fore : back;
|
color = b & 0x80 ? fore : back;
|
||||||
var c0 = color[0],
|
const c0 = color[0],
|
||||||
c1 = color[1],
|
c1 = color[1],
|
||||||
c2 = color[2];
|
c2 = color[2];
|
||||||
data[off + 0] = data[off + 4] = c0;
|
data[off + 0] = data[off + 4] = c0;
|
||||||
|
@ -144,9 +145,9 @@ export class TextPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_blink() {
|
_blink() {
|
||||||
for (var row = 0; row < 24; row++) {
|
for (let row = 0; row < 24; row++) {
|
||||||
for (var col = 0; col < 40; col++) {
|
for (let col = 0; col < 40; col++) {
|
||||||
var val = this._buffer[row][col];
|
const val = this._buffer[row][col];
|
||||||
if (!val) {
|
if (!val) {
|
||||||
this.writeAt(row, col, val);
|
this.writeAt(row, col, val);
|
||||||
}
|
}
|
||||||
|
@ -155,8 +156,8 @@ export class TextPage {
|
||||||
this._dirty = true;
|
this._dirty = true;
|
||||||
}
|
}
|
||||||
refresh() {
|
refresh() {
|
||||||
for (var row = 0; row < 24; row++) {
|
for (let row = 0; row < 24; row++) {
|
||||||
for (var col = 0; col < 40; col++) {
|
for (let col = 0; col < 40; col++) {
|
||||||
this.writeAt(row, col, this._buffer[row][col]);
|
this.writeAt(row, col, this._buffer[row][col]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,6 +172,9 @@ export class TextPage {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
blit() {
|
blit() {
|
||||||
|
if (!this._page || !this._context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this._dirty) {
|
if (this._dirty) {
|
||||||
this._context.putImageData(
|
this._context.putImageData(
|
||||||
this._page,
|
this._page,
|
||||||
|
@ -186,7 +190,7 @@ export class TextPage {
|
||||||
setContext(context: CanvasRenderingContext2D) {
|
setContext(context: CanvasRenderingContext2D) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this._page = context.createImageData(560, 384);
|
this._page = context.createImageData(560, 384);
|
||||||
for (var idx = 0; idx < 560 * 384 * 4; idx++) {
|
for (let idx = 0; idx < 560 * 384 * 4; idx++) {
|
||||||
this._page.data[idx] = 0xff;
|
this._page.data[idx] = 0xff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -202,24 +206,24 @@ export class TextPage {
|
||||||
return charCode;
|
return charCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
var buffer = "",
|
let buffer = '',
|
||||||
line,
|
line,
|
||||||
charCode;
|
charCode;
|
||||||
var row, col;
|
let row, col;
|
||||||
for (row = 0; row < 24; row++) {
|
for (row = 0; row < 24; row++) {
|
||||||
line = "";
|
line = '';
|
||||||
for (col = 0; col < 40; col++) {
|
for (col = 0; col < 40; col++) {
|
||||||
charCode = mapCharCode(this._buffer[row][col]);
|
charCode = mapCharCode(this._buffer[row][col]);
|
||||||
line += String.fromCharCode(charCode);
|
line += String.fromCharCode(charCode);
|
||||||
}
|
}
|
||||||
line = line.trimRight();
|
line = line.trimRight();
|
||||||
buffer += line + "\n";
|
buffer += line + '\n';
|
||||||
}
|
}
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
clear() {
|
clear() {
|
||||||
for (var row = 0; row < 24; row++) {
|
for (let row = 0; row < 24; row++) {
|
||||||
for (var col = 0; col < 40; col++) {
|
for (let col = 0; col < 40; col++) {
|
||||||
this._buffer[row][col] = 0x20;
|
this._buffer[row][col] = 0x20;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,5 +232,5 @@ export class TextPage {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
transcript = "";
|
transcript = '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import CPU6502 from "../cpu6502";
|
import CPU6502 from '../cpu6502';
|
||||||
import { debug } from "../util";
|
import { debug } from '../util';
|
||||||
import { byte } from "../types";
|
import { byte } from '../types';
|
||||||
|
|
||||||
const rom = [
|
const rom = [
|
||||||
0xa9, 0xaa, 0x20, 0xef, 0xff, 0xa9, 0x8d, 0x20, 0xef, 0xff, 0xa0, 0xff, 0xc8,
|
0xa9, 0xaa, 0x20, 0xef, 0xff, 0xa9, 0x8d, 0x20, 0xef, 0xff, 0xa0, 0xff, 0xc8,
|
||||||
|
@ -50,19 +50,18 @@ export default class ACI {
|
||||||
return 0xc1;
|
return 0xc1;
|
||||||
}
|
}
|
||||||
read(page: byte, off: byte) {
|
read(page: byte, off: byte) {
|
||||||
var now = this.cpu.getCycles();
|
const now = this.cpu.getCycles();
|
||||||
var result = rom[off];
|
let result = rom[off];
|
||||||
if (page == 0xc0) {
|
if (page === 0xc0) {
|
||||||
if (this._recording) {
|
if (this._recording) {
|
||||||
var delta = now - this._last;
|
const delta = now - this._last;
|
||||||
this.buffer.push(delta);
|
this.buffer.push(delta);
|
||||||
this._last = now;
|
this._last = now;
|
||||||
} else {
|
} else {
|
||||||
var progress;
|
|
||||||
if (this._readOffset < this.buffer.length) {
|
if (this._readOffset < this.buffer.length) {
|
||||||
if (now > this._next) {
|
if (now > this._next) {
|
||||||
if (this._readOffset % 1000 == 0) {
|
if (this._readOffset % 1000 === 0) {
|
||||||
debug("Read " + this._readOffset / 1000);
|
debug('Read ' + this._readOffset / 1000);
|
||||||
}
|
}
|
||||||
this._flip = !this._flip;
|
this._flip = !this._flip;
|
||||||
this._next = now + this.buffer[this._readOffset++];
|
this._next = now + this.buffer[this._readOffset++];
|
||||||
|
@ -70,9 +69,9 @@ export default class ACI {
|
||||||
}
|
}
|
||||||
result = this._flip ? rom[off | 0x01] : rom[off & 0xfe];
|
result = this._flip ? rom[off | 0x01] : rom[off & 0xfe];
|
||||||
|
|
||||||
progress =
|
const progress =
|
||||||
Math.round((this._readOffset / this.buffer.length) * 100) / 100;
|
Math.round((this._readOffset / this.buffer.length) * 100) / 100;
|
||||||
if (this._progress != progress) {
|
if (this._progress !== progress) {
|
||||||
this._progress = progress;
|
this._progress = progress;
|
||||||
this.cb.progress(this._progress);
|
this.cb.progress(this._progress);
|
||||||
}
|
}
|
||||||
|
@ -83,14 +82,14 @@ export default class ACI {
|
||||||
case 0x00:
|
case 0x00:
|
||||||
this._recording = false;
|
this._recording = false;
|
||||||
this._beKind = true;
|
this._beKind = true;
|
||||||
debug("Entering ACI CLI");
|
debug('Entering ACI CLI');
|
||||||
break;
|
break;
|
||||||
case 0x63:
|
case 0x63:
|
||||||
if (this._recording) {
|
if (this._recording) {
|
||||||
this.buffer.push(5000000);
|
this.buffer.push(5000000);
|
||||||
this._recording = false;
|
this._recording = false;
|
||||||
}
|
}
|
||||||
debug("Exiting ACI CLI");
|
debug('Exiting ACI CLI');
|
||||||
break;
|
break;
|
||||||
case 0x70: // WRITE
|
case 0x70: // WRITE
|
||||||
this._recording = true;
|
this._recording = true;
|
||||||
|
@ -98,7 +97,7 @@ export default class ACI {
|
||||||
this._beKind = false;
|
this._beKind = false;
|
||||||
this.buffer = [];
|
this.buffer = [];
|
||||||
}
|
}
|
||||||
debug("Start write");
|
debug('Start write');
|
||||||
this._last = now;
|
this._last = now;
|
||||||
break;
|
break;
|
||||||
//case 0x7c: // WBITLOOP:
|
//case 0x7c: // WBITLOOP:
|
||||||
|
@ -107,7 +106,7 @@ export default class ACI {
|
||||||
// break;
|
// break;
|
||||||
case 0x8d: // READ
|
case 0x8d: // READ
|
||||||
this._recording = false;
|
this._recording = false;
|
||||||
debug("Start read");
|
debug('Start read');
|
||||||
if (this._beKind) {
|
if (this._beKind) {
|
||||||
this._readOffset = 0;
|
this._readOffset = 0;
|
||||||
this._next = now + 5000000;
|
this._next = now + 5000000;
|
||||||
|
@ -131,7 +130,7 @@ export default class ACI {
|
||||||
setState() {}
|
setState() {}
|
||||||
|
|
||||||
setData(data: number[][]) {
|
setData(data: number[][]) {
|
||||||
var seg, idx, jdx, d, b;
|
let seg, idx, jdx, d, b;
|
||||||
this.buffer = [];
|
this.buffer = [];
|
||||||
for (seg = 0; seg < data.length; seg++) {
|
for (seg = 0; seg < data.length; seg++) {
|
||||||
for (idx = 0; idx < 16384; idx++) {
|
for (idx = 0; idx < 16384; idx++) {
|
||||||
|
|
2114
js/cpu6502.ts
2114
js/cpu6502.ts
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1,3 @@
|
||||||
var Apple1 = require('./apple1');
|
import * as Apple1 from './apple1';
|
||||||
|
|
||||||
module.exports = { Apple1: Apple1 };
|
window.Apple1 = Apple1;
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
export default class Prefs {
|
export default class Prefs {
|
||||||
havePrefs() {
|
havePrefs() {
|
||||||
return typeof localStorage !== "undefined";
|
return typeof localStorage !== 'undefined';
|
||||||
}
|
}
|
||||||
readPref(name: string) {
|
readPref(name: string) {
|
||||||
if (localStorage) return localStorage.getItem(name);
|
if (localStorage) return localStorage.getItem(name);
|
||||||
|
|
10
js/ram.ts
10
js/ram.ts
|
@ -9,9 +9,9 @@
|
||||||
* implied warranty.
|
* implied warranty.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { base64_decode, base64_encode } from "./base64";
|
import { base64_decode, base64_encode } from './base64';
|
||||||
import { allocMemPages } from "./util";
|
import { allocMemPages } from './util';
|
||||||
import type { byte } from "./types";
|
import type { byte } from './types';
|
||||||
|
|
||||||
export interface RAMState {
|
export interface RAMState {
|
||||||
start: byte;
|
start: byte;
|
||||||
|
@ -25,8 +25,8 @@ export default class RAM {
|
||||||
constructor(private start_page: byte, private end_page: byte) {
|
constructor(private start_page: byte, private end_page: byte) {
|
||||||
this.mem = allocMemPages(end_page - start_page + 1);
|
this.mem = allocMemPages(end_page - start_page + 1);
|
||||||
|
|
||||||
for (var page = 0; page <= end_page; page++) {
|
for (let page = 0; page <= end_page; page++) {
|
||||||
for (var off = 0; off < 0x100; off++) {
|
for (let off = 0; off < 0x100; off++) {
|
||||||
this.mem[page * 0x100 + off] = 0; // Math.floor(Math.random()*256);
|
this.mem[page * 0x100 + off] = 0; // Math.floor(Math.random()*256);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { byte } from "js/types";
|
import { byte } from 'js/types';
|
||||||
|
|
||||||
export default class Basic {
|
export default class Basic {
|
||||||
ram = [
|
ram = [
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { byte } from "js/types";
|
import { byte } from 'js/types';
|
||||||
|
|
||||||
export default class Bios {
|
export default class Bios {
|
||||||
rom = [
|
rom = [
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { byte } from "js/types";
|
import { byte } from 'js/types';
|
||||||
|
|
||||||
export default class EnhancedBasic {
|
export default class EnhancedBasic {
|
||||||
rom = [
|
rom = [
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { byte } from "js/types";
|
import { byte } from 'js/types';
|
||||||
|
|
||||||
export default class Krusader {
|
export default class Krusader {
|
||||||
rom = [
|
rom = [
|
||||||
|
|
|
@ -9,16 +9,16 @@
|
||||||
* implied warranty.
|
* implied warranty.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { address } from "./types";
|
import type { address } from './types';
|
||||||
|
|
||||||
export const SYMBOLS: Record<address, string> = {
|
export const SYMBOLS: Record<address, string> = {
|
||||||
0xd010: "KBD",
|
0xd010: 'KBD',
|
||||||
0xd011: "KBDCR",
|
0xd011: 'KBDCR',
|
||||||
0xd012: "DSP",
|
0xd012: 'DSP',
|
||||||
0xd013: "DSPCR",
|
0xd013: 'DSPCR',
|
||||||
|
|
||||||
0xff1f: "GETLINE",
|
0xff1f: 'GETLINE',
|
||||||
0xffef: "ECHO",
|
0xffef: 'ECHO',
|
||||||
0xffdc: "PRBYTE",
|
0xffdc: 'PRBYTE',
|
||||||
0xffe5: "PRHEX",
|
0xffe5: 'PRHEX',
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,11 +9,11 @@
|
||||||
* implied warranty.
|
* implied warranty.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import CPU6502 from "js/cpu6502";
|
import CPU6502 from 'js/cpu6502';
|
||||||
import { debug, toHex } from "../util";
|
import { debug, toHex } from '../util';
|
||||||
import Apple1IO from "js/apple1io";
|
import Apple1IO from 'js/apple1io';
|
||||||
import { TextPage } from "js/canvas1";
|
import { TextPage } from 'js/canvas1';
|
||||||
import { byte } from "js/types";
|
import { byte } from 'js/types';
|
||||||
|
|
||||||
// keycode: [plain, cntl, shift]
|
// keycode: [plain, cntl, shift]
|
||||||
|
|
||||||
|
@ -140,29 +140,29 @@ const keymap: Record<byte, readonly byte[]> = {
|
||||||
|
|
||||||
const keys = [
|
const keys = [
|
||||||
[
|
[
|
||||||
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", ":", "-", "RESET"],
|
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', ':', '-', 'RESET'],
|
||||||
["ESC", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "FEED", "RETURN"],
|
['ESC', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'FEED', 'RETURN'],
|
||||||
["CTRL", "A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "OUT", "CLS"],
|
['CTRL', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', 'OUT', 'CLS'],
|
||||||
["SHIFT", "Z", "X", "C", "V", "B", "N", "M", ",", ".", "/", "SHIFT"],
|
['SHIFT', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', 'SHIFT'],
|
||||||
[" "],
|
[' '],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
["!", '"', "#", "$", "%", "&", "'", "(", ")", "0", "*", "=", "RESET"],
|
['!', '"', '#', '$', '%', '&', "'", '(', ')', '0', '*', '=', 'RESET'],
|
||||||
["ESC", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "@", "LINE", "RETURN"],
|
['ESC', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', '@', 'LINE', 'RETURN'],
|
||||||
["CTRL", "A", "S", "D", "F", "BELL", "H", "J", "K", "L", "+", "RUB", "CLS"],
|
['CTRL', 'A', 'S', 'D', 'F', 'BELL', 'H', 'J', 'K', 'L', '+', 'RUB', 'CLS'],
|
||||||
["SHIFT", "Z", "X", "C", "V", "B", "^", "M", "<", ">", "?", "SHIFT"],
|
['SHIFT', 'Z', 'X', 'C', 'V', 'B', '^', 'M', '<', '>', '?', 'SHIFT'],
|
||||||
[" "],
|
[' '],
|
||||||
],
|
],
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function mapKeyEvent(evt: KeyboardEvent) {
|
export function mapKeyEvent(evt: KeyboardEvent) {
|
||||||
var code = evt.keyCode;
|
const code = evt.keyCode;
|
||||||
|
|
||||||
if (code in keymap) {
|
if (code in keymap) {
|
||||||
return keymap[code][evt.shiftKey ? 2 : evt.ctrlKey ? 1 : 0];
|
return keymap[code][evt.shiftKey ? 2 : evt.ctrlKey ? 1 : 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
debug("Unhandled key = " + toHex(code));
|
debug('Unhandled key = ' + toHex(code));
|
||||||
return 0xff;
|
return 0xff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,76 +182,74 @@ export class KeyBoard {
|
||||||
|
|
||||||
shiftKey(down: boolean) {
|
shiftKey(down: boolean) {
|
||||||
this.shifted = down;
|
this.shifted = down;
|
||||||
this.kb.querySelectorAll(".key-SHIFT").forEach(function (el) {
|
this.kb.querySelectorAll('.key-SHIFT').forEach(function (el) {
|
||||||
if (down) {
|
if (down) {
|
||||||
el.classList.add("active");
|
el.classList.add('active');
|
||||||
} else {
|
} else {
|
||||||
el.classList.remove("active");
|
el.classList.remove('active');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
controlKey(down: boolean) {
|
controlKey(down: boolean) {
|
||||||
this.controlled = down;
|
this.controlled = down;
|
||||||
this.kb.querySelectorAll(".key-CTRL").forEach(function (el) {
|
this.kb.querySelectorAll('.key-CTRL').forEach(function (el) {
|
||||||
if (down) {
|
if (down) {
|
||||||
el.classList.add("active");
|
el.classList.add('active');
|
||||||
} else {
|
} else {
|
||||||
el.classList.remove("active");
|
el.classList.remove('active');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
var x, y, row, key, key1, key2, label, label1, label2;
|
let x, y, row, key, key1, key2, label, label1, label2;
|
||||||
|
|
||||||
function buildLabel(k: string) {
|
function buildLabel(k: string) {
|
||||||
var span = document.createElement("span");
|
const span = document.createElement('span');
|
||||||
span.innerHTML = k;
|
span.innerHTML = k;
|
||||||
if (k.length > 1 && !k.startsWith("&")) {
|
if (k.length > 1 && !k.startsWith('&')) {
|
||||||
span.classList.add("small");
|
span.classList.add('small');
|
||||||
}
|
}
|
||||||
return span;
|
return span;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _mouseup = (
|
const _mouseup = (event: Event) => {
|
||||||
event: MouseEvent & {
|
if (!(event.currentTarget instanceof HTMLElement)) {
|
||||||
currentTarget: HTMLElement;
|
return;
|
||||||
}
|
}
|
||||||
) => {
|
event.currentTarget.classList.remove('pressed');
|
||||||
event.currentTarget.classList.remove("pressed");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const _mousedown = (
|
const _mousedown = (event: Event) => {
|
||||||
event: MouseEvent & {
|
if (!(event.currentTarget instanceof HTMLElement)) {
|
||||||
currentTarget: HTMLElement;
|
return;
|
||||||
}
|
}
|
||||||
) => {
|
event.currentTarget.classList.add('pressed');
|
||||||
event.currentTarget.classList.add("pressed");
|
let key = event.currentTarget.dataset[this.shifted ? 'key2' : 'key1'];
|
||||||
var key = event.currentTarget.dataset[this.shifted ? "key2" : "key1"];
|
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "BELL":
|
case 'BELL':
|
||||||
key = "G";
|
key = 'G';
|
||||||
break;
|
break;
|
||||||
case "RETURN":
|
case 'RETURN':
|
||||||
key = "\r";
|
key = '\r';
|
||||||
break;
|
break;
|
||||||
case "LINE":
|
case 'LINE':
|
||||||
case "FEED":
|
case 'FEED':
|
||||||
key = "\n";
|
key = '\n';
|
||||||
break;
|
break;
|
||||||
case "RUB":
|
case 'RUB':
|
||||||
case "OUT":
|
case 'OUT':
|
||||||
key = "_"; // 0x5f
|
key = '_'; // 0x5f
|
||||||
break;
|
break;
|
||||||
case " ":
|
case ' ':
|
||||||
key = " ";
|
key = ' ';
|
||||||
break;
|
break;
|
||||||
case "ESC":
|
case 'ESC':
|
||||||
key = "\0x1b";
|
key = '\0x1b';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@ -259,31 +257,31 @@ export class KeyBoard {
|
||||||
|
|
||||||
if (key.length > 1) {
|
if (key.length > 1) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "SHIFT":
|
case 'SHIFT':
|
||||||
this.shifted = !this.shifted;
|
this.shifted = !this.shifted;
|
||||||
this.kb
|
this.kb
|
||||||
.querySelectorAll(".key-SHIFT")
|
.querySelectorAll<HTMLElement>('.key-SHIFT')
|
||||||
.forEach(function (el: HTMLElement) {
|
.forEach(function (el: HTMLElement) {
|
||||||
el.classList.toggle("active");
|
el.classList.toggle('active');
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "CTRL":
|
case 'CTRL':
|
||||||
this.controlled = !this.controlled;
|
this.controlled = !this.controlled;
|
||||||
this.kb.querySelectorAll(".key-CTRL").forEach(function (el) {
|
this.kb.querySelectorAll('.key-CTRL').forEach(function (el) {
|
||||||
el.classList.toggle("active");
|
el.classList.toggle('active');
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "RESET":
|
case 'RESET':
|
||||||
this.cpu.reset();
|
this.cpu.reset();
|
||||||
break;
|
break;
|
||||||
case "CLS":
|
case 'CLS':
|
||||||
this.text.clear();
|
this.text.clear();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.controlled && key >= "@" && key <= "_") {
|
if (this.controlled && key >= '@' && key <= '_') {
|
||||||
this.io.keyDown(key.charCodeAt(0) - 0x40);
|
this.io.keyDown(key.charCodeAt(0) - 0x40);
|
||||||
} else {
|
} else {
|
||||||
this.io.keyDown(key.charCodeAt(0));
|
this.io.keyDown(key.charCodeAt(0));
|
||||||
|
@ -292,46 +290,46 @@ export class KeyBoard {
|
||||||
};
|
};
|
||||||
|
|
||||||
for (y = 0; y < 5; y++) {
|
for (y = 0; y < 5; y++) {
|
||||||
row = document.createElement("div");
|
row = document.createElement('div');
|
||||||
row.classList.add("row", "row" + y);
|
row.classList.add('row', 'row' + y);
|
||||||
this.kb.append(row);
|
this.kb.append(row);
|
||||||
for (x = 0; x < keys[0][y].length; x++) {
|
for (x = 0; x < keys[0][y].length; x++) {
|
||||||
key1 = keys[0][y][x];
|
key1 = keys[0][y][x];
|
||||||
key2 = keys[1][y][x];
|
key2 = keys[1][y][x];
|
||||||
|
|
||||||
label = document.createElement("div");
|
label = document.createElement('div');
|
||||||
label1 = buildLabel(key1);
|
label1 = buildLabel(key1);
|
||||||
label2 = buildLabel(key2);
|
label2 = buildLabel(key2);
|
||||||
|
|
||||||
key = document.createElement("div");
|
key = document.createElement('div');
|
||||||
key.classList.add("key", "key-" + key1.replace(/[&;]/g, ""));
|
key.classList.add('key', 'key-' + key1.replace(/[&;]/g, ''));
|
||||||
|
|
||||||
if (key1.length > 1) {
|
if (key1.length > 1) {
|
||||||
if (key1 != key2) {
|
if (key1 !== key2) {
|
||||||
key.classList.add("vcenter2");
|
key.classList.add('vcenter2');
|
||||||
} else {
|
} else {
|
||||||
key.classList.add("vcenter");
|
key.classList.add('vcenter');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key1 != key2) {
|
if (key1 !== key2) {
|
||||||
key.classList.add("key-" + key2.replace(/[&;]/g, ""));
|
key.classList.add('key-' + key2.replace(/[&;]/g, ''));
|
||||||
label.append(label2);
|
label.append(label2);
|
||||||
label.append(document.createElement("br"));
|
label.append(document.createElement('br'));
|
||||||
}
|
}
|
||||||
label.append(label1);
|
label.append(label1);
|
||||||
key.append(label);
|
key.append(label);
|
||||||
key.dataset["key1"] = key1;
|
key.dataset['key1'] = key1;
|
||||||
key.dataset["key2"] = key2;
|
key.dataset['key2'] = key2;
|
||||||
|
|
||||||
if (window.ontouchstart === undefined) {
|
if (window.ontouchstart === undefined) {
|
||||||
key.addEventListener("mousedown", _mousedown);
|
key.addEventListener('mousedown', _mousedown);
|
||||||
key.addEventListener("mouseup", _mouseup);
|
key.addEventListener('mouseup', _mouseup);
|
||||||
key.addEventListener("mouseout", _mouseup);
|
key.addEventListener('mouseout', _mouseup);
|
||||||
} else {
|
} else {
|
||||||
key.addEventListener("touchstart", _mousedown);
|
key.addEventListener('touchstart', _mousedown);
|
||||||
key.addEventListener("touchend", _mouseup);
|
key.addEventListener('touchend', _mouseup);
|
||||||
key.addEventListener("touchleave", _mouseup);
|
key.addEventListener('touchleave', _mouseup);
|
||||||
}
|
}
|
||||||
|
|
||||||
row.append(key);
|
row.append(key);
|
||||||
|
|
22
js/util.ts
22
js/util.ts
|
@ -9,13 +9,13 @@
|
||||||
* implied warranty.
|
* implied warranty.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { byte, word } from "./types";
|
import { byte, word } from './types';
|
||||||
|
|
||||||
var hex_digits = "0123456789ABCDEF";
|
const hex_digits = '0123456789ABCDEF';
|
||||||
var bin_digits = "01";
|
const bin_digits = '01';
|
||||||
|
|
||||||
export function allocMem(size: word) {
|
export function allocMem(size: word) {
|
||||||
var result;
|
let result;
|
||||||
if (window.Uint8Array) {
|
if (window.Uint8Array) {
|
||||||
result = new Uint8Array(size);
|
result = new Uint8Array(size);
|
||||||
} else {
|
} else {
|
||||||
|
@ -37,8 +37,8 @@ export function toHex(v: byte, n?: 2 | 4) {
|
||||||
if (!n) {
|
if (!n) {
|
||||||
n = v < 256 ? 2 : 4;
|
n = v < 256 ? 2 : 4;
|
||||||
}
|
}
|
||||||
var result = "";
|
let result = '';
|
||||||
for (var idx = 0; idx < n; idx++) {
|
for (let idx = 0; idx < n; idx++) {
|
||||||
result = hex_digits[v & 0x0f] + result;
|
result = hex_digits[v & 0x0f] + result;
|
||||||
v >>= 4;
|
v >>= 4;
|
||||||
}
|
}
|
||||||
|
@ -46,8 +46,8 @@ export function toHex(v: byte, n?: 2 | 4) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toBinary(v: byte) {
|
export function toBinary(v: byte) {
|
||||||
var result = "";
|
let result = '';
|
||||||
for (var idx = 0; idx < 8; idx++) {
|
for (let idx = 0; idx < 8; idx++) {
|
||||||
result = bin_digits[v & 0x01] + result;
|
result = bin_digits[v & 0x01] + result;
|
||||||
v >>= 1;
|
v >>= 1;
|
||||||
}
|
}
|
||||||
|
@ -60,8 +60,8 @@ export function gup(name: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hup() {
|
export function hup() {
|
||||||
var regex = new RegExp("#(.*)");
|
const regex = new RegExp('#(.*)');
|
||||||
var results = regex.exec(window.location.hash);
|
const results = regex.exec(window.location.hash);
|
||||||
if (!results) return "";
|
if (!results) return '';
|
||||||
else return decodeURIComponent(results[1]);
|
else return decodeURIComponent(results[1]);
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -29,11 +29,14 @@
|
||||||
"@babel/core": "^7.4.0",
|
"@babel/core": "^7.4.0",
|
||||||
"@babel/preset-env": "^7.4.2",
|
"@babel/preset-env": "^7.4.2",
|
||||||
"@types/micromodal": "^0.3.3",
|
"@types/micromodal": "^0.3.3",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||||
"ajv": "^6.9.2",
|
"ajv": "^6.9.2",
|
||||||
"babel-jest": "^26.3.0",
|
"babel-jest": "^29.5.0",
|
||||||
"eslint": "^8.3.0",
|
"eslint": "^8.3.0",
|
||||||
"jest": "^27.3.1",
|
"eslint-plugin-jest": "^27.2.3",
|
||||||
|
"jest": "^29.5.0",
|
||||||
"node-forge": "^1.3.0",
|
"node-forge": "^1.3.0",
|
||||||
|
"ts-jest": "^29.1.1",
|
||||||
"ts-loader": "^9.4.4",
|
"ts-loader": "^9.4.4",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^5.1.6",
|
||||||
"webpack": "^5.64.4",
|
"webpack": "^5.64.4",
|
||||||
|
@ -41,6 +44,7 @@
|
||||||
"webpack-dev-server": "^4.6.0"
|
"webpack-dev-server": "^4.6.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/jest": "^29.5.3",
|
||||||
"micromodal": "^0.4.9"
|
"micromodal": "^0.4.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
|
|
||||||
import CPU6502 from '../js/cpu6502';
|
|
||||||
import Test6502 from './roms/6502test';
|
|
||||||
import Test65C02 from './roms/65C02test';
|
|
||||||
|
|
||||||
import { toHex } from '../js/util';
|
|
||||||
|
|
||||||
describe.skip('CPU', function () {
|
|
||||||
var cpu;
|
|
||||||
var lastPC = 0;
|
|
||||||
var done = false;
|
|
||||||
|
|
||||||
function traceCB() {
|
|
||||||
var pc = cpu.getPC();
|
|
||||||
done = lastPC == pc;
|
|
||||||
lastPC = pc;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('6502', function () {
|
|
||||||
it('completes the test ROM', function () {
|
|
||||||
cpu = new CPU6502();
|
|
||||||
var test = new Test6502();
|
|
||||||
cpu.addPageHandler(test);
|
|
||||||
cpu.setPC(0x400);
|
|
||||||
|
|
||||||
do {
|
|
||||||
cpu.stepCyclesDebug(1000, traceCB);
|
|
||||||
} while (!done);
|
|
||||||
|
|
||||||
expect(toHex(lastPC)).toEqual(toHex(0x3469));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('65C02', function () {
|
|
||||||
it('completes the test ROM', function () {
|
|
||||||
cpu = new CPU6502({'65C02': true});
|
|
||||||
var test = new Test65C02();
|
|
||||||
cpu.addPageHandler(test);
|
|
||||||
cpu.setPC(0x400);
|
|
||||||
|
|
||||||
do {
|
|
||||||
cpu.stepCyclesDebug(1000, traceCB);
|
|
||||||
} while (!done);
|
|
||||||
|
|
||||||
expect(toHex(lastPC)).toEqual(toHex(0x24f1));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import CPU6502, { FLAVOR_ROCKWELL_65C02 } from '../js/cpu6502';
|
||||||
|
// From https://github.com/Klaus2m5/6502_65C02_functional_tests
|
||||||
|
import Test6502 from './roms/6502test';
|
||||||
|
import Test65C02 from './roms/65C02test';
|
||||||
|
|
||||||
|
import { toHex } from '../js/util';
|
||||||
|
|
||||||
|
describe('CPU', function () {
|
||||||
|
let cpu: CPU6502;
|
||||||
|
let lastPC = 0;
|
||||||
|
let done = false;
|
||||||
|
|
||||||
|
function traceCB() {
|
||||||
|
const pc = cpu.getPC();
|
||||||
|
done = lastPC === pc;
|
||||||
|
lastPC = pc;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('6502', function () {
|
||||||
|
it('completes the test ROM', function () {
|
||||||
|
cpu = new CPU6502();
|
||||||
|
const test = new Test6502();
|
||||||
|
cpu.addPageHandler(test);
|
||||||
|
cpu.setPC(0x400);
|
||||||
|
|
||||||
|
do {
|
||||||
|
cpu.stepCyclesDebug(1000, traceCB);
|
||||||
|
} while (!done);
|
||||||
|
|
||||||
|
expect(toHex(lastPC)).toEqual(toHex(0x3469));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('65C02', function () {
|
||||||
|
it('completes the test ROM', function () {
|
||||||
|
cpu = new CPU6502({ flavor: FLAVOR_ROCKWELL_65C02 });
|
||||||
|
const test = new Test65C02();
|
||||||
|
cpu.addPageHandler(test);
|
||||||
|
cpu.setPC(0x400);
|
||||||
|
|
||||||
|
do {
|
||||||
|
cpu.stepCyclesDebug(1000, traceCB);
|
||||||
|
} while (!done);
|
||||||
|
|
||||||
|
expect(toHex(lastPC)).toEqual(toHex(0x24f1));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
2240
test/cpu6502.spec.js
2240
test/cpu6502.spec.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,29 @@
|
||||||
|
// From https://github.com/Klaus2m5/6502_65C02_functional_tests
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { MemoryPages, byte } from '../../js/types';
|
||||||
|
|
||||||
|
export default class Test6502 implements MemoryPages {
|
||||||
|
private data: Buffer;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.data = fs.readFileSync(path.join(__dirname, '6502_functional_test.bin'));
|
||||||
|
}
|
||||||
|
|
||||||
|
start = () => {
|
||||||
|
return 0x00;
|
||||||
|
};
|
||||||
|
|
||||||
|
end = () => {
|
||||||
|
return 0xff;
|
||||||
|
};
|
||||||
|
|
||||||
|
read = (page: byte, off: byte) => {
|
||||||
|
return this.data[page << 8 | off];
|
||||||
|
};
|
||||||
|
|
||||||
|
write = (page: byte, off: byte, val: byte) => {
|
||||||
|
this.data[page << 8 | off] = val;
|
||||||
|
};
|
||||||
|
}
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,29 @@
|
||||||
|
// From https://github.com/Klaus2m5/6502_65C02_functional_tests
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { MemoryPages, byte } from '../../js/types';
|
||||||
|
|
||||||
|
export default class Test65C02 implements MemoryPages {
|
||||||
|
private data: Buffer;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.data = fs.readFileSync(path.join(__dirname, '65C02_extended_opcodes_test.bin'));
|
||||||
|
}
|
||||||
|
|
||||||
|
start = () => {
|
||||||
|
return 0x00;
|
||||||
|
};
|
||||||
|
|
||||||
|
end = () => {
|
||||||
|
return 0xff;
|
||||||
|
};
|
||||||
|
|
||||||
|
read = (page: byte, off: byte) => {
|
||||||
|
return this.data[page << 8 | off];
|
||||||
|
};
|
||||||
|
|
||||||
|
write = (page: byte, off: byte, val: byte) => {
|
||||||
|
this.data[page << 8 | off] = val;
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { byte } from "../../js/types";
|
||||||
|
|
||||||
|
export const assertByte = (b: byte) => {
|
||||||
|
expect(b <= 0xff).toEqual(true);
|
||||||
|
expect(b >= 0x00).toEqual(true);
|
||||||
|
};
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { MemoryPages, byte } from "../../js/types";
|
||||||
|
import { assertByte } from "./asserts";
|
||||||
|
|
||||||
|
export class Program implements MemoryPages {
|
||||||
|
private data: Buffer;
|
||||||
|
|
||||||
|
constructor(private page: byte, code: byte[]) {
|
||||||
|
this.data = Buffer.from(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
return this.page;
|
||||||
|
}
|
||||||
|
|
||||||
|
end() {
|
||||||
|
return this.page;
|
||||||
|
}
|
||||||
|
|
||||||
|
read(page: byte, off: byte) {
|
||||||
|
assertByte(page);
|
||||||
|
assertByte(off);
|
||||||
|
return this.data[off];
|
||||||
|
}
|
||||||
|
|
||||||
|
write(_page: byte, _off: byte, _val: byte) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bios = new Program(
|
||||||
|
0xff,
|
||||||
|
[
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x45, 0x4c, 0x4c,
|
||||||
|
0x4f, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
|
||||||
|
0x00, 0x04, 0x00, 0xff,
|
||||||
|
]
|
||||||
|
);
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { flags, CpuState } from "js/cpu6502";
|
||||||
|
import { byte } from "js/types";
|
||||||
|
import { toHex } from "js/util";
|
||||||
|
|
||||||
|
export const dumpStatusRegister = (sr: byte) =>
|
||||||
|
[
|
||||||
|
sr & flags.N ? "N" : "-",
|
||||||
|
sr & flags.V ? "V" : "-",
|
||||||
|
sr & flags.X ? "X" : "-",
|
||||||
|
sr & flags.B ? "B" : "-",
|
||||||
|
sr & flags.D ? "D" : "-",
|
||||||
|
sr & flags.I ? "I" : "-",
|
||||||
|
sr & flags.Z ? "Z" : "-",
|
||||||
|
sr & flags.C ? "C" : "-",
|
||||||
|
].join("");
|
||||||
|
|
||||||
|
const detail = !!process.env.JEST_DETAIL;
|
||||||
|
|
||||||
|
export function toReadableState(state: CpuState) {
|
||||||
|
if (detail) {
|
||||||
|
const { pc, sp, a, x, y, s } = state;
|
||||||
|
|
||||||
|
return {
|
||||||
|
pc: toHex(pc, 4),
|
||||||
|
sp: toHex(sp),
|
||||||
|
a: toHex(a),
|
||||||
|
x: toHex(x),
|
||||||
|
y: toHex(y),
|
||||||
|
s: dumpStatusRegister(s),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { MemoryPages, byte, word } from "js/types";
|
||||||
|
import { assertByte } from "./asserts";
|
||||||
|
|
||||||
|
export type Log = [address: word, value: byte, types: "read" | "write"];
|
||||||
|
export class TestMemory implements MemoryPages {
|
||||||
|
private data: Buffer;
|
||||||
|
private logging: boolean = false;
|
||||||
|
private log: Log[] = [];
|
||||||
|
|
||||||
|
constructor(private size: number) {
|
||||||
|
this.data = Buffer.alloc(size << 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
end() {
|
||||||
|
return this.size - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
read(page: byte, off: byte) {
|
||||||
|
assertByte(page);
|
||||||
|
assertByte(off);
|
||||||
|
|
||||||
|
const val = this.data[(page << 8) | off];
|
||||||
|
if (this.logging) {
|
||||||
|
this.log.push([(page << 8) | off, val, "read"]);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
write(page: byte, off: byte, val: byte) {
|
||||||
|
assertByte(page);
|
||||||
|
assertByte(off);
|
||||||
|
assertByte(val);
|
||||||
|
|
||||||
|
if (this.logging) {
|
||||||
|
this.log.push([(page << 8) | off, val, "write"]);
|
||||||
|
}
|
||||||
|
this.data[(page << 8) | off] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.log = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
logStart() {
|
||||||
|
this.log = [];
|
||||||
|
this.logging = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
logStop() {
|
||||||
|
this.logging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLog() {
|
||||||
|
return this.log;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,15 +8,13 @@
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"lib": ["DOM", "ES6"],
|
"lib": ["DOM", "ES6"],
|
||||||
"noImplicitAny": true,
|
"strict": true,
|
||||||
"noImplicitThis": true,
|
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"exactOptionalPropertyTypes": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strictNullChecks": true,
|
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
|
|
@ -9,9 +9,6 @@ module.exports =
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve('dist/'),
|
path: path.resolve('dist/'),
|
||||||
library: 'Apple1',
|
|
||||||
libraryExport: 'Apple1',
|
|
||||||
libraryTarget: 'var'
|
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
|
Loading…
Reference in New Issue