mirror of
https://github.com/whscullin/apple1js.git
synced 2024-12-12 14:30:44 +00:00
commit
6e49066162
@ -10,6 +10,10 @@ trim_trailing_whitespace = true
|
||||
[*.js]
|
||||
indent_size = 4
|
||||
|
||||
[*.ts]
|
||||
indent_size = 2
|
||||
quote_type = single
|
||||
|
||||
[*.html]
|
||||
indent_size = 2
|
||||
|
||||
|
212
.eslintrc.json
212
.eslintrc.json
@ -1,20 +1,53 @@
|
||||
{
|
||||
// Global
|
||||
"root": true,
|
||||
"plugins": [
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:jest/recommended"
|
||||
],
|
||||
"globals": {
|
||||
"tapes": "writable"
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
2,
|
||||
4
|
||||
],
|
||||
"quotes": [
|
||||
2,
|
||||
"single"
|
||||
],
|
||||
"linebreak-style": [
|
||||
2,
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"semi": [
|
||||
2,
|
||||
"always"
|
||||
"eqeqeq": [
|
||||
"error",
|
||||
"smart"
|
||||
],
|
||||
"prefer-const": [
|
||||
"error"
|
||||
],
|
||||
"no-var": "error",
|
||||
"no-use-before-define": "off",
|
||||
"no-console": [
|
||||
"error",
|
||||
{
|
||||
"allow": [
|
||||
"info",
|
||||
"warn",
|
||||
"error"
|
||||
]
|
||||
}
|
||||
],
|
||||
"prettier/prettier": "error",
|
||||
// Jest configuration
|
||||
"jest/expect-expect": [
|
||||
"error",
|
||||
{
|
||||
"assertFunctionNames": [
|
||||
"expect*",
|
||||
"checkImageData",
|
||||
"testCode"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"env": {
|
||||
@ -22,16 +55,109 @@
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"globals": {
|
||||
"tapes": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"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": {
|
||||
"no-console": 0
|
||||
},
|
||||
@ -40,17 +166,49 @@
|
||||
"jquery": false,
|
||||
"browser": false
|
||||
}
|
||||
}, {
|
||||
"files": [ "test/*"],
|
||||
},
|
||||
// Test configuration
|
||||
{
|
||||
"files": [
|
||||
"test/**/*"
|
||||
],
|
||||
"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": {
|
||||
"commonjs": true
|
||||
}
|
||||
},
|
||||
// Worker configuration
|
||||
{
|
||||
"files": [
|
||||
"workers/*"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": "workers/tsconfig.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"coverage/**/*"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"pragma": "h",
|
||||
"version": "16"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
27
.github/workflows/nodejs.yml
vendored
27
.github/workflows/nodejs.yml
vendored
@ -4,23 +4,22 @@ on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [16.x, 18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: npm install, build, and test
|
||||
run: |
|
||||
npm ci
|
||||
npm run build --if-present
|
||||
npm test
|
||||
env:
|
||||
CI: true
|
||||
- uses: actions/checkout@v1
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: npm install, build, and test
|
||||
run: |
|
||||
npm ci
|
||||
npm run build --if-present
|
||||
npm test
|
||||
env:
|
||||
CI: true
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
.*~
|
||||
.vscode
|
||||
.DS_Store
|
||||
/dist
|
||||
/node_modules
|
||||
|
@ -10,4 +10,15 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts?$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader'
|
||||
},
|
||||
],
|
||||
exclude: /node_modules/,
|
||||
}
|
||||
]
|
||||
};
|
||||
|
29
jest.config.js
Normal file
29
jest.config.js
Normal file
@ -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',
|
||||
};
|
498
js/apple1.js
498
js/apple1.js
@ -1,498 +0,0 @@
|
||||
import MicroModal from 'micromodal';
|
||||
|
||||
import Apple1IO from './apple1io';
|
||||
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 Bios from './roms/bios';
|
||||
import Krusader from './roms/krusader';
|
||||
|
||||
import ACI from './cards/aci';
|
||||
|
||||
import { mapKeyEvent, KeyBoard } from './ui/keyboard';
|
||||
|
||||
var DEBUG=false;
|
||||
var TRACE=true;
|
||||
var skidmarks = [];
|
||||
|
||||
var focused = false;
|
||||
var startTime = Date.now();
|
||||
var lastCycles = 0;
|
||||
var renderedFrames = 0, lastFrames = 0;
|
||||
var paused = false;
|
||||
|
||||
var hashtag;
|
||||
var prefs = new Prefs();
|
||||
var runTimer = null;
|
||||
var cpu = new CPU6502();
|
||||
|
||||
var krusader = window.location.hash == '#krusader';
|
||||
|
||||
var raml, ramh, rom, aci, io, text, keyboard;
|
||||
|
||||
// 32K base memory. Should be 0x0f for 4K, 0x1f for 8K, 0x3f for 16K
|
||||
raml = new RAM(0x00, 0x7f);
|
||||
text = new TextPage();
|
||||
text.init();
|
||||
|
||||
aci = new ACI(cpu, { progress: function(val) {
|
||||
document.querySelector('#tape').style.width = val * 100 + 'px';
|
||||
}});
|
||||
io = new Apple1IO(text);
|
||||
|
||||
if (krusader) {
|
||||
ramh = null;
|
||||
rom = new Krusader();
|
||||
} else {
|
||||
// ramh = new RAM(0xe0, 0xef); // 4K ACI memory.
|
||||
ramh = new Basic();
|
||||
rom = new Bios();
|
||||
}
|
||||
keyboard = new KeyBoard('#keyboard', cpu, io, text);
|
||||
|
||||
cpu.addPageHandler(raml);
|
||||
if (ramh) {
|
||||
cpu.addPageHandler(ramh);
|
||||
}
|
||||
cpu.addPageHandler(rom);
|
||||
|
||||
cpu.addPageHandler(aci);
|
||||
cpu.addPageHandler(io);
|
||||
|
||||
var showFPS = false;
|
||||
|
||||
//aci.setData([0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88])
|
||||
//aci.setData([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07])
|
||||
//aci.setData([0x01,0x23,0x45,0x67,0x89,0xab,0xcd,0xef])
|
||||
|
||||
//aci.setData(tapes['BASIC']);
|
||||
aci.setData(window.tapes['Microchess'].tracks);
|
||||
|
||||
// Audio Buffer Source
|
||||
var context;
|
||||
if (typeof window.webkitAudioContext !== 'undefined') {
|
||||
context = new window.webkitAudioContext();
|
||||
} else if (typeof window.AudioContext !== 'undefined') {
|
||||
context = new window.AudioContext();
|
||||
}
|
||||
|
||||
export function doLoadLocal(files) {
|
||||
context.resume();
|
||||
files = files || document.querySelector('#local_file').files;
|
||||
if (files.length == 1) {
|
||||
var file = files[0];
|
||||
var fileReader = new FileReader();
|
||||
fileReader.onload = function(ev) {
|
||||
context.decodeAudioData(
|
||||
ev.target.result,
|
||||
function(buffer) {
|
||||
var buf = [];
|
||||
var data = buffer.getChannelData(0);
|
||||
var old = (data[0] > 0.25);
|
||||
var last = 0;
|
||||
for (var idx = 1; idx < data.length; idx++) {
|
||||
var current = (data[idx] > 0.25);
|
||||
if (current != old) {
|
||||
var delta = idx - last;
|
||||
buf.push(parseInt(delta / buffer.sampleRate * 1023000));
|
||||
old = current;
|
||||
last = idx;
|
||||
}
|
||||
}
|
||||
aci.buffer = buf;
|
||||
MicroModal.close('local-modal');
|
||||
},
|
||||
function() {
|
||||
window.alert('Unable to read tape file: ' + file.name);
|
||||
}
|
||||
);
|
||||
};
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
}
|
||||
}
|
||||
|
||||
function updateKHz() {
|
||||
var now = Date.now();
|
||||
var ms = now - startTime;
|
||||
var cycles = cpu.cycles();
|
||||
var delta;
|
||||
|
||||
if (showFPS) {
|
||||
delta = renderedFrames - lastFrames;
|
||||
var fps = parseInt(delta/(ms/1000));
|
||||
document.querySelector('#khz').innerHTML = fps + 'fps';
|
||||
} else {
|
||||
delta = cycles - lastCycles;
|
||||
var khz = parseInt(delta/ms);
|
||||
document.querySelector('#khz').innerHTML = khz + 'KHz';
|
||||
}
|
||||
|
||||
startTime = now;
|
||||
lastCycles = cycles;
|
||||
lastFrames = renderedFrames;
|
||||
}
|
||||
|
||||
var loading = false;
|
||||
var throttling = true;
|
||||
var turbotape = false;
|
||||
|
||||
export function toggleFPS() {
|
||||
showFPS = !showFPS;
|
||||
}
|
||||
|
||||
export function toggleSpeed()
|
||||
{
|
||||
throttling = document.querySelector('#speed_toggle').checked;
|
||||
if (runTimer) {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
export function setKeyBuffer(text) {
|
||||
io.paste(text);
|
||||
}
|
||||
|
||||
export function setTurboTape(val) {
|
||||
turbotape = val;
|
||||
}
|
||||
|
||||
var _requestAnimationFrame =
|
||||
window.requestAnimationFrame ||
|
||||
window.mozRequestAnimationFrame ||
|
||||
window.webkitRequestAnimationFrame ||
|
||||
window.msRequestAnimationFrame;
|
||||
|
||||
function run(pc) {
|
||||
if (runTimer) {
|
||||
clearInterval(runTimer);
|
||||
}
|
||||
|
||||
if (pc) {
|
||||
cpu.setPC(pc);
|
||||
}
|
||||
|
||||
var ival = 30, step = 1023 * ival, stepMax = step;
|
||||
|
||||
if (!throttling) {
|
||||
ival = 1;
|
||||
}
|
||||
|
||||
var now, last = Date.now();
|
||||
var runFn = function() {
|
||||
now = Date.now();
|
||||
renderedFrames++;
|
||||
if (_requestAnimationFrame) {
|
||||
step = (now - last) * 1023;
|
||||
last = now;
|
||||
if (step > stepMax) {
|
||||
step = stepMax;
|
||||
}
|
||||
}
|
||||
if (document.location.hash != hashtag) {
|
||||
hashtag = document.location.hash;
|
||||
}
|
||||
if (!loading) {
|
||||
if (DEBUG) {
|
||||
cpu.stepCyclesDebug(TRACE ? 1 : step, function() {
|
||||
var line = cpu.dumpRegisters() + ' ' + cpu.dumpPC();
|
||||
if (TRACE) {
|
||||
debug(line);
|
||||
} else {
|
||||
skidmarks.push();
|
||||
if (skidmarks.length > 256) {
|
||||
skidmarks.shift();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cpu.stepCycles(step);
|
||||
}
|
||||
text.blit();
|
||||
}
|
||||
if (!paused && _requestAnimationFrame) {
|
||||
_requestAnimationFrame(runFn);
|
||||
}
|
||||
};
|
||||
if (_requestAnimationFrame) {
|
||||
_requestAnimationFrame(runFn);
|
||||
} else {
|
||||
runTimer = setInterval(runFn, ival);
|
||||
}
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (runTimer) {
|
||||
clearInterval(runTimer);
|
||||
}
|
||||
runTimer = null;
|
||||
}
|
||||
|
||||
function reset()
|
||||
{
|
||||
cpu.reset();
|
||||
}
|
||||
|
||||
export function loadBinary(bin) {
|
||||
stop();
|
||||
for (var idx = 0; idx < bin.length; idx++) {
|
||||
var pos = bin.start + idx;
|
||||
cpu.write(pos >> 8, pos & 0xff, bin.data[idx]);
|
||||
}
|
||||
run(bin.start);
|
||||
}
|
||||
|
||||
var _key;
|
||||
function _keydown(evt) {
|
||||
if (evt.keyCode === 112) {
|
||||
cpu.reset();
|
||||
} else if (evt.keyCode === 113) {
|
||||
if (document.webkitIsFullScreen) {
|
||||
document.webkitCancelFullScreen();
|
||||
} else {
|
||||
var elem = document.getElementById('display');
|
||||
elem.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
|
||||
}
|
||||
} else if (evt.key === 'Shift') {
|
||||
keyboard.shiftKey(true);
|
||||
} else if (evt.key == 'Control') {
|
||||
keyboard.controlKey(true);
|
||||
} else if (!focused && (!evt.metaKey || evt.ctrlKey)) {
|
||||
evt.preventDefault();
|
||||
|
||||
var key = mapKeyEvent(evt);
|
||||
if (key != 0xff) {
|
||||
if (_key != 0xff) io.keyUp();
|
||||
io.keyDown(key);
|
||||
_key = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _keyup(evt) {
|
||||
_key = 0xff;
|
||||
|
||||
if (evt.key === 'Shift') {
|
||||
keyboard.shiftKey(false);
|
||||
} else if (evt.key === 'Control') {
|
||||
keyboard.controlKey(false);
|
||||
} else {
|
||||
if (!focused) {
|
||||
io.keyUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var _updateScreenTimer = null;
|
||||
|
||||
export function updateScreen() {
|
||||
var green = document.querySelector('#green_screen').checked;
|
||||
var scanlines = document.querySelector('#show_scanlines').checked;
|
||||
|
||||
text.green(green);
|
||||
text.scanlines(scanlines);
|
||||
|
||||
if (!_updateScreenTimer)
|
||||
_updateScreenTimer =
|
||||
setInterval(function() {
|
||||
text.refresh();
|
||||
clearInterval(_updateScreenTimer);
|
||||
_updateScreenTimer = null;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
paused = false;
|
||||
export function pauseRun(b) {
|
||||
if (paused) {
|
||||
run();
|
||||
b.value = 'Pause';
|
||||
} else {
|
||||
stop();
|
||||
b.value = 'Run';
|
||||
}
|
||||
paused = !paused;
|
||||
}
|
||||
|
||||
export function openOptions() {
|
||||
MicroModal.show('options-modal');
|
||||
}
|
||||
|
||||
export function openLoadText(event) {
|
||||
if (event && event.altKey) {
|
||||
MicroModal.show('local-modal');
|
||||
} else {
|
||||
MicroModal.show('input-modal');
|
||||
}
|
||||
}
|
||||
|
||||
export function doLoadText() {
|
||||
var text = document.querySelector('#text_input').value;
|
||||
if (!text.indexOf('//Binary')) {
|
||||
var lines = text.split('\n');
|
||||
lines.forEach(function(line) {
|
||||
var parts = line.split(': ');
|
||||
if (parts.length == 2) {
|
||||
var addr;
|
||||
if (parts[0].length > 0) {
|
||||
addr = parseInt(parts[0], 16);
|
||||
}
|
||||
var data = parts[1].split(' ');
|
||||
for (var idx = 0; idx < data.length; idx++) {
|
||||
cpu.write(addr >> 8, addr & 0xff, parseInt(data[idx], 16));
|
||||
addr++;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
io.paste(text);
|
||||
}
|
||||
MicroModal.close('input-modal');
|
||||
}
|
||||
|
||||
export function handleDragOver(event) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
|
||||
export function handleDrop(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
var dt = event.dataTransfer;
|
||||
if (dt.files.length > 0) {
|
||||
doLoadLocal(dt.files);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleDragEnd(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();
|
||||
}
|
||||
}
|
||||
|
||||
MicroModal.init();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
hashtag = document.location.hash;
|
||||
|
||||
/*
|
||||
* Input Handling
|
||||
*/
|
||||
|
||||
var canvas = document.getElementById('text');
|
||||
var context = canvas.getContext('2d');
|
||||
|
||||
text.setContext(context);
|
||||
|
||||
window.addEventListener('keydown', _keydown);
|
||||
window.addEventListener('keyup', _keyup);
|
||||
|
||||
window.addEventListener('paste', (event) => {
|
||||
var paste = (event.clipboardData || window.clipboardData).getData('text');
|
||||
setKeyBuffer(paste);
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
window.addEventListener('copy', (event) => {
|
||||
event.clipboardData.setData('text/plain', text.getText());
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
document.querySelector('.overscan').addEventListener('paste', function(event) {
|
||||
io.paste(event.originalEvent.clipboardData().getData('text/plain'));
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
document.querySelectorAll('input,textarea').forEach(function(el) {
|
||||
el.addEventListener('focus', function() { focused = true; });
|
||||
});
|
||||
document.querySelectorAll('input,textarea').forEach(function(el) {
|
||||
el.addEventListener('blur', function() { focused = false; });
|
||||
});
|
||||
keyboard.create();
|
||||
|
||||
if (prefs.havePrefs()) {
|
||||
document.querySelectorAll('input[type=checkbox]').forEach(function(el) {
|
||||
var val = prefs.readPref(el.id);
|
||||
if (val != null)
|
||||
el.checked = JSON.parse(val);
|
||||
});
|
||||
document.querySelectorAll('input[type=checkbox]').forEach(function(el) {
|
||||
el.addEventListener('change', function() {
|
||||
prefs.writePref(el.id, JSON.stringify(el.checked));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
turbotape = document.querySelector('#turbo_tape').checked;
|
||||
|
||||
Object.keys(window.tapes).sort().forEach(function(key) {
|
||||
var option = document.createElement('option');
|
||||
option.value = key;
|
||||
option.text = key;
|
||||
document.querySelector('#tape_select').append(option);
|
||||
});
|
||||
|
||||
function doTapeSelect() {
|
||||
var tapeId = document.querySelector('#tape_select').value;
|
||||
var tape = window.tapes[tapeId];
|
||||
if (!tape) {
|
||||
document.querySelector('#text_input').value = '';
|
||||
return;
|
||||
}
|
||||
debug('Loading', tapeId);
|
||||
|
||||
window.location.hash = tapeId;
|
||||
reset();
|
||||
if (turbotape) {
|
||||
var trackIdx = 0, script = '';
|
||||
var parts = tape.script.split('\n');
|
||||
// Ignore part 0 (C100R)
|
||||
// Split part 1 into ranges
|
||||
var ranges = parts[1].split(' ');
|
||||
var idx;
|
||||
for (idx = 0; idx < ranges.length; idx++) {
|
||||
var range = ranges[idx].split('.');
|
||||
var start = parseInt(range[0], 16);
|
||||
var end = parseInt(range[1], 16);
|
||||
var track = tape.tracks[trackIdx];
|
||||
var kdx = 0;
|
||||
for (var jdx = start; jdx <= end; jdx++) {
|
||||
cpu.write(jdx >> 8, jdx & 0xff, track[kdx++]);
|
||||
}
|
||||
trackIdx++;
|
||||
}
|
||||
// Execute parts 2-n
|
||||
for (idx = 2; idx < parts.length; idx++) {
|
||||
if (parts[idx]) {
|
||||
script += parts[idx] + '\n';
|
||||
}
|
||||
}
|
||||
document.querySelector('#text_input').value = script;
|
||||
} else {
|
||||
aci.setData(tape.tracks);
|
||||
document.querySelector('#text_input').value = tape.script;
|
||||
}
|
||||
doLoadText();
|
||||
}
|
||||
document.querySelector('#tape_select').addEventListener('change', doTapeSelect);
|
||||
|
||||
run();
|
||||
setInterval(updateKHz, 1000);
|
||||
updateScreen();
|
||||
|
||||
var tape = hup();
|
||||
if (tape) {
|
||||
openLoadText();
|
||||
document.querySelector('#tape_select').value = tape;
|
||||
doTapeSelect();
|
||||
}
|
||||
});
|
536
js/apple1.ts
Normal file
536
js/apple1.ts
Normal file
@ -0,0 +1,536 @@
|
||||
/* 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 MicroModal from 'micromodal';
|
||||
|
||||
import Apple1IO from './apple1io';
|
||||
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 Bios from './roms/bios';
|
||||
import Krusader from './roms/krusader';
|
||||
|
||||
import ACI from './cards/aci';
|
||||
|
||||
import { mapKeyEvent, KeyBoard } from './ui/keyboard';
|
||||
import { address, byte } from './types';
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let DEBUG = false;
|
||||
// eslint-disable-next-line prefer-const
|
||||
let TRACE = true;
|
||||
const skidmarks: string[] = [];
|
||||
|
||||
let focused = false;
|
||||
let startTime = Date.now();
|
||||
let lastCycles = 0;
|
||||
let renderedFrames = 0,
|
||||
lastFrames = 0;
|
||||
let paused = false;
|
||||
|
||||
let hashtag: string | undefined;
|
||||
const prefs = new Prefs();
|
||||
let runTimer: ReturnType<typeof setInterval> | null = null;
|
||||
const cpu = new CPU6502();
|
||||
|
||||
const krusader = window.location.hash === '#krusader';
|
||||
|
||||
let ramh, rom;
|
||||
|
||||
// 32K base memory. Should be 0x0f for 4K, 0x1f for 8K, 0x3f for 16K
|
||||
const raml = new RAM(0x00, 0x7f);
|
||||
const text = new TextPage();
|
||||
text.init();
|
||||
|
||||
const aci = new ACI(cpu, {
|
||||
progress: function (val) {
|
||||
document.querySelector<HTMLElement>('#tape')!.style.width =
|
||||
val * 100 + 'px';
|
||||
},
|
||||
});
|
||||
const io = new Apple1IO(text);
|
||||
|
||||
if (krusader) {
|
||||
ramh = null;
|
||||
rom = new Krusader();
|
||||
} else {
|
||||
// ramh = new RAM(0xe0, 0xef); // 4K ACI memory.
|
||||
ramh = new Basic();
|
||||
rom = new Bios();
|
||||
}
|
||||
const keyboard = new KeyBoard('#keyboard', cpu, io, text);
|
||||
|
||||
cpu.addPageHandler(raml);
|
||||
if (ramh) {
|
||||
cpu.addPageHandler(ramh);
|
||||
}
|
||||
cpu.addPageHandler(rom);
|
||||
|
||||
cpu.addPageHandler(aci);
|
||||
cpu.addPageHandler(io);
|
||||
|
||||
let showFPS = false;
|
||||
|
||||
interface Tape {
|
||||
script: string;
|
||||
tracks: number[][];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
tapes: Record<string, Tape>;
|
||||
}
|
||||
}
|
||||
|
||||
//aci.setData([0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88])
|
||||
//aci.setData([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07])
|
||||
//aci.setData([0x01,0x23,0x45,0x67,0x89,0xab,0xcd,0xef])
|
||||
|
||||
//aci.setData(tapes['BASIC']);
|
||||
aci.setData(window.tapes['Microchess'].tracks);
|
||||
|
||||
// Audio Buffer Source
|
||||
declare global {
|
||||
interface Window {
|
||||
webkitAudioContext: AudioContext;
|
||||
}
|
||||
}
|
||||
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
const context = new AudioContext();
|
||||
|
||||
export function doLoadLocal(files: FileList) {
|
||||
context
|
||||
.resume()
|
||||
.then(() => {
|
||||
files =
|
||||
files || document.querySelector<HTMLInputElement>('#local_file')!.files;
|
||||
if (files.length === 1) {
|
||||
const file = files[0];
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = function (ev) {
|
||||
context
|
||||
.decodeAudioData(
|
||||
ev.target!.result as ArrayBuffer,
|
||||
function (buffer) {
|
||||
const buf = [];
|
||||
const data = buffer.getChannelData(0);
|
||||
let old = data[0] > 0.25;
|
||||
let last = 0;
|
||||
for (let idx = 1; idx < data.length; idx++) {
|
||||
const current = data[idx] > 0.25;
|
||||
if (current !== old) {
|
||||
const delta = idx - last;
|
||||
buf.push(Math.floor((delta / buffer.sampleRate) * 1023000));
|
||||
old = current;
|
||||
last = idx;
|
||||
}
|
||||
}
|
||||
aci.buffer = buf;
|
||||
MicroModal.close('local-modal');
|
||||
},
|
||||
function () {
|
||||
window.alert('Unable to read tape file: ' + file.name);
|
||||
},
|
||||
)
|
||||
.catch(console.error);
|
||||
};
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
function updateKHz() {
|
||||
const now = Date.now();
|
||||
const ms = now - startTime;
|
||||
const cycles = cpu.getCycles();
|
||||
let delta: number;
|
||||
|
||||
if (showFPS) {
|
||||
delta = renderedFrames - lastFrames;
|
||||
const fps = Math.floor(delta / (ms / 1000));
|
||||
document.querySelector('#khz')!.innerHTML = fps + 'fps';
|
||||
} else {
|
||||
delta = cycles - lastCycles;
|
||||
const khz = Math.floor(delta / ms);
|
||||
document.querySelector('#khz')!.innerHTML = khz + 'KHz';
|
||||
}
|
||||
|
||||
startTime = now;
|
||||
lastCycles = cycles;
|
||||
lastFrames = renderedFrames;
|
||||
}
|
||||
|
||||
let throttling = true;
|
||||
let turbotape = false;
|
||||
|
||||
export function toggleFPS() {
|
||||
showFPS = !showFPS;
|
||||
}
|
||||
|
||||
export function toggleSpeed() {
|
||||
throttling =
|
||||
document.querySelector<HTMLInputElement>('#speed_toggle')!.checked;
|
||||
if (runTimer) {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
export function setKeyBuffer(text: string) {
|
||||
io.paste(text);
|
||||
}
|
||||
|
||||
export function setTurboTape(val: boolean) {
|
||||
turbotape = val;
|
||||
}
|
||||
|
||||
function run(pc?: address) {
|
||||
if (runTimer) {
|
||||
clearInterval(runTimer);
|
||||
}
|
||||
|
||||
if (pc) {
|
||||
cpu.setPC(pc);
|
||||
}
|
||||
|
||||
let ival = 30;
|
||||
let step = 1023 * ival;
|
||||
const stepMax = step;
|
||||
|
||||
if (!throttling) {
|
||||
ival = 1;
|
||||
}
|
||||
|
||||
let now;
|
||||
let last = Date.now();
|
||||
const runFn = function () {
|
||||
now = Date.now();
|
||||
renderedFrames++;
|
||||
step = (now - last) * 1023;
|
||||
last = now;
|
||||
if (step > stepMax) {
|
||||
step = stepMax;
|
||||
}
|
||||
if (document.location.hash !== hashtag) {
|
||||
hashtag = document.location.hash;
|
||||
}
|
||||
if (DEBUG) {
|
||||
cpu.stepCyclesDebug(TRACE ? 1 : step, function () {
|
||||
const line = JSON.stringify(cpu.getState());
|
||||
if (TRACE) {
|
||||
debug(line);
|
||||
} else {
|
||||
skidmarks.push(line);
|
||||
if (skidmarks.length > 256) {
|
||||
skidmarks.shift();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cpu.stepCycles(step);
|
||||
}
|
||||
text.blit();
|
||||
if (!paused) {
|
||||
requestAnimationFrame(runFn);
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(runFn);
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (runTimer) {
|
||||
clearInterval(runTimer);
|
||||
}
|
||||
runTimer = null;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
cpu.reset();
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Document {
|
||||
webkitCancelFullScreen: () => void;
|
||||
webkitIsFullScreen: boolean;
|
||||
}
|
||||
interface Element {
|
||||
webkitRequestFullScreen: (options?: unknown) => void;
|
||||
}
|
||||
}
|
||||
|
||||
let _key: byte;
|
||||
function _keydown(evt: KeyboardEvent) {
|
||||
if (evt.keyCode === 112) {
|
||||
cpu.reset();
|
||||
} else if (evt.keyCode === 113) {
|
||||
if (document.webkitIsFullScreen) {
|
||||
document.webkitCancelFullScreen();
|
||||
} else {
|
||||
const elem = document.getElementById('display');
|
||||
elem!.webkitRequestFullScreen();
|
||||
}
|
||||
} else if (evt.key === 'Shift') {
|
||||
keyboard.shiftKey(true);
|
||||
} else if (evt.key === 'Control') {
|
||||
keyboard.controlKey(true);
|
||||
} else if (!focused && (!evt.metaKey || evt.ctrlKey)) {
|
||||
evt.preventDefault();
|
||||
|
||||
const key = mapKeyEvent(evt);
|
||||
if (key !== 0xff) {
|
||||
if (_key !== 0xff) io.keyUp();
|
||||
io.keyDown(key);
|
||||
_key = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _keyup(evt: KeyboardEvent) {
|
||||
_key = 0xff;
|
||||
|
||||
if (evt.key === 'Shift') {
|
||||
keyboard.shiftKey(false);
|
||||
} else if (evt.key === 'Control') {
|
||||
keyboard.controlKey(false);
|
||||
} else {
|
||||
if (!focused) {
|
||||
io.keyUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _updateScreenTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
export function updateScreen() {
|
||||
const green =
|
||||
document.querySelector<HTMLInputElement>('#green_screen')!.checked;
|
||||
const scanlines =
|
||||
document.querySelector<HTMLInputElement>('#show_scanlines')!.checked;
|
||||
|
||||
text.green(green);
|
||||
text.scanlines(scanlines);
|
||||
|
||||
if (!_updateScreenTimer)
|
||||
_updateScreenTimer = setInterval(function () {
|
||||
text.refresh();
|
||||
if (_updateScreenTimer) {
|
||||
clearInterval(_updateScreenTimer);
|
||||
}
|
||||
_updateScreenTimer = null;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
paused = false;
|
||||
export function pauseRun(b: HTMLButtonElement) {
|
||||
if (paused) {
|
||||
run();
|
||||
b.value = 'Pause';
|
||||
} else {
|
||||
stop();
|
||||
b.value = 'Run';
|
||||
}
|
||||
paused = !paused;
|
||||
}
|
||||
|
||||
export function openOptions() {
|
||||
MicroModal.show('options-modal');
|
||||
}
|
||||
|
||||
export function openLoadText(event?: MouseEvent) {
|
||||
if (event && event.altKey) {
|
||||
MicroModal.show('local-modal');
|
||||
} else {
|
||||
MicroModal.show('input-modal');
|
||||
}
|
||||
}
|
||||
|
||||
export function doLoadText() {
|
||||
const text = document.querySelector<HTMLInputElement>('#text_input')!.value;
|
||||
if (!text.indexOf('//Binary')) {
|
||||
const lines = text.split('\n');
|
||||
lines.forEach(function (line) {
|
||||
const parts = line.split(': ');
|
||||
if (parts.length === 2) {
|
||||
let addr: address = 0;
|
||||
if (parts[0].length > 0) {
|
||||
addr = parseInt(parts[0], 16);
|
||||
}
|
||||
const data = parts[1].split(' ');
|
||||
for (let idx = 0; idx < data.length; idx++) {
|
||||
cpu.write(addr >> 8, addr & 0xff, parseInt(data[idx], 16));
|
||||
addr++;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
io.paste(text);
|
||||
}
|
||||
MicroModal.close('input-modal');
|
||||
}
|
||||
|
||||
export function handleDragOver(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer!.dropEffect = 'copy';
|
||||
}
|
||||
|
||||
export function handleDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const dt = event.dataTransfer;
|
||||
if (dt?.files && dt.files.length > 0) {
|
||||
doLoadLocal(dt.files);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleDragEnd(event: DragEvent) {
|
||||
const dt = event.dataTransfer;
|
||||
if (dt?.items) {
|
||||
for (let i = 0; i < dt.items.length; i++) {
|
||||
dt.items.remove(i);
|
||||
}
|
||||
} else {
|
||||
event.dataTransfer?.clearData();
|
||||
}
|
||||
}
|
||||
|
||||
MicroModal.init();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
hashtag = document.location.hash;
|
||||
|
||||
/*
|
||||
* Input Handling
|
||||
*/
|
||||
|
||||
const canvas = document.querySelector<HTMLCanvasElement>('#text')!;
|
||||
const context = canvas.getContext('2d')!;
|
||||
|
||||
text.setContext(context);
|
||||
|
||||
window.addEventListener('keydown', _keydown);
|
||||
window.addEventListener('keyup', _keyup);
|
||||
|
||||
window.addEventListener('paste', (event) => {
|
||||
const paste = event.clipboardData!.getData('text/plain');
|
||||
setKeyBuffer(paste);
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
window.addEventListener('copy', (event) => {
|
||||
event.clipboardData?.setData('text/plain', text.getText());
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
document.querySelectorAll('input,textarea').forEach(function (el) {
|
||||
el.addEventListener('focus', function () {
|
||||
focused = true;
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('input,textarea').forEach(function (el) {
|
||||
el.addEventListener('blur', function () {
|
||||
focused = false;
|
||||
});
|
||||
});
|
||||
keyboard.create();
|
||||
|
||||
if (prefs.havePrefs()) {
|
||||
document
|
||||
.querySelectorAll<HTMLInputElement>('input[type=checkbox]')
|
||||
.forEach(function (el) {
|
||||
const val = prefs.readPref(el.id);
|
||||
if (val != null) el.checked = !!JSON.parse(val);
|
||||
});
|
||||
document
|
||||
.querySelectorAll<HTMLInputElement>('input[type=checkbox]')
|
||||
.forEach(function (el) {
|
||||
el.addEventListener('change', function () {
|
||||
prefs.writePref(el.id, JSON.stringify(el.checked));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
turbotape = document.querySelector<HTMLInputElement>('#turbo_tape')!.checked;
|
||||
|
||||
Object.keys(window.tapes)
|
||||
.sort()
|
||||
.forEach(function (key) {
|
||||
const option = document.createElement('option');
|
||||
option.value = key;
|
||||
option.text = key;
|
||||
document.querySelector('#tape_select')!.append(option);
|
||||
});
|
||||
|
||||
function doTapeSelect() {
|
||||
const tapeId =
|
||||
document.querySelector<HTMLInputElement>('#tape_select')!.value;
|
||||
const tape = window.tapes[tapeId];
|
||||
if (!tape) {
|
||||
document.querySelector<HTMLInputElement>('#text_input')!.value = '';
|
||||
return;
|
||||
}
|
||||
debug('Loading', tapeId);
|
||||
|
||||
window.location.hash = tapeId;
|
||||
reset();
|
||||
if (turbotape) {
|
||||
let trackIdx = 0,
|
||||
script = '';
|
||||
const parts = tape.script.split('\n');
|
||||
// Ignore part 0 (C100R)
|
||||
// Split part 1 into ranges
|
||||
const ranges = parts[1].split(' ');
|
||||
let idx;
|
||||
for (idx = 0; idx < ranges.length; idx++) {
|
||||
const range = ranges[idx].split('.');
|
||||
const start = parseInt(range[0], 16);
|
||||
const end = parseInt(range[1], 16);
|
||||
const track = tape.tracks[trackIdx];
|
||||
let kdx = 0;
|
||||
for (let jdx = start; jdx <= end; jdx++) {
|
||||
cpu.write(jdx >> 8, jdx & 0xff, track[kdx++]);
|
||||
}
|
||||
trackIdx++;
|
||||
}
|
||||
// Execute parts 2-n
|
||||
for (idx = 2; idx < parts.length; idx++) {
|
||||
if (parts[idx]) {
|
||||
script += parts[idx] + '\n';
|
||||
}
|
||||
}
|
||||
document.querySelector<HTMLInputElement>('#text_input')!.value = script;
|
||||
} else {
|
||||
aci.setData(tape.tracks);
|
||||
document.querySelector<HTMLInputElement>('#text_input')!.value =
|
||||
tape.script;
|
||||
}
|
||||
doLoadText();
|
||||
}
|
||||
document
|
||||
.querySelector('#tape_select')!
|
||||
.addEventListener('change', doTapeSelect);
|
||||
|
||||
run();
|
||||
setInterval(updateKHz, 1000);
|
||||
updateScreen();
|
||||
|
||||
const tape = hup();
|
||||
if (tape) {
|
||||
openLoadText();
|
||||
document.querySelector<HTMLInputElement>('#tape_select')!.value = tape;
|
||||
doTapeSelect();
|
||||
}
|
||||
});
|
@ -1,97 +0,0 @@
|
||||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
export default function Apple1IO(text) {
|
||||
var _key = 0;
|
||||
var _keyReady = false;
|
||||
|
||||
var _displayReady = false;
|
||||
var _nextDSP = 0;
|
||||
var _buffer = [];
|
||||
|
||||
var LOC = {
|
||||
KBD: 0x10,
|
||||
KBDRDY: 0x011,
|
||||
DSP: 0x12,
|
||||
DSPCTL: 0x13
|
||||
};
|
||||
|
||||
return {
|
||||
start: function() { return 0xd0; },
|
||||
end: function() { return 0xd0; },
|
||||
read: function(page, off) {
|
||||
var result = 0;
|
||||
off &= 0x13;
|
||||
switch (off) {
|
||||
case LOC.KBD:
|
||||
// Keyboard
|
||||
if (_buffer.length) {
|
||||
result = _buffer.shift().toUpperCase().charCodeAt(0) & 0x7f;
|
||||
_keyReady = (_buffer.length > 0);
|
||||
} else {
|
||||
result = _key;
|
||||
_keyReady = false;
|
||||
}
|
||||
result |= 0x80;
|
||||
break;
|
||||
case LOC.KBDRDY:
|
||||
result = _keyReady ? 0x80 : 0x00;
|
||||
break;
|
||||
case LOC.DSP:
|
||||
// Display
|
||||
// result = (Math.random() > 0.5) ? 0x80 : 0x00;
|
||||
result = (Date.now() > _nextDSP) ? 0x00 : 0x80;
|
||||
break;
|
||||
case LOC.DSPCTL:
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
write: function(page, off, val) {
|
||||
off &= 0x13;
|
||||
switch (off) {
|
||||
case LOC.KBD:
|
||||
break;
|
||||
case LOC.KBDRDY:
|
||||
break;
|
||||
case LOC.DSP:
|
||||
// Display
|
||||
if (_displayReady) {
|
||||
text.write(val);
|
||||
}
|
||||
_nextDSP = Date.now() + ((_buffer.length > 0) ? 0 : 17);
|
||||
break;
|
||||
case LOC.DSPCTL:
|
||||
// Don't pretend we care what the value was...
|
||||
_displayReady = true;
|
||||
break;
|
||||
}
|
||||
},
|
||||
reset: function apple1io_reset() {
|
||||
text.clear();
|
||||
_buffer = [];
|
||||
_keyReady = false;
|
||||
_displayReady = false;
|
||||
},
|
||||
keyUp: function apple1io_keyUp() {
|
||||
},
|
||||
keyDown: function apple1io_keyDown(key) {
|
||||
_key = key;
|
||||
_keyReady = true;
|
||||
},
|
||||
paste: function apple1io_paste(buffer) {
|
||||
buffer = buffer.replace(/\/\/.*\n/g, '');
|
||||