import MicroModal from 'micromodal'; import Apple2IO from './apple2io'; import ApplesoftDump from './applesoft/decompiler'; import { HiresPage, LoresPage, VideoModes } from './canvas'; import CPU6502 from './cpu6502'; import MMU from './mmu'; import Prefs from './prefs'; import { debug, gup, hup } from './util'; import Audio from './ui/audio'; import DriveLights from './ui/drive_lights'; import { gamepad, configGamepad, initGamepad, processGamepad } from './ui/gamepad'; import KeyBoard from './ui/keyboard'; import Printer from './ui/printer'; import Tape, { TAPE_TYPES } from './ui/tape'; import DiskII, { DISK_TYPES } from './cards/disk2'; import Parallel from './cards/parallel'; import RAMFactor from './cards/ramfactor'; import Thunderclock from './cards/thunderclock'; import apple2e_charset from './roms/apple2e_char'; import apple2enh_charset from './roms/apple2enh_char'; import rmfont_charset from './roms/rmfont_char'; import Apple2eROM from './roms/apple2e'; import Apple2eEnhancedROM from './roms/apple2enh'; import SYMBOLS from './symbols'; var kHz = 1023; var focused = false; var startTime = Date.now(); var lastCycles = 0; var renderedFrames = 0, lastFrames = 0; var paused = false; var hashtag; var DEBUG = false; var TRACE = false; var MAX_TRACE = 256; var trace = []; var disk_categories = {'Local Saves': []}; var disk_sets = {}; var disk_cur_name = []; var disk_cur_cat = []; var _currentDrive = 1; export function openLoad(drive, event) { _currentDrive = parseInt(drive, 10); if (event.metaKey) { openLoadHTTP(drive); } else { if (disk_cur_cat[drive]) { document.querySelector('#category_select').value = disk_cur_cat[drive]; selectCategory(); } MicroModal.show('load-modal'); } } export function openSave(drive, event) { _currentDrive = parseInt(drive, 10); var mimetype = 'application/octet-stream'; var data = disk2.getBinary(drive); var a = document.querySelector('#local_save_link'); var blob = new Blob([data], { 'type': mimetype }); a.href = window.URL.createObjectURL(blob); a.download = drivelights.label(drive) + '.dsk'; if (event.metaKey) { dumpDisk(drive); } else { document.querySelector('#save_name').value = drivelights.label(drive); MicroModal.show('save-modal'); } } export function handleDragOver(drive, event) { event.preventDefault(); event.dataTransfer.dropEffect = 'copy'; } export function handleDragEnd(drive, event) { var dt = event.dataTransfer; if (dt.items) { for (var i = 0; i < dt.items.length; i++) { dt.items.remove(i); } } else { event.dataTransfer.clearData(); } } export function handleDrop(drive, event) { event.preventDefault(); event.stopPropagation(); if (drive < 1) { if (!disk2.getMetadata(1)) { drive = 1; } else if (!disk2.getMetadata(2)) { drive = 2; } else { drive = 1; } } var dt = event.dataTransfer; if (dt.files.length == 1) { doLoadLocal(drive, dt.files[0]); } else if (dt.files.length == 2) { doLoadLocal(1, dt.files[0]); doLoadLocal(2, dt.files[1]); } else { for (var idx = 0; idx < dt.items.length; idx++) { if (dt.items[idx].type === 'text/uri-list') { dt.items[idx].getAsString(function(url) { var parts = document.location.hash.split('|'); parts[drive - 1] = url; document.location.hash = parts.join('|'); }); } } } } var loading = false; function loadAjax(drive, url) { loading = true; MicroModal.show('loading-modal'); fetch(url).then(function(response) { return response.json(); }).then(function(data) { if (data.type == 'binary') { loadBinary(drive, data); } else if (DISK_TYPES.includes(data.type)) { loadDisk(drive, data); } initGamepad(data.gamepad); MicroModal.close('loading-modal'); loading = false; }).catch(function(error) { window.alert(error || status); MicroModal.close('loading-modal'); loading = false; }); } export function doLoad() { MicroModal.close('load-modal'); var urls = document.querySelector('#disk_select').value, url; if (urls && urls.length) { if (typeof(urls) == 'string') { url = urls; } else { url = urls[0]; } } var files = document.querySelector('#local_file').files; if (files.length == 1) { doLoadLocal(_currentDrive, files[0]); } else if (url) { var filename; MicroModal.close('load-modal'); if (url.substr(0,6) == 'local:') { filename = url.substr(6); if (filename == '__manage') { openManage(); } else { loadLocalStorage(_currentDrive, filename); } } else { var r1 = /json\/disks\/(.*).json$/.exec(url); if (r1) { filename = r1[1]; } else { filename = url; } var parts = document.location.hash.split('|'); parts[_currentDrive - 1] = filename; document.location.hash = parts.join('|'); } } }; export function doSave() { var name = document.querySelector('#save_name').value; saveLocalStorage(_currentDrive, name); MicroModal.close('save-modal'); }; export function doDelete(name) { if (window.confirm('Delete ' + name + '?')) { deleteLocalStorage(name); } } function doLoadLocal(drive, file) { var parts = file.name.split('.'); var ext = parts[parts.length - 1].toLowerCase(); if (DISK_TYPES.includes(ext)) { doLoadLocalDisk(drive, file); } else if ($.inArray(ext, TAPE_TYPES) >= 0) { tape.doLoadLocalTape(file, function() { MicroModal.close('load-modal'); }); } else { window.alert('Unknown file type: ' + ext); MicroModal.close('load-modal'); } } function doLoadLocalDisk(drive, file) { var fileReader = new FileReader(); fileReader.onload = function() { var parts = file.name.split('.'); var ext = parts.pop().toLowerCase(); var name = parts.join('.'); if (disk2.setBinary(drive, name, ext, this.result)) { drivelights.label(drive, name); MicroModal.close('load-modal'); initGamepad(); } }; fileReader.readAsArrayBuffer(file); } function doLoadHTTP(drive, _url) { var url = _url || document.querySelector('#http_url').value; if (url) { var req = new XMLHttpRequest(); req.open('GET', url, true); req.responseType = 'arraybuffer'; req.onload = function() { var urlParts = url.split('/'); var file = urlParts.pop(); var fileParts = file.split('.'); var ext = fileParts.pop().toLowerCase(); var name = decodeURIComponent(fileParts.join('.')); if (disk2.setBinary(drive, name, ext, req.response)) { drivelights.label(drive, name); MicroModal.close('http-modal'); initGamepad(); } }; req.send(null); } } function openLoadHTTP(drive) { _currentDrive = parseInt(drive, 10); MicroModal.show('http-modal'); } function openManage() { MicroModal.show('manage-modal'); } var prefs = new Prefs(); var romVersion = prefs.readPref('computer_type2e'); var enhanced = false; var multiScreen = false; var rom; var char_rom = apple2e_charset; switch (romVersion) { case 'apple2e': rom = new Apple2eROM(); break; case 'apple2rm': rom = new Apple2eEnhancedROM(); char_rom = rmfont_charset; enhanced = true; break; default: rom = new Apple2eEnhancedROM(); char_rom =apple2enh_charset; enhanced = true; } var runTimer = null; var cpu = new CPU6502({'65C02': enhanced}); var context1, context2, context3, context4; var canvas1 = document.getElementById('screen'); var canvas2 = document.getElementById('screen2'); var canvas3 = document.getElementById('screen3'); var canvas4 = document.getElementById('screen4'); context1 = canvas1.getContext('2d'); if (canvas4) { multiScreen = true; context2 = canvas2.getContext('2d'); context3 = canvas3.getContext('2d'); context4 = canvas4.getContext('2d'); } else if (canvas2) { multiScreen = true; context2 = context1; context3 = canvas2.getContext('2d'); context4 = context3; } else { context2 = context1; context3 = context1; context4 = context1; } var gr = new LoresPage(1, char_rom, true, context1); var gr2 = new LoresPage(2, char_rom, true, context2); var hgr = new HiresPage(1, context3); var hgr2 = new HiresPage(2, context4); var vm = new VideoModes(gr, hgr, gr2, hgr2, true); vm.enhanced(enhanced); vm.multiScreen(multiScreen); var dumper = new ApplesoftDump(cpu); var drivelights = new DriveLights(); var io = new Apple2IO(cpu, vm); var keyboard = new KeyBoard(cpu, io, true); var audio = new Audio(io); var tape = new Tape(io); var printer = new Printer('#printer-modal .paper'); var mmu = new MMU(cpu, vm, gr, gr2, hgr, hgr2, io, rom); cpu.addPageHandler(mmu); var parallel = new Parallel(io, 1, printer); var slinky = new RAMFactor(io, 2, 1024 * 1024); var disk2 = new DiskII(io, 6, drivelights); var clock = new Thunderclock(io, 7); io.setSlot(1, parallel); io.setSlot(2, slinky); io.setSlot(6, disk2); io.setSlot(7, clock); var showFPS = false; 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), 10); document.querySelector('#khz').innerText = fps + 'fps'; } else { delta = cycles - lastCycles; var khz = parseInt(delta/ms); document.querySelector('#khz').innerText = khz + 'KHz'; } startTime = now; lastCycles = cycles; lastFrames = renderedFrames; } export function toggleShowFPS() { showFPS = !showFPS; } export function updateSound() { var on = document.querySelector('#enable_sound').checked; var label = document.querySelector('#toggle-sound i'); audio.enable(on); if (on) { label.classList.remove('fa-volume-off'); label.classList.add('fa-volume-up'); } else { label.classList.remove('fa-volume-up'); label.classList.add('fa-volume-off'); } }; function dumpDisk(drive) { var wind = window.open('', '_blank'); wind.document.title = drivelights.label(drive); wind.document.write('
');
    wind.document.write(disk2.getJSON(drive, true));
    wind.document.write('
'); wind.document.close(); } export function dumpProgram() { var wind = window.open('', '_blank'); wind.document.title = 'Program Listing'; wind.document.write('
');
    wind.document.write(dumper.toString());
    wind.document.write('
'); wind.document.close(); } export function step() { if (runTimer) { clearInterval(runTimer); } runTimer = null; cpu.step(function() { debug(cpu.dumpRegisters()); debug(cpu.dumpPC()); }); } var accelerated = false; export function updateCPU() { accelerated = document.querySelector('#accelerator_toggle').checked; kHz = accelerated ? 4092 : 1023; io.updateHz(kHz * 1000); if (runTimer) { run(); } }; 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; var now, last = Date.now(); var runFn = function() { now = Date.now(); var step = (now - last) * kHz, stepMax = kHz * ival; last = now; if (step > stepMax) { step = stepMax; } if (document.location.hash != hashtag) { hashtag = document.location.hash; var hash = hup(); if (hash) { processHash(hash); } } if (!loading) { mmu.resetVB(); if (DEBUG) { cpu.stepCyclesDebug(TRACE ? 1 : step, function() { var line = cpu.dumpRegisters() + ' ' + cpu.dumpPC(undefined, SYMBOLS); if (TRACE) { debug(line); } else { trace.push(line); if (trace.length > MAX_TRACE) { trace.shift(); } } }); } else { cpu.stepCycles(step); } if (vm.blit()) { renderedFrames++; } io.tick(); } processGamepad(io); if (!paused && _requestAnimationFrame) { _requestAnimationFrame(runFn); } }; if (_requestAnimationFrame) { _requestAnimationFrame(runFn); } else { runTimer = setInterval(runFn, ival); } } function stop() { if (runTimer) { clearInterval(runTimer); } runTimer = null; } export function reset() { cpu.reset(); } var state = null; function storeStateLocal() { window.localStorage['apple2.state'] = JSON.stringify(state); } function restoreStateLocal() { var data = window.localStorage['apple2.state']; if (data) { state = JSON.parse(data); } } function saveState() { if (state && !window.confirm('Overwrite Saved State?')) { return; } state = { cpu: cpu.getState(), io: io.getState(), mmu: mmu.getState(), vm: vm.getState(), disk2: disk2.getState(), drivelights: drivelights.getState() }; if (slinky) { state.slinky = slinky.getState(); } if (window.localStorage) { storeStateLocal(); } } function restoreState() { if (window.localStorage) { restoreStateLocal(); } if (!state) { return; } cpu.setState(state.cpu); io.setState(state.io); mmu.setState(state.mmu); vm.setState(state.vm); disk2.setState(state.disk2); drivelights.setState(state.drivelights); if (slinky && state.slinky) { slinky.setState(state.slinky); } } 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); } export function selectCategory() { document.querySelector('#disk_select').innerHTML = ''; var cat = disk_categories[document.querySelector('#category_select').value]; if (cat) { for (var idx = 0; idx < cat.length; idx++) { var file = cat[idx], name = file.name; if (file.disk) { name += ' - ' + file.disk; } var option = document.createElement('option'); option.value = file.filename; option.innerText = name; document.querySelector('#disk_select').append(option); if (disk_cur_name[_currentDrive] == name) { option.selected = true; } } } } export function selectDisk() { document.querySelector('#local_file').value = ''; } export function clickDisk() { doLoad(); } function loadDisk(drive, disk) { var name = disk.name; var category = disk.category; if (disk.disk) { name += ' - ' + disk.disk; } disk_cur_cat[drive] = category; disk_cur_name[drive] = name; drivelights.label(drive, name); disk2.setDisk(drive, disk); initGamepad(disk.gamepad); } /* * LocalStorage Disk Storage */ function updateLocalStorage() { var diskIndex = JSON.parse(window.localStorage.diskIndex || '{}'); var names = [], name, cat; for (name in diskIndex) { if (diskIndex.hasOwnProperty(name)) { names.push(name); } } cat = disk_categories['Local Saves'] = []; document.querySelector('#manage-modal-content').innerHTML = ''; names.forEach(function(name) { cat.push({ 'category': 'Local Saves', 'name': name, 'filename': 'local:' + name }); document.querySelector('#manage-modal-content').innerHTML = '' + name + ' Delete
'; }); cat.push({ 'category': 'Local Saves', 'name': 'Manage Saves...', 'filename': 'local:__manage' }); } function saveLocalStorage(drive, name) { var diskIndex = JSON.parse(window.localStorage.diskIndex || '{}'); var json = disk2.getJSON(drive); diskIndex[name] = json; window.localStorage.diskIndex = JSON.stringify(diskIndex); window.alert('Saved'); drivelights.label(drive, name); drivelights.dirty(drive, false); updateLocalStorage(); } function deleteLocalStorage(name) { var diskIndex = JSON.parse(window.localStorage.diskIndex || '{}'); if (diskIndex[name]) { delete diskIndex[name]; window.alert('Deleted'); } window.localStorage.diskIndex = JSON.stringify(diskIndex); updateLocalStorage(); } function loadLocalStorage(drive, name) { var diskIndex = JSON.parse(window.localStorage.diskIndex || '{}'); if (diskIndex[name]) { disk2.setJSON(drive, diskIndex[name]); drivelights.label(drive, name); drivelights.dirty(drive, false); } } function processHash(hash) { var files = hash.split('|'); for (var idx = 0; idx < files.length; idx++) { var file = files[idx]; if (file.indexOf('://') > 0) { var parts = file.split('.'); var ext = parts[parts.length - 1].toLowerCase(); if (ext == 'json') { loadAjax(idx + 1, file); } else { doLoadHTTP(idx + 1, file); } } else { loadAjax(idx + 1, 'json/disks/' + file + '.json'); } } } /* * Keyboard/Gamepad routines */ function _keydown(evt) { if (!focused && (!evt.metaKey || evt.ctrlKey)) { evt.preventDefault(); var key = keyboard.mapKeyEvent(evt); if (key != 0xff) { io.keyDown(key); } } if (evt.keyCode === 112) { // F1 - Reset cpu.reset(); evt.preventDefault(); // prevent launching help } else if (evt.keyCode === 113) { // F2 - Full Screen var elem = document.getElementById('screen'); if (evt.shiftKey) { // Full window, but not full screen document.querySelector('#display').classList.toggle('zoomwindow'); document.querySelector('#display > div').classList.toggle('overscan'); document.querySelector('#display > div').classList.toggle('flexbox-centering'); document.querySelector('#screen').classList.toggle('maxhw'); document.querySelector('#header').classList.toggle('hidden'); document.querySelectorAll('.inset').forEach((el) => el.classList.toggle('hidden')); document.querySelector('#reset').classList.toggle('hidden'); } else if (document.webkitCancelFullScreen) { if (document.webkitIsFullScreen) { document.webkitCancelFullScreen(); } else { if (Element.ALLOW_KEYBOARD_INPUT) { elem.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); } else { elem.webkitRequestFullScreen(); } } } else if (document.mozCancelFullScreen) { if (document.mozIsFullScreen) { document.mozCancelFullScreen(); } else { elem.mozRequestFullScreen(); } } } else if (evt.keyCode === 114) { // F3 io.keyDown(0x1b); } else if (evt.keyCode === 117) { // F6 Quick Save saveState(); } else if (evt.keyCode === 120) { // F9 Quick Restore restoreState(); } else if (evt.keyCode == 16) { // Shift keyboard.shiftKey(true); } else if (evt.keyCode == 17) { // Control keyboard.controlKey(true); } else if (evt.keyCode == 91 || evt.keyCode == 93) { // Command keyboard.commandKey(true); } else if (evt.keyCode == 18) { // Alt if (evt.location == 1) { keyboard.commandKey(true); } else { keyboard.optionKey(true); } } } function _keyup(evt) { if (!focused) io.keyUp(); if (evt.keyCode == 16) { // Shift keyboard.shiftKey(false); } else if (evt.keyCode == 17) { // Control keyboard.controlKey(false); } else if (evt.keyCode == 91 || evt.keyCode == 93) { // Command keyboard.commandKey(false); } else if (evt.keyCode == 18) { // Alt if (evt.location == 1) { keyboard.commandKey(false); } else { keyboard.optionKey(false); } } } export function updateScreen() { var green = document.querySelector('#green_screen').checked; var scanlines = document.querySelector('#show_scanlines').checked; vm.green(green); vm.scanlines(scanlines); }; var disableMouseJoystick = false; var flipX = false; var flipY = false; var swapXY = false; export function updateJoystick() { disableMouseJoystick = document.querySelector('#disable_mouse').checked; flipX = document.querySelector('#flip_x').checked; flipY = document.querySelector('#flip_y').checked; swapXY = document.querySelector('#swap_x_y').checked; configGamepad(flipX, flipY); if (disableMouseJoystick) { io.paddle(0, 0.5); io.paddle(1, 0.5); return; } } function _mousemove(evt) { if (gamepad || disableMouseJoystick) { return; } var s = document.querySelector('#screen'); var offset = { top: s.clientTop, left: s.clientLeft }; var x = (evt.pageX - offset.left) / s.clientWidth, y = (evt.pageY - offset.top) / s.clientHeight, z = x; if (swapXY) { x = y; y = z; } io.paddle(0, flipX ? 1 - x : x); io.paddle(1, flipY ? 1 - y : y); } export function pauseRun() { var label = document.querySelector('#pause-run i'); if (paused) { run(); label.classList.remove('fa-play'); label.classList.add('fa-pause'); } else { stop(); label.classList.remove('fa-pause'); label.classList.add('fa-play'); } paused = !paused; } export function toggleSound() { var enableSound = document.querySelector('#enable_sound'); enableSound.checked = !enableSound.checked; updateSound(); } export function openOptions() { MicroModal.show('options-modal'); }; export function openPrinterModal() { MicroModal.show('printer-modal'); }; MicroModal.init(); document.addEventListener('DOMContentLoaded', function() { hashtag = document.location.hash; /* * Input Handling */ window.addEventListener('keydown', _keydown); window.addEventListener('keyup', _keyup); window.addEventListener('mousedown', function() { audio.autoStart(); }); document.querySelectorAll('canvas').forEach(function(canvas) { canvas.addEventListener('mousedown', function(evt) { if (!gamepad) { io.buttonDown(evt.which == 1 ? 0 : 1); } evt.preventDefault(); }); canvas.addEventListener('mouseup', function(evt) { if (!gamepad) { io.buttonUp(evt.which == 1 ? 0 : 1); } }); }); document.body.addEventListener('mousemove', _mousemove); document.querySelectorAll('input,textarea').forEach(function(input) { input.addEventListener('input', function() { focused = true; }); input.addEventListener('blur', function() { focused = false; }); }); keyboard.create('#keyboard'); if (prefs.havePrefs()) { document.querySelectorAll('#options-modal input[type=checkbox]').forEach(function(el) { var val = prefs.readPref(el.id); if (val) { el.checked = JSON.parse(val); } el.addEventListener('change', function() { prefs.writePref(el.id, JSON.stringify(el.checked)); }); }); document.querySelectorAll('#options-modal select').forEach(function(el) { var val = prefs.readPref(el.id); if (val) { el.value = val; } el.addEventListener('change', function() { prefs.writePref(el.id, el.value); }); }); } cpu.reset(); setInterval(updateKHz, 1000); updateSound(); updateScreen(); updateCPU(); if (window.localStorage !== undefined) { document.querySelectorAll('.disksave').forEach(function (el) { el.style.display = 'inline-block';}); } var oldcat = ''; var option; for (var idx = 0; idx < window.disk_index.length; idx++) { var file = window.disk_index[idx]; var cat = file.category; var name = file.name, disk = file.disk; if (cat != oldcat) { option = document.createElement('option'); option.value = cat; option.innerText = cat; document.querySelector('#category_select').append(option); disk_categories[cat] = []; oldcat = cat; } disk_categories[cat].push(file); if (disk) { if (!disk_sets[name]) { disk_sets[name] = []; } disk_sets[name].push(file); } } option = document.createElement('option'); option.innerText = 'Local Saves'; document.querySelector('#category_select').append(option); updateLocalStorage(); initGamepad(); // Check for disks in hashtag var hash = gup('disk') || hup(); if (hash) { processHash(hash); } if (navigator.standalone) { document.body.classList.add('standalone'); } run(); });