* Enabled prettier

* Update lint, fix issues

* Restore some array formatting
This commit is contained in:
Will Scullin 2023-11-24 06:45:55 -08:00 committed by GitHub
parent e7891114c6
commit 1e79d9d59d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
172 changed files with 35468 additions and 33785 deletions

View File

@ -1,4 +1,5 @@
dist
json/disks/index.js
node_modules
submodules
tmp

View File

@ -1,23 +1,17 @@
{
// Global
"root": true,
"plugins": [
"prettier"
],
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:jest/recommended"
],
"rules": {
"indent": [
"error",
4,
{
"SwitchCase": 1
}
],
"quotes": [
"error",
"single"
],
"prettier/prettier": "error",
"linebreak-style": [
"error",
"unix"
@ -80,12 +74,6 @@
"rules": {
// 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",

View File

@ -3,25 +3,25 @@ name: Node CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x]
strategy:
matrix:
node-version: [16.x, 18.x]
steps:
- uses: actions/checkout@v2
with:
submodules: "true"
- 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
steps:
- uses: actions/checkout@v2
with:
submodules: 'true'
- 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

2
.prettierignore Normal file
View File

@ -0,0 +1,2 @@
js/roms
submodules

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": true
}

View File

@ -1,10 +1,9 @@
{
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-css-modules"
],
"rules": {
"indentation": 4,
"selector-class-pattern": "^[a-z][a-zA-Z0-9_-]+$"
}
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-css-modules"
],
"rules": {
"selector-class-pattern": "^[a-z][a-zA-Z0-9_-]+$"
}
}

View File

@ -46,29 +46,29 @@ then
### 2019-03-02
* Behind the scenes
- Behind the scenes
A lot of not so visible changes behind the scenes. The website now runs directly off of github, and has a more modern toolchain (Goodbye make, Perl and closure compiler, hello Webpack!) to facilitate development.
* Performance
- Performance
In particular, the screen no longer redraws as quickly as possible.
* Drag and Drop
- Drag and Drop
Disk images can be dragged into the window to load them.
* Contributions
- Contributions
Thanks to [Ian Flanigan](https://github.com/iflan) for additions to improve ChromeBook behavior.
### 2017-10-08
* Better iOS support
- Better iOS support
Bluetooth keyboards now work better. Reset is Ctrl-Shift-Delete. iOS now allows loading disks from iCloud and services like Drop Box. Saving locally is still not supported by iOS. I now understand why sound doesn't work, and I'm working on a work-around.
* Source Maps
- Source Maps
Although the source code has always been available, by default I serve up minified Javascript for performance reasons. But now you can poke around more easily.
@ -76,102 +76,104 @@ then
(It's been a long time since I updated, so this is a rough list)
* Videx Videoterm Emulation (\]\[js)
- Videx Videoterm Emulation (\]\[js)
PR#3 now does something on the Apple \]\[!.
* AppleColor RGB Card Emulation (//jse)
- AppleColor RGB Card Emulation (//jse)
Now supports a bunch of the mostly non-standard video formats found on the AppleColor RGB card, including 16 color text, 16 color hires mode, and mixed black and white and color double hires
* Machine selection
- Machine selection
You can now select between original, autostart and plus Apple \]\[s, and unenhanced and enhanced //es.
## Updates (2013-07-04)
* RAMFactor Emulation (//jse)
- RAMFactor Emulation (//jse)
I now simulate having a 1 Megabyte RAMFactor card in slot 2.
* Thunderclock Emulation
- Thunderclock Emulation
There is cursory emulation of the Thunderclock card, enough to keep ProDOS applications from asking you to enter the date all the time. ProDOS attempts to guess the year from the month, the day and the day of the week, something that needs to be patched every 6 years. This means newer versions think it's 1996, older versions are stuck in the 80s.
* Firefox Nightly Joystick Support
- Firefox Nightly Joystick Support
Joystick support has yet to officially land, but the latest nightlies support the gamepad API.
## Updates (2013-03-20)
* Animation Frames
- Animation Frames
I've switched from using setInterval() to requestAnimationFrame() where supported. This, in conjunction with the graphics re-write, seems to smooth performance and provide a more stable CPU speed.
* Graphics Re-Write
- Graphics Re-Write
This (third) re-write of the graphics system should improve performance with graphics intensive programs. Rather than rendering each graphics update as it happens, updates are rendered each animation frame.
## Updates (2013-03-12)
* Apple //e
- Apple //e
After much flailing, and much staring at MMU emulation code in despair, I've finally published my Apple //e emulator. It's probably a little more rough than I'd hoped, but it has a lot of features that I really wanted to get into it, like basic double hires support, and it uses the enhanced Apple //e ROMs.
## Updates (2013-02-25)
* Joystick Support
- Joystick Support
Chrome only so far, the nascent gamepad API has finally allowed me to add basic joystick support. I can now re-live my glory days of Skyfox.
* Re-written CPU emulator
- Re-written CPU emulator
I finally got around to applying some of the many lessons I learned along the way writing my first CPU emulator in Javascript. The last re-working gave me about a 100% performance gain.
* Finally Fixed Oregon Trail
- Finally Fixed Oregon Trail
This seems to have been a major disappointment for many people. I was able to make it as a banker, but I'm embarassed to reveal my score.
* Competition
- Competition
Now in addition to [Gil Megidish's](http://www.megidish.net/apple2js/) Apple2JS, there's a couple of new kids on the block, including [David Caldwell's](http://porkrind.org/a2/) Apple II+ emulator where he's put a lot more thought into the graphics rendering than I have, and [appletoo](https://github.com/nicholasbs/appletoo), which I just stumbled across while looking for David's emulator and haven't had much time to look at.
## Requirements
* A Browser with HTML5 Support
- A Browser with HTML5 Support
The most recent versions of [Google Chrome](https://www.google.com/chrome/), [Safari](https://www.apple.com/safari/), [Firefox](https://www.firefox.com/), and [Opera](https//www.opera.com/) all seem to work reasonably well these days,although variations in HTML5 support pop up, and occasionally a major release will move things around out from under me. IE prior to 9 lacks canvas tag support and is unsupported. [IE 9+](https://windows.microsoft.com/ie9) renders nicely on a modern machine.
* Basic Knowledge of the Apple \]\[
- Basic Knowledge of the Apple \]\[
If you don't know how to use an Apple \]\[, this won't be much fun for you.
## Acknowledgements
* I'm using the following libraries:
- I'm using the following libraries:
* [jQuery](https://jquery.com) and [jQuery UI](https://jqueryui.com)
* Base64 Utilities via [KvZ](http://kevin.vanzonneveld.net/)
* LED graphics from [Modern Life](http://modernl.com/).
* [CFFA2 Firmware](http://dreher.net/?s=projects/CFforAppleII&c=projects/CFforAppleII/downloads1.php) by Chris Schumann, Rich Dreher and Dave Lyons
- [jQuery](https://jquery.com) and [jQuery UI](https://jqueryui.com)
- Base64 Utilities via [KvZ](http://kevin.vanzonneveld.net/)
- LED graphics from [Modern Life](http://modernl.com/).
- [CFFA2 Firmware](http://dreher.net/?s=projects/CFforAppleII&c=projects/CFforAppleII/downloads1.php) by Chris Schumann, Rich Dreher and Dave Lyons
* I heavily referenced:
- I heavily referenced:
* [_Beneath Apple DOS_](http://www.scribd.com/doc/200679/Beneath-Apple-DOS-By-Don-Worth-and-Pieter-Lechner) by Don Worth and Pieter Lechner
* _Inside the Apple //e_ by Gary B. Little
* [_DOS 3.3 Anatomy_](http://apple2.org.za/gswv/a2zine/GS.WorldView/Resources/DOS.3.3.ANATOMY/)
* [_Apple II Disk Drive Article_](http://www.doc.ic.ac.uk/~ih/doc/stepper/others/example3/diskii_specs.html) by Neil Parker
* [6502.org](http://6502.org/)
* The [comp.sys.apple2.programmer](http://www.faqs.org/faqs/apple2/programmerfaq/part1/) FAQ
* [Understanding the Apple \]\[](https://archive.org/details/understanding_the_apple_ii) and [Understanding the Apple //e](https://archive.org/details/Understanding_the_Apple_IIe) by Jim Sather.
* [Apple II Colors](https://mrob.com/pub/xapple2/colors.html) by Robert Munafo.
- [_Beneath Apple DOS_](http://www.scribd.com/doc/200679/Beneath-Apple-DOS-By-Don-Worth-and-Pieter-Lechner) by Don Worth and Pieter Lechner
- _Inside the Apple //e_ by Gary B. Little
- [_DOS 3.3 Anatomy_](http://apple2.org.za/gswv/a2zine/GS.WorldView/Resources/DOS.3.3.ANATOMY/)
- [_Apple II Disk Drive Article_](http://www.doc.ic.ac.uk/~ih/doc/stepper/others/example3/diskii_specs.html) by Neil Parker
- [6502.org](http://6502.org/)
- The [comp.sys.apple2.programmer](http://www.faqs.org/faqs/apple2/programmerfaq/part1/) FAQ
- [Understanding the Apple \]\[](https://archive.org/details/understanding_the_apple_ii) and [Understanding the Apple //e](https://archive.org/details/Understanding_the_Apple_IIe) by Jim Sather.
- [Apple II Colors](https://mrob.com/pub/xapple2/colors.html) by Robert Munafo.
* And special thanks to:
- And special thanks to:
* [ADTPro](http://adtpro.sourceforge.net/) for allowing me to pull some of my circa 1980 programming efforts off some ancient floppies.
* [KEGS](http://kegs.sourceforge.net/), because at some point I got so tired of futzing with ADC/SBC code I just ported the KEGS C code for those opcodes to Javascript so I could stop worrying about it.
* [Apple II History](http://apple2history.org/), for a lovely, informative site.
* [Gil Megidish](http://www.megidish.net/apple2js/), for the kick in the pants to finally post my version, once I realized there was, in fact, another apple2js in the world.
* [AppleWin](https://github.com/AppleWin/AppleWin/), whose source code is a goldmine of useful references.
* [Zellyn Hunter](https://github.com/zellyn/a2audit) and a2audit, for allowing me to get really nitpicky in my memory emulation.
- [ADTPro](http://adtpro.sourceforge.net/) for allowing me to pull some of my circa 1980 programming efforts off some ancient floppies.
- [KEGS](http://kegs.sourceforge.net/), because at some point I got so tired of futzing with ADC/SBC code I just ported the KEGS C code for those opcodes to Javascript so I could stop worrying about it.
- [Apple II History](http://apple2history.org/), for a lovely, informative site.
- [Gil Megidish](http://www.megidish.net/apple2js/), for the kick in the pants to finally post my version, once I realized there was, in fact, another apple2js in the world.
- [AppleWin](https://github.com/AppleWin/AppleWin/), whose source code is a goldmine of useful references.
- [Zellyn Hunter](https://github.com/zellyn/a2audit) and a2audit, for allowing me to get really nitpicky in my memory emulation.
* Contributors
* [Snapperfish](http://github.com/Snapperfish) Various fixes
- Contributors
- [Snapperfish](http://github.com/Snapperfish) Various fixes

View File

@ -12,16 +12,17 @@ module.exports = {
[
'@babel/typescript',
{
jsxPragma: 'h'
}
jsxPragma: 'h',
},
],
],
plugins: [
[
'@babel/plugin-transform-react-jsx', {
'@babel/plugin-transform-react-jsx',
{
pragma: 'h',
pragmaFrag: 'Fragment',
}
]
]
},
],
],
};

View File

@ -44,7 +44,7 @@ function read2MG(fileData) {
return {
diskData: fileData.slice(headerLength),
type: type,
readOnly: readOnly
readOnly: readOnly,
};
}
@ -53,11 +53,11 @@ function readTracks(type, diskData) {
if (type === 'nib') {
let start = 0;
let end = 0x1A00;
let end = 0x1a00;
while (start < diskData.length) {
const trackData = diskData.slice(start, end);
start += 0x1A00;
end += 0x1A00;
start += 0x1a00;
end += 0x1a00;
tracks.push(trackData.toString('base64'));
}
@ -82,7 +82,6 @@ function readTracks(type, diskData) {
const fileName = argv._[0];
let readOnly = argv.r || argv.readOnly ? true : undefined;
const name = argv.n || argv.name;
const category = argv.c || argv.category;
@ -110,7 +109,7 @@ fs.readFile(fileName, (err, fileData) => {
let diskData;
if (type === '2mg') {
({diskData, readOnly, type} = read2MG(fileData));
({ diskData, readOnly, type } = read2MG(fileData));
} else {
diskData = fileData;
}
@ -123,7 +122,7 @@ fs.readFile(fileName, (err, fileData) => {
readOnly,
disk,
'2e': e,
data: readTracks(type, diskData)
data: readTracks(type, diskData),
};
Object.keys(entry).forEach((key) => {

View File

@ -1,6 +1,5 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
@ -13,8 +12,12 @@ for (const fileName of dir.sort()) {
if (/\.json$/.test(fileName)) {
const json = fs.readFileSync(path.resolve(diskPath, fileName));
const data = JSON.parse(json);
if (data.private) { continue; }
if (!data.name || !data.category) { continue; }
if (data.private) {
continue;
}
if (!data.name || !data.category) {
continue;
}
const entry = {
filename: `json/disks/${fileName}`,

View File

@ -1,6 +1,6 @@
#header {
width: 580px;
margin: auto;
width: 580px;
margin: auto;
}
img {
@ -16,22 +16,22 @@ img {
margin: 0;
padding: 3px 0 0 10;
color: black;
font-family: "Adobe Garamond Pro",Garamond,Times;
font-family: 'Adobe Garamond Pro', Garamond, Times;
font-size: 13px;
font-weight: normal;
user-select: none;
}
.motter {
font-family: "Motter Tektura";
font-family: 'Motter Tektura';
font-size: 12px;
}
input[type="button"] {
input[type='button'] {
width: 75px;
}
input[type="text"] {
input[type='text'] {
width: 40px;
}
@ -73,7 +73,7 @@ body {
padding: 0;
border: 0;
position: fixed;
top:0;
top: 0;
bottom: 0;
left: 0;
right: 0;
@ -100,7 +100,9 @@ body {
height: 100%;
}
.full-page #header, .full-page .inset, .full-page #reset {
.full-page #header,
.full-page .inset,
.full-page #reset {
display: none;
}
@ -187,9 +189,9 @@ th {
}
canvas {
display: block;
float: left;
image-rendering: pixelated;
display: block;
float: left;
image-rendering: pixelated;
}
.mono {
@ -199,7 +201,13 @@ canvas {
.scanlines:after {
display: block;
pointer-events: none;
background-image: repeating-linear-gradient(to bottom, transparent 0, transparent 1px, rgba(0,0,0,0.5) 1px, rgba(0,0,0,0.5) 2px);
background-image: repeating-linear-gradient(
to bottom,
transparent 0,
transparent 1px,
rgba(0, 0, 0, 0.5) 1px,
rgba(0, 0, 0, 0.5) 2px
);
content: '';
position: absolute;
top: 0;
@ -209,7 +217,13 @@ canvas {
}
.full-page .scanlines:after {
background-image: repeating-linear-gradient(to bottom, transparent 0, transparent 0.25vh, rgba(0,0,0,0.5) 0.25vh, rgba(0,0,0,0.5) 0.5vh);
background-image: repeating-linear-gradient(
to bottom,
transparent 0,
transparent 0.25vh,
rgba(0, 0, 0, 0.5) 0.25vh,
rgba(0, 0, 0, 0.5) 0.5vh
);
}
#screen {
@ -238,7 +252,7 @@ canvas {
#about {
padding: 16px;
margin: 0;
font-family: "Adobe Garamond Pro",Garamond,Times;
font-family: 'Adobe Garamond Pro', Garamond, Times;
font-size: 14px;
background-color: #fff;
color: #000;
@ -287,7 +301,7 @@ canvas {
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.6);
background: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
@ -307,10 +321,10 @@ canvas {
font-size: 14px;
justify-content: space-between;
align-items: center;
background: #44372C;
background: #44372c;
color: #fff;
padding: 5px 11px;
border: 1px outset #66594E;
border: 1px outset #66594e;
border-radius: 3px;
user-select: none;
}
@ -325,13 +339,15 @@ canvas {
box-sizing: border-box;
}
.modal__close, .modal__close:active, .modal__close:hover {
.modal__close,
.modal__close:active,
.modal__close:hover {
background: transparent;
padding: 3px;
}
.modal__header .modal__close:before {
content: "\2715";
content: '\2715';
}
.modal__content {
@ -348,10 +364,10 @@ canvas {
button,
a.button {
background: #44372C;
background: #44372c;
color: #fff;
padding: 2px 8px;
border: 1px outset #66594E;
border: 1px outset #66594e;
border-radius: 3px;
font-size: 15px;
text-decoration: none;
@ -359,14 +375,14 @@ a.button {
button:hover,
a.button:hover {
background-color: #55473D;
border: 1px outset #66594E;
background-color: #55473d;
border: 1px outset #66594e;
}
button:active,
a.button:active {
background-color: #22150A;
border: 1px outset #44372C;
background-color: #22150a;
border: 1px outset #44372c;
}
button:focus,
@ -374,7 +390,8 @@ a.button:hover {
outline: none;
}
#keyboard, #reset {
#keyboard,
#reset {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
@ -435,23 +452,23 @@ a.button:hover {
padding: 0;
width: 32px;
height: 32px;
background-color: #44372C;
background-color: #44372c;
color: white;
text-align: center;
position: relative;
border-left: 5px solid #65594D;
border-top: 5px solid #65594D;
border-right: 5px solid #110E0D;
border-bottom: 5px solid #110E0D;
border-left: 5px solid #65594d;
border-top: 5px solid #65594d;
border-right: 5px solid #110e0d;
border-bottom: 5px solid #110e0d;
/* border: 5px outset #66594E; */
border-radius: 5px;
cursor: pointer;
}
#keyboard .pressed {
background-color: #22150A;
border-left: 5px solid #44372C;
border-top: 5px solid #44372C;
background-color: #22150a;
border-left: 5px solid #44372c;
border-top: 5px solid #44372c;
border-right: 5px solid #000000;
border-bottom: 5px solid #000000;
/* border: 5px outset #44372C; */
@ -602,11 +619,11 @@ a.button:hover {
}
#reset {
background: #44372C;
border-left: 3px solid #65594D;
border-top: 3px solid #65594D;
border-right: 3px solid #110E0D;
border-bottom: 3px solid #110E0D;
background: #44372c;
border-left: 3px solid #65594d;
border-top: 3px solid #65594d;
border-right: 3px solid #110e0d;
border-bottom: 3px solid #110e0d;
/* border: 5px outset #66594E; */
border-radius: 3px;
color: white;
@ -621,14 +638,14 @@ a.button:hover {
}
#reset:hover {
background: #44372C;
border: 3px outset #66594E;
background: #44372c;
border: 3px outset #66594e;
}
#reset:active {
background-color: #22150A;
border-left: 3px solid #44372C;
border-top: 3px solid #44372C;
background-color: #22150a;
border-left: 3px solid #44372c;
border-top: 3px solid #44372c;
border-right: 3px solid #000000;
border-bottom: 3px solid #000000;
}
@ -654,8 +671,8 @@ a.button:hover {
}
.standalone {
position: fixed;
width: 100%;
position: fixed;
width: 100%;
}
.standalone #header {

View File

@ -1,32 +1,38 @@
<!DOCTYPE html>
<!doctype html>
<head>
<title>PreApple II</title>
<meta name="viewport" content="width=640, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta charset="utf-8" />
<title>PreApple II</title>
<meta name="viewport" content="width=640, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta charset="utf-8" />
<link rel="apple-touch-icon" href="img/webapp-iphone.png" />
<link rel="apple-touch-icon" size="72x72" href="img/webapp-ipad.png" />
<link rel="shortcut icon" href="img/logoicon.png" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v6.1.1/css/all.css" />
<style>
body {
background-color: #c4c1a0;
margin: 16px 0;
}
</style>
<link rel="apple-touch-icon" href="img/webapp-iphone.png" />
<link rel="apple-touch-icon" size="72x72" href="img/webapp-ipad.png" />
<link rel="shortcut icon" href="img/logoicon.png" />
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v6.1.1/css/all.css"
/>
<style>
body {
background-color: #c4c1a0;
margin: 16px 0;
}
</style>
</head>
<body>
<div id="app"></div>
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
<filter id="green">
<feColorMatrix type="matrix" values="0.0 0.0 0.0 0.0 0
<div id="app"></div>
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
<filter id="green">
<feColorMatrix
type="matrix"
values="0.0 0.0 0.0 0.0 0
0.0 1.0 0.0 0.0 0
0.0 0.0 0.5 0.0 0
0.0 0.0 0.0 1.0 0" />
</filter>
</svg>
<script src="dist/preact.bundle.js"></script>
0.0 0.0 0.0 1.0 0"
/>
</filter>
</svg>
<script src="dist/preact.bundle.js"></script>
</body>

View File

@ -1,5 +1,5 @@
module.exports = {
'moduleNameMapper': {
moduleNameMapper: {
'^js/(.*)': '<rootDir>/js/$1',
'^test/(.*)': '<rootDir>/test/$1',
'\\.css$': 'identity-obj-proxy',
@ -9,28 +9,17 @@ module.exports = {
// https://github.com/preactjs/enzyme-adapter-preact-pure/issues/179#issuecomment-1201096897
'^preact(/(.*)|$)': 'preact$1',
},
'roots': [
'js/',
'test/',
],
'testMatch': [
'**/?(*.)+(spec|test).+(ts|js|tsx)'
],
'transform': {
roots: ['js/', 'test/'],
testMatch: ['**/?(*.)+(spec|test).+(ts|js|tsx)'],
transform: {
'^.+\\.js$': 'babel-jest',
'^.+\\.ts$': 'ts-jest',
'^.*\\.tsx$': 'ts-jest',
},
'transformIgnorePatterns': [
transformIgnorePatterns: [
'/node_modules/(?!(@testing-library/preact/dist/esm)/)',
],
'setupFilesAfterEnv': [
'<rootDir>/test/jest-setup.ts'
],
'coveragePathIgnorePatterns': [
'/node_modules/',
'/js/roms/',
'/test/',
],
'preset': 'ts-jest',
setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'],
coveragePathIgnorePatterns: ['/node_modules/', '/js/roms/', '/test/'],
preset: 'ts-jest',
};

View File

@ -5,16 +5,8 @@ import {
VideoModes,
VideoModesState,
} from './videomodes';
import {
HiresPage2D,
LoresPage2D,
VideoModes2D,
} from './canvas';
import {
HiresPageGL,
LoresPageGL,
VideoModesGL,
} from './gl';
import { HiresPage2D, LoresPage2D, VideoModes2D } from './canvas';
import { HiresPageGL, LoresPageGL, VideoModesGL } from './gl';
import ROM from './roms/rom';
import { Apple2IOState } from './apple2io';
import {
@ -82,7 +74,7 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
private stats: Stats = {
cycles: 0,
frames: 0,
renderedFrames: 0
renderedFrames: 0,
};
public ready: Promise<void>;
@ -92,23 +84,30 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
}
async init(options: Apple2Options) {
const romImportPromise = import(`./roms/system/${options.rom}`) as Promise<{ default: new () => ROM }>;
const characterRomImportPromise = import(`./roms/character/${options.characterRom}`) as Promise<{ default: ReadonlyUint8Array }>;
const romImportPromise = import(
`./roms/system/${options.rom}`
) as Promise<{
default: new () => ROM;
}>;
const characterRomImportPromise = import(
`./roms/character/${options.characterRom}`
) as Promise<{ default: ReadonlyUint8Array }>;
const LoresPage = options.gl ? LoresPageGL : LoresPage2D;
const HiresPage = options.gl ? HiresPageGL : HiresPage2D;
const VideoModes = options.gl ? VideoModesGL : VideoModes2D;
this.cpu = new CPU6502({
flavor: options.enhanced ? FLAVOR_ROCKWELL_65C02 : FLAVOR_6502
flavor: options.enhanced ? FLAVOR_ROCKWELL_65C02 : FLAVOR_6502,
});
this.vm = new VideoModes(options.canvas, options.e);
const [{ default: Apple2ROM }, { default: characterRom }] = await Promise.all([
romImportPromise,
characterRomImportPromise,
this.vm.ready,
]);
const [{ default: Apple2ROM }, { default: characterRom }] =
await Promise.all([
romImportPromise,
characterRomImportPromise,
this.vm.ready,
]);
this.rom = new Apple2ROM();
this.characterRom = characterRom;
@ -121,13 +120,22 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
this.tick = options.tick;
if (options.e) {
this.mmu = new MMU(this.cpu, this.vm, this.gr, this.gr2, this.hgr, this.hgr2, this.io, this.rom);
this.mmu = new MMU(
this.cpu,
this.vm,
this.gr,
this.gr2,
this.hgr,
this.hgr2,
this.io,
this.rom
);
this.cpu.addPageHandler(this.mmu);
} else {
this.ram = [
new RAM(0x00, 0x03),
new RAM(0x0C, 0x1F),
new RAM(0x60, 0xBF)
new RAM(0x0c, 0x1f),
new RAM(0x60, 0xbf),
];
this.cpu.addPageHandler(this.ram[0]);
@ -158,7 +166,8 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
const interval = 30;
let now, last = Date.now();
let now,
last = Date.now();
const runFn = () => {
const kHz = this.io.getKHz();
now = Date.now();
@ -228,7 +237,7 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
vm: this.vm.getState(),
io: this.io.getState(),
mmu: this.mmu?.getState(),
ram: this.ram?.map(bank => bank.getState()),
ram: this.ram?.map((bank) => bank.getState()),
};
return state;

View File

@ -12,7 +12,7 @@ type Annunciators = Record<annunciator, boolean>;
export interface Apple2IOState {
annunciators: Annunciators;
cards: Array<unknown | null>;
cards: Array<unknown>;
}
export type SampleListener = (sample: number[]) => void;
@ -33,12 +33,12 @@ const LOC = {
SETHIRES: 0x57, // select Hi-res
CLRAN0: 0x58, // Set annunciator-0 output to 0
SETAN0: 0x59, // Set annunciator-0 output to 1
CLRAN1: 0x5A, // Set annunciator-1 output to 0
SETAN1: 0x5B, // Set annunciator-1 output to 1
CLRAN2: 0x5C, // Set annunciator-2 output to 0
SETAN2: 0x5D, // Set annunciator-2 output to 1
CLRAN3: 0x5E, // Set annunciator-3 output to 0
SETAN3: 0x5F, // Set annunciator-3 output to 1
CLRAN1: 0x5a, // Set annunciator-1 output to 0
SETAN1: 0x5b, // Set annunciator-1 output to 1
CLRAN2: 0x5c, // Set annunciator-2 output to 0
SETAN2: 0x5d, // Set annunciator-2 output to 1
CLRAN3: 0x5e, // Set annunciator-3 output to 0
SETAN3: 0x5f, // Set annunciator-3 output to 1
TAPEIN: 0x60, // bit 7: data from cassette
PB0: 0x61, // game Pushbutton 0 / open apple (command) key data
PB1: 0x62, // game Pushbutton 1 / closed apple (option) key data
@ -51,7 +51,9 @@ const LOC = {
ACCEL: 0x74, // CPU Speed control
};
export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState> {
export default class Apple2IO
implements MemoryPages, Restorable<Apple2IOState>
{
private _slot: Array<Card | null> = new Array<Card | null>(7).fill(null);
private _auxRom: Memory | null = null;
@ -85,7 +87,10 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
private _tapeNext: number = 0;
private _tapeCurrent = false;
constructor(private readonly cpu: CPU6502, private readonly vm: VideoModes) {
constructor(
private readonly cpu: CPU6502,
private readonly vm: VideoModes
) {
this.init();
}
@ -99,8 +104,16 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
_tick() {
const now = this.cpu.getCycles();
const phase = this._didAudio ? (this._phase > 0 ? this._high : this._low) : 0.0;
for (; this._sampleTime < now; this._sampleTime += this._cycles_per_sample) {
const phase = this._didAudio
? this._phase > 0
? this._high
: this._low
: 0.0;
for (
;
this._sampleTime < now;
this._sampleTime += this._cycles_per_sample
) {
this._sample[this._sampleIdx++] = phase;
if (this._sampleIdx === this._sample_size) {
if (this._audioListener) {
@ -114,7 +127,7 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
}
_calcSampleRate() {
this._cycles_per_sample = this._khz * 1000 / this._rate;
this._cycles_per_sample = (this._khz * 1000) / this._rate;
}
_updateKHz(khz: number) {
@ -200,16 +213,16 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
result = this._button[2] ? 0x80 : 0;
break;
case LOC.PADDLE0:
result = (delta < (this._paddle[0] * 2756) ? 0x80 : 0x00);
result = delta < this._paddle[0] * 2756 ? 0x80 : 0x00;
break;
case LOC.PADDLE1:
result = (delta < (this._paddle[1] * 2756) ? 0x80 : 0x00);
result = delta < this._paddle[1] * 2756 ? 0x80 : 0x00;
break;
case LOC.PADDLE2:
result = (delta < (this._paddle[2] * 2756) ? 0x80 : 0x00);
result = delta < this._paddle[2] * 2756 ? 0x80 : 0x00;
break;
case LOC.PADDLE3:
result = (delta < (this._paddle[3] * 2756) ? 0x80 : 0x00);
result = delta < this._paddle[3] * 2756 ? 0x80 : 0x00;
break;
case LOC.ACCEL:
if (val !== undefined) {
@ -225,13 +238,12 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
if (this._tapeOffset < this._tape.length) {
this._tapeCurrent = this._tape[this._tapeOffset][1];
while (now >= this._tapeNext) {
if ((this._tapeOffset % 1000) === 0) {
if (this._tapeOffset % 1000 === 0) {
debug(`Read ${this._tapeOffset / 1000}`);
}
this._tapeCurrent = this._tape[this._tapeOffset][1];
this._tapeNext += this._tape[this._tapeOffset++][0];
}
}
result = this._tapeCurrent ? 0x80 : 0x00;
@ -344,7 +356,7 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
slot = page & 0x0f;
card = this._slot[slot];
if (this._auxRom !== card) {
// _debug('Setting auxRom to slot', slot);
// _debug('Setting auxRom to slot', slot);
this._auxRom = card;
}
if (card) {
@ -380,7 +392,7 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
slot = page & 0x0f;
card = this._slot[slot];
if (this._auxRom !== card) {
// _debug('Setting auxRom to slot', slot);
// _debug('Setting auxRom to slot', slot);
this._auxRom = card;
}
if (card) {
@ -399,13 +411,15 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
// TODO vet more potential state
return {
annunciators: this._annunciators,
cards: this._slot.map((card) => card ? card.getState() : null)
cards: this._slot.map((card) => (card ? card.getState() : null)),
};
}
setState(state: Apple2IOState) {
this._annunciators = state.annunciators;
state.cards.map((cardState, idx) => this._slot[idx]?.setState(cardState));
state.cards.map(
(cardState, idx) => this._slot[idx]?.setState(cardState)
);
}
setSlot(slot: slot, card: Card) {

View File

@ -51,7 +51,10 @@ function writeWord(mem: Memory, addr: word, val: byte) {
class LineBuffer implements IterableIterator<string> {
private prevChar: number = 0;
constructor(private readonly line: string, private curChar: number = 0) { }
constructor(
private readonly line: string,
private curChar: number = 0
) {}
[Symbol.iterator](): IterableIterator<string> {
return this;
@ -132,7 +135,11 @@ export default class ApplesoftCompiler {
* @param programStart Optional start address of the program. Defaults to
* standard Applesoft program address, 0x801.
*/
static compileToMemory(mem: Memory, program: string, programStart: word = PROGRAM_START) {
static compileToMemory(
mem: Memory,
program: string,
programStart: word = PROGRAM_START
) {
const compiler = new ApplesoftCompiler();
compiler.compile(program);
const compiledProgram: Uint8Array = compiler.program(programStart);
@ -305,8 +312,8 @@ export default class ApplesoftCompiler {
for (const lineNo of lineNumbers) {
const lineBytes = this.lines.get(lineNo) || [];
const nextLineAddr = programStart + result.length + 4
+ lineBytes.length + 1; // +1 for the zero at end of line
const nextLineAddr =
programStart + result.length + 4 + lineBytes.length + 1; // +1 for the zero at end of line
result.push(nextLineAddr & 0xff, nextLineAddr >> 8);
result.push(lineNo & 0xff, lineNo >> 8);
result.push(...lineBytes);

View File

@ -48,7 +48,6 @@ const DEFAULT_DECOMPILE_OPTIONS: DecompileOptions = {
const MAX_LINES = 32768;
export default class ApplesoftDecompiler {
/**
* Returns a decompiler for the program in the given memory.
*
@ -57,10 +56,16 @@ export default class ApplesoftDecompiler {
static decompilerFromMemory(ram: Memory): ApplesoftDecompiler {
const program: byte[] = [];
const start = ram.read(0x00, TXTTAB) + (ram.read(0x00, TXTTAB + 1) << 8);
const start =
ram.read(0x00, TXTTAB) + (ram.read(0x00, TXTTAB + 1) << 8);
const end = ram.read(0x00, PRGEND) + (ram.read(0x00, PRGEND + 1) << 8);
if (start >= 0xc000 || end >= 0xc000) {
throw new Error(`Program memory ${toHex(start, 4)}-${toHex(end, 4)} out of range`);
throw new Error(
`Program memory ${toHex(start, 4)}-${toHex(
end,
4
)} out of range`
);
}
for (let addr = start; addr <= end; addr++) {
program.push(ram.read(addr >> 8, addr & 0xff));
@ -117,8 +122,10 @@ export default class ApplesoftDecompiler {
* is the offset of the line number of the line; the tokens follow.
*/
private forEachLine(
from: number, to: number,
callback: (offset: word) => void): void {
from: number,
to: number,
callback: (offset: word) => void
): void {
let count = 0;
let offset = 0;
let nextLineAddr = this.wordAt(offset);
@ -223,12 +230,15 @@ export default class ApplesoftDecompiler {
* @param from The first line to print (default 0).
* @param to The last line to print (default end of program).
*/
list(options: Partial<ListOptions> = {},
from: number = 0, to: number = 65536): string {
list(
options: Partial<ListOptions> = {},
from: number = 0,
to: number = 65536
): string {
const allOptions = { ...DEFAULT_LIST_OPTIONS, ...options };
let result = '';
this.forEachLine(from, to, offset => {
this.forEachLine(from, to, (offset) => {
result += this.listLine(offset, allOptions);
});
return result;
@ -264,7 +274,8 @@ export default class ApplesoftDecompiler {
spaceIf = () => false;
if (token === STRING_TO_TOKEN['AT']) {
spaceIf = (nextToken) => nextToken.toUpperCase().startsWith('N');
spaceIf = (nextToken) =>
nextToken.toUpperCase().startsWith('N');
}
offset++;
@ -329,13 +340,20 @@ export default class ApplesoftDecompiler {
/**
* Decompiles the program based on the given options.
*/
decompile(options: Partial<DecompileOptions> = {},
from: number = 0, to: number = 65536): string {
decompile(
options: Partial<DecompileOptions> = {},
from: number = 0,
to: number = 65536
): string {
const allOptions = { ...DEFAULT_DECOMPILE_OPTIONS, ...options };
const results: string[] = [];
this.forEachLine(from, to, offset => {
results.push(allOptions.style === 'compact' ? this.compactLine(offset) : this.prettyLine(offset));
this.forEachLine(from, to, (offset) => {
results.push(
allOptions.style === 'compact'
? this.compactLine(offset)
: this.prettyLine(offset)
);
});
return results.join('\n');
}

View File

@ -1,23 +1,15 @@
import { byte, word, Memory } from 'js/types';
import { toHex } from 'js/util';
import {
CURLINE,
ARG,
FAC,
ARYTAB,
STREND,
TXTTAB,
VARTAB
} from './zeropage';
import { CURLINE, ARG, FAC, ARYTAB, STREND, TXTTAB, VARTAB } from './zeropage';
export type ApplesoftValue = word | number | string | ApplesoftArray;
export type ApplesoftValue = word | string | ApplesoftArray;
export type ApplesoftArray = Array<ApplesoftValue>;
export enum VariableType {
Float = 0,
String = 1,
Function = 2,
Integer = 3
Integer = 3,
}
export interface ApplesoftVariable {
@ -27,7 +19,6 @@ export interface ApplesoftVariable {
value: ApplesoftValue | undefined;
}
export class ApplesoftHeap {
constructor(private mem: Memory) {}
@ -61,7 +52,7 @@ export class ApplesoftHeap {
if (exponent === 0) {
return 0;
}
exponent = (exponent & 0x80 ? 1 : -1) * ((exponent & 0x7F) - 1);
exponent = (exponent & 0x80 ? 1 : -1) * ((exponent & 0x7f) - 1);
let msb = this.readByte(addr + 1);
const sb3 = this.readByte(addr + 2);
@ -74,7 +65,7 @@ export class ApplesoftHeap {
} else {
sign = msb & 0x80 ? -1 : 1;
}
msb &= 0x7F;
msb &= 0x7f;
const mantissa = (msb << 24) | (sb3 << 16) | (sb2 << 8) | lsb;
return sign * (1 + mantissa / 0x80000000) * Math.pow(2, exponent);
@ -83,7 +74,7 @@ export class ApplesoftHeap {
private readString(len: byte, addr: word): string {
let str = '';
for (let idx = 0; idx < len; idx++) {
str += String.fromCharCode(this.readByte(addr + idx) & 0x7F);
str += String.fromCharCode(this.readByte(addr + idx) & 0x7f);
}
return str;
}
@ -91,13 +82,13 @@ export class ApplesoftHeap {
private readVar(addr: word) {
const firstByte = this.readByte(addr);
const lastByte = this.readByte(addr + 1);
const firstLetter = firstByte & 0x7F;
const lastLetter = lastByte & 0x7F;
const firstLetter = firstByte & 0x7f;
const lastLetter = lastByte & 0x7f;
const name =
String.fromCharCode(firstLetter) +
(lastLetter ? String.fromCharCode(lastLetter) : '');
const type = (lastByte & 0x80) >> 7 | (firstByte & 0x80) >> 6;
const type = ((lastByte & 0x80) >> 7) | ((firstByte & 0x80) >> 6);
return { name, type };
}
@ -158,7 +149,7 @@ export class ApplesoftHeap {
for (addr = simpleVariableTable; addr < arrayVariableTable; addr += 7) {
const { name, type } = this.readVar(addr);
switch (type) {
switch (type as VariableType) {
case VariableType.Float:
value = this.readFloat(addr + 2);
break;

View File

@ -108,116 +108,116 @@ export const TOKEN_TO_STRING: Record<byte, string> = {
0xe7: 'CHR$',
0xe8: 'LEFT$',
0xe9: 'RIGHT$',
0xea: 'MID$'
0xea: 'MID$',
};
/** Map from keyword to token. */
export const STRING_TO_TOKEN: Record<string, byte> = {
'END': 0x80,
'FOR': 0x81,
'NEXT': 0x82,
'DATA': 0x83,
'INPUT': 0x84,
'DEL': 0x85,
'DIM': 0x86,
'READ': 0x87,
'GR': 0x88,
'TEXT': 0x89,
END: 0x80,
FOR: 0x81,
NEXT: 0x82,
DATA: 0x83,
INPUT: 0x84,
DEL: 0x85,
DIM: 0x86,
READ: 0x87,
GR: 0x88,
TEXT: 0x89,
'PR#': 0x8a,
'IN#': 0x8b,
'CALL': 0x8c,
'PLOT': 0x8d,
'HLIN': 0x8e,
'VLIN': 0x8f,
'HGR2': 0x90,
'HGR': 0x91,
CALL: 0x8c,
PLOT: 0x8d,
HLIN: 0x8e,
VLIN: 0x8f,
HGR2: 0x90,
HGR: 0x91,
'HCOLOR=': 0x92,
'HPLOT': 0x93,
'DRAW': 0x94,
'XDRAW': 0x95,
'HTAB': 0x96,
'HOME': 0x97,
HPLOT: 0x93,
DRAW: 0x94,
XDRAW: 0x95,
HTAB: 0x96,
HOME: 0x97,
'ROT=': 0x98,
'SCALE=': 0x99,
'SHLOAD': 0x9a,
'TRACE': 0x9b,
'NOTRACE': 0x9c,
'NORMAL': 0x9d,
'INVERSE': 0x9e,
'FLASH': 0x9f,
SHLOAD: 0x9a,
TRACE: 0x9b,
NOTRACE: 0x9c,
NORMAL: 0x9d,
INVERSE: 0x9e,
FLASH: 0x9f,
'COLOR=': 0xa0,
'POP=': 0xa1,
'VTAB': 0xa2,
VTAB: 0xa2,
'HIMEM:': 0xa3,
'LOMEM:': 0xa4,
'ONERR': 0xa5,
'RESUME': 0xa6,
'RECALL': 0xa7,
'STORE': 0xa8,
ONERR: 0xa5,
RESUME: 0xa6,
RECALL: 0xa7,
STORE: 0xa8,
'SPEED=': 0xa9,
'LET': 0xaa,
'GOTO': 0xab,
'RUN': 0xac,
'IF': 0xad,
'RESTORE': 0xae,
LET: 0xaa,
GOTO: 0xab,
RUN: 0xac,
IF: 0xad,
RESTORE: 0xae,
'&': 0xaf,
'GOSUB': 0xb0,
'RETURN': 0xb1,
'REM': 0xb2,
'STOP': 0xb3,
'ON': 0xb4,
'WAIT': 0xb5,
'LOAD': 0xb6,
'SAVE': 0xb7,
'DEF': 0xb8,
'POKE': 0xb9,
'PRINT': 0xba,
'CONT': 0xbb,
'LIST': 0xbc,
'CLEAR': 0xbd,
'GET': 0xbe,
'NEW': 0xbf,
GOSUB: 0xb0,
RETURN: 0xb1,
REM: 0xb2,
STOP: 0xb3,
ON: 0xb4,
WAIT: 0xb5,
LOAD: 0xb6,
SAVE: 0xb7,
DEF: 0xb8,
POKE: 0xb9,
PRINT: 0xba,
CONT: 0xbb,
LIST: 0xbc,
CLEAR: 0xbd,
GET: 0xbe,
NEW: 0xbf,
'TAB(': 0xc0,
'TO': 0xc1,
'FN': 0xc2,
TO: 0xc1,
FN: 0xc2,
'SPC(': 0xc3,
'THEN': 0xc4,
'AT': 0xc5,
'NOT': 0xc6,
'STEP': 0xc7,
THEN: 0xc4,
AT: 0xc5,
NOT: 0xc6,
STEP: 0xc7,
'+': 0xc8,
'-': 0xc9,
'*': 0xca,
'/': 0xcb,
'^': 0xcc,
'AND': 0xcd,
'OR': 0xce,
AND: 0xcd,
OR: 0xce,
'>': 0xcf,
'=': 0xd0,
'<': 0xd1,
'SGN': 0xd2,
'INT': 0xd3,
'ABS': 0xd4,
'USR': 0xd5,
'FRE': 0xd6,
SGN: 0xd2,
INT: 0xd3,
ABS: 0xd4,
USR: 0xd5,
FRE: 0xd6,
'SCRN(': 0xd7,
'PDL': 0xd8,
'POS': 0xd9,
'SQR': 0xda,
'RND': 0xdb,
'LOG': 0xdc,
'EXP': 0xdd,
'COS': 0xde,
'SIN': 0xdf,
'TAN': 0xe0,
'ATN': 0xe1,
'PEEK': 0xe2,
'LEN': 0xe3,
'STR$': 0xe4,
'VAL': 0xe5,
'ASC': 0xe6,
'CHR$': 0xe7,
'LEFT$': 0xe8,
'RIGHT$': 0xe9,
'MID$': 0xea
PDL: 0xd8,
POS: 0xd9,
SQR: 0xda,
RND: 0xdb,
LOG: 0xdc,
EXP: 0xdd,
COS: 0xde,
SIN: 0xdf,
TAN: 0xe0,
ATN: 0xe1,
PEEK: 0xe2,
LEN: 0xe3,
STR$: 0xe4,
VAL: 0xe5,
ASC: 0xe6,
CHR$: 0xe7,
LEFT$: 0xe8,
RIGHT$: 0xe9,
MID$: 0xea,
};

View File

@ -11,17 +11,17 @@ export const TXTTAB = 0x67;
/** Start of variables (word) */
export const VARTAB = 0x69;
/** Start of arrays (word) */
export const ARYTAB = 0x6B;
export const ARYTAB = 0x6b;
/** End of strings (word). (Strings are allocated down from HIMEM.) */
export const STREND = 0x6D;
export const STREND = 0x6d;
/** Current line */
export const CURLINE = 0x75;
/** Floating Point accumulator (float) */
export const FAC = 0x9D;
export const FAC = 0x9d;
/** Floating Point arguments (float) */
export const ARG = 0xA5;
export const ARG = 0xa5;
/**
* End of program (word). This is actually 1 or 2 bytes past the three
* zero bytes that end the program.
*/
export const PRGEND = 0xAF;
export const PRGEND = 0xaf;

View File

@ -5,7 +5,9 @@ const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
/** Encode an array of bytes in base64. */
export function base64_encode(data: null | undefined): undefined;
export function base64_encode(data: memory): string;
export function base64_encode(data: memory | null | undefined): string | undefined {
export function base64_encode(
data: memory | null | undefined
): string | undefined {
// Twacked by Will Scullin to handle arrays of 'bytes'
// http://kevin.vanzonneveld.net
@ -25,28 +27,39 @@ export function base64_encode(data: memory | null | undefined): string | undefin
// return atob(data);
//}
let o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc='';
let o1,
o2,
o3,
h1,
h2,
h3,
h4,
bits,
i = 0,
ac = 0,
enc = '';
const tmp_arr = [];
if (!data) {
return undefined;
}
do { // pack three octets into four hexets
do {
// pack three octets into four hexets
o1 = data[i++];
o2 = data[i++];
o3 = data[i++];
bits = o1<<16 | o2<<8 | o3;
bits = (o1 << 16) | (o2 << 8) | o3;
h1 = bits>>18 & 0x3f;
h2 = bits>>12 & 0x3f;
h3 = bits>>6 & 0x3f;
h1 = (bits >> 18) & 0x3f;
h2 = (bits >> 12) & 0x3f;
h3 = (bits >> 6) & 0x3f;
h4 = bits & 0x3f;
// use hexets to index into b64, and append result to encoded string
tmp_arr[ac++] = B64.charAt(h1) + B64.charAt(h2) + B64.charAt(h3) + B64.charAt(h4);
tmp_arr[ac++] =
B64.charAt(h1) + B64.charAt(h2) + B64.charAt(h3) + B64.charAt(h4);
} while (i < data.length);
enc = tmp_arr.join('');
@ -68,7 +81,9 @@ export function base64_decode(data: null | undefined): undefined;
/** Returns an array of bytes from the given base64-encoded string. */
export function base64_decode(data: string): memory;
/** Returns an array of bytes from the given base64-encoded string. */
export function base64_decode(data: string | null | undefined): memory | undefined {
export function base64_decode(
data: string | null | undefined
): memory | undefined {
// Twacked by Will Scullin to handle arrays of 'bytes'
// http://kevin.vanzonneveld.net
@ -91,23 +106,33 @@ export function base64_decode(data: string | null | undefined): memory | undefin
// return btoa(data);
//}
let o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0;
let o1,
o2,
o3,
h1,
h2,
h3,
h4,
bits,
i = 0,
ac = 0;
const tmp_arr = [];
if (!data) {
return undefined;
}
do { // unpack four hexets into three octets using index points in B64
do {
// unpack four hexets into three octets using index points in B64
h1 = B64.indexOf(data.charAt(i++));
h2 = B64.indexOf(data.charAt(i++));
h3 = B64.indexOf(data.charAt(i++));
h4 = B64.indexOf(data.charAt(i++));
bits = h1<<18 | h2<<12 | h3<<6 | h4;
bits = (h1 << 18) | (h2 << 12) | (h3 << 6) | h4;
o1 = bits>>16 & 0xff;
o2 = bits>>8 & 0xff;
o1 = (bits >> 16) & 0xff;
o2 = (bits >> 8) & 0xff;
o3 = bits & 0xff;
tmp_arr[ac++] = o1;
@ -126,7 +151,7 @@ const DATA_URL_PREFIX = 'data:application/octet-stream;base64,';
export function base64_json_parse(json: string): 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 value;

View File

@ -8,15 +8,11 @@ import {
VideoModes,
VideoModesState,
bank,
pageNo
pageNo,
} from './videomodes';
const dim = (c: Color): Color => {
return [
c[0] * 0.75 & 0xff,
c[1] * 0.75 & 0xff,
c[2] * 0.75 & 0xff
];
return [(c[0] * 0.75) & 0xff, (c[1] * 0.75) & 0xff, (c[2] * 0.75) & 0xff];
};
// hires colors
@ -49,25 +45,25 @@ const _colors: Color[] = [
//
const r4 = [
0, // Black
2, // Dark Blue
4, // Dark Green
6, // Medium Blue
0, // Black
2, // Dark Blue
4, // Dark Green
6, // Medium Blue
8, // Brown
5, // Gray 1
12, // Light Green
14, // Aqua
8, // Brown
5, // Gray 1
12, // Light Green
14, // Aqua
1, // Red
3, // Purple
10, // Gray 2
7, // Pink
1, // Red
3, // Purple
10, // Gray 2
7, // Pink
9, // Orange
11, // Light Blue
13, // Yellow
15 // White
9, // Orange
11, // Light Blue
13, // Yellow
15, // White
] as const;
const dcolors: Color[] = [
@ -93,7 +89,7 @@ const notDirty: Region = {
top: 193,
bottom: -1,
left: 561,
right: -1
right: -1,
} as const;
/****************************************************************************
@ -131,14 +127,18 @@ export class LoresPage2D implements LoresPage {
}
private _drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = data[off + 4] = c0;
data[off + 1] = data[off + 5] = c1;
data[off + 2] = data[off + 6] = c2;
}
private _drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = c0;
data[off + 1] = c1;
data[off + 2] = c2;
@ -148,10 +148,10 @@ export class LoresPage2D implements LoresPage {
let inverse = false;
if (this.e) {
if (!this.vm._80colMode && !this.vm.altCharMode) {
inverse = ((val & 0xc0) === 0x40) && this._blink;
inverse = (val & 0xc0) === 0x40 && this._blink;
}
} else {
inverse = !((val & 0x80) || (val & 0x40) && this._blink);
inverse = !(val & 0x80 || (val & 0x40 && this._blink));
}
return inverse;
}
@ -177,19 +177,22 @@ export class LoresPage2D implements LoresPage {
// These are used by both bank 0 and 1
private _start() {
return (0x04 * this.page);
return 0x04 * this.page;
}
private _end() { return (0x04 * this.page) + 0x03; }
private _end() {
return 0x04 * this.page + 0x03;
}
private _read(page: byte, off: byte, bank: bank) {
const addr = (page << 8) | off, base = addr & 0x3FF;
const addr = (page << 8) | off,
base = addr & 0x3ff;
return this._buffer[bank][base];
}
private _write(page: byte, off: byte, val: byte, bank: bank) {
const addr = (page << 8) | off;
const base = addr & 0x3FF;
const base = addr & 0x3ff;
let fore, back;
if (this._buffer[bank][base] === val && !this._refreshing) {
@ -201,23 +204,35 @@ export class LoresPage2D implements LoresPage {
const adj = off - col;
// 000001cd eabab000 -> 000abcde
const ab = (adj & 0x18);
const ab = adj & 0x18;
const cd = (page & 0x03) << 1;
const ee = adj >> 7;
const row = ab | cd | ee;
const data = this.imageData.data;
if ((row < 24) && (col < 40)) {
if (row < 24 && col < 40) {
let y = row << 3;
if (y < this.dirty.top) { this.dirty.top = y; }
if (y < this.dirty.top) {
this.dirty.top = y;
}
y += 8;
if (y > this.dirty.bottom) { this.dirty.bottom = y; }
if (y > this.dirty.bottom) {
this.dirty.bottom = y;
}
let x = col * 14;
if (x < this.dirty.left) { this.dirty.left = x; }
if (x < this.dirty.left) {
this.dirty.left = x;
}
x += 14;
if (x > this.dirty.right) { this.dirty.right = x; }
if (x > this.dirty.right) {
this.dirty.right = x;
}
if (this.vm.textMode || this.vm.hiresMode || (this.vm.mixedMode && row > 19)) {
if (
this.vm.textMode ||
this.vm.hiresMode ||
(this.vm.mixedMode && row > 19)
) {
if (this.vm._80colMode) {
const inverse = this._checkInverse(val);
@ -225,15 +240,16 @@ export class LoresPage2D implements LoresPage {
back = inverse ? whiteCol : blackCol;
if (!this.vm.altCharMode) {
val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
val = val >= 0x40 && val < 0x80 ? val - 0x40 : val;
}
let offset = (col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
let offset =
(col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
for (let jdx = 0; jdx < 8; jdx++) {
let b = this.charset[val * 8 + jdx];
for (let idx = 0; idx < 7; idx++) {
const color = (b & 0x01) ? back : fore;
const color = b & 0x01 ? back : fore;
this._drawHalfPixel(data, offset, color);
b >>= 1;
offset += 4;
@ -249,7 +265,7 @@ export class LoresPage2D implements LoresPage {
back = inverse ? whiteCol : blackCol;
if (!this.vm.altCharMode) {
val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
val = val >= 0x40 && val < 0x80 ? val - 0x40 : val;
}
let offset = (col * 14 + row * 560 * 8) * 4;
@ -263,7 +279,7 @@ export class LoresPage2D implements LoresPage {
for (let jdx = 0; jdx < 8; jdx++) {
let b = this.charset[val * 8 + jdx];
for (let idx = 0; idx < 7; idx++) {
const color = (b & 0x01) ? back : fore;
const color = b & 0x01 ? back : fore;
this._drawPixel(data, offset, color);
b >>= 1;
offset += 8;
@ -271,7 +287,10 @@ export class LoresPage2D implements LoresPage {
offset += 546 * 4;
}
} else {
const colorMode = this.vm.mixedMode && !this.vm.textMode && !this.vm.monoMode;
const colorMode =
this.vm.mixedMode &&
!this.vm.textMode &&
!this.vm.monoMode;
// var val0 = col > 0 ? _buffer[0][base - 1] : 0;
// var val2 = col < 39 ? _buffer[0][base + 1] : 0;
@ -281,7 +300,9 @@ export class LoresPage2D implements LoresPage {
if (colorMode) {
// var b0 = charset[val0 * 8 + jdx];
// var b2 = charset[val2 * 8 + jdx];
if (inverse) { b ^= 0x1ff; }
if (inverse) {
b ^= 0x1ff;
}
}
for (let idx = 0; idx < 7; idx++) {
@ -298,7 +319,7 @@ export class LoresPage2D implements LoresPage {
}
odd = !odd;
} else {
color = (b & 0x80) ? fore : back;
color = b & 0x80 ? fore : back;
}
this._drawPixel(data, offset, color);
b <<= 1;
@ -310,17 +331,18 @@ export class LoresPage2D implements LoresPage {
}
} else {
if (this.vm._80colMode && !this.vm.an3State) {
let offset = (col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
let offset =
(col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
if (this.vm.monoMode) {
for (let jdx = 0; jdx < 8; jdx++) {
let b = (jdx < 4) ? (val & 0x0f) : (val >> 4);
b |= (b << 4);
b |= (b << 8);
let b = jdx < 4 ? val & 0x0f : val >> 4;
b |= b << 4;
b |= b << 8;
if (col & 0x1) {
b >>= 2;
}
for (let idx = 0; idx < 7; idx++) {
const color = (b & 0x01) ? whiteCol : blackCol;
const color = b & 0x01 ? whiteCol : blackCol;
this._drawHalfPixel(data, offset, color);
b >>= 1;
offset += 4;
@ -332,8 +354,8 @@ export class LoresPage2D implements LoresPage {
val = ((val & 0x77) << 1) | ((val & 0x88) >> 3);
}
for (let jdx = 0; jdx < 8; jdx++) {
const color = _colors[(jdx < 4) ?
(val & 0x0f) : (val >> 4)];
const color =
_colors[jdx < 4 ? val & 0x0f : val >> 4];
for (let idx = 0; idx < 7; idx++) {
this._drawHalfPixel(data, offset, color);
offset += 4;
@ -346,14 +368,14 @@ export class LoresPage2D implements LoresPage {
if (this.vm.monoMode) {
for (let jdx = 0; jdx < 8; jdx++) {
let b = (jdx < 4) ? (val & 0x0f) : (val >> 4);
b |= (b << 4);
b |= (b << 8);
let b = jdx < 4 ? val & 0x0f : val >> 4;
b |= b << 4;
b |= b << 8;
if (col & 0x1) {
b >>= 2;
}
for (let idx = 0; idx < 14; idx++) {
const color = (b & 0x0001) ? whiteCol : blackCol;
const color = b & 0x0001 ? whiteCol : blackCol;
this._drawHalfPixel(data, offset, color);
b >>= 1;
offset += 4;
@ -362,7 +384,8 @@ export class LoresPage2D implements LoresPage {
}
} else {
for (let jdx = 0; jdx < 8; jdx++) {
const color = _colors[(jdx < 4) ? (val & 0x0f) : (val >> 4)];
const color =
_colors[jdx < 4 ? val & 0x0f : val >> 4];
for (let idx = 0; idx < 7; idx++) {
this._drawPixel(data, offset, color);
offset += 8;
@ -376,7 +399,8 @@ export class LoresPage2D implements LoresPage {
}
refresh() {
this.highColorTextMode = !this.vm.an3State && this.vm.textMode && !this.vm._80colMode;
this.highColorTextMode =
!this.vm.an3State && this.vm.textMode && !this.vm._80colMode;
let addr = 0x400 * this.page;
this._refreshing = true;
@ -395,7 +419,7 @@ export class LoresPage2D implements LoresPage {
this._blink = !this._blink;
for (let idx = 0; idx < 0x400; idx++, addr++) {
const b = this._buffer[0][idx];
if ((b & 0xC0) === 0x40) {
if ((b & 0xc0) === 0x40) {
this._write(addr >> 8, addr & 0xff, this._buffer[0][idx], 0);
}
}
@ -424,7 +448,7 @@ export class LoresPage2D implements LoresPage {
buffer: [
new Uint8Array(this._buffer[0]),
new Uint8Array(this._buffer[1]),
]
],
};
}
@ -443,25 +467,29 @@ export class LoresPage2D implements LoresPage {
}
private mapCharCode(charCode: byte) {
charCode &= 0x7F;
charCode &= 0x7f;
if (charCode < 0x20) {
charCode += 0x40;
}
if (!this.e && (charCode >= 0x60)) {
if (!this.e && charCode >= 0x60) {
charCode -= 0x40;
}
return charCode;
}
getText() {
let buffer = '', line, charCode;
let buffer = '',
line,
charCode;
let row, col, base;
for (row = 0; row < 24; row++) {
base = this.rowToBase(row);
line = '';
if (this.e && this.vm._80colMode) {
for (col = 0; col < 80; col++) {
charCode = this.mapCharCode(this._buffer[1 - col % 2][base + Math.floor(col / 2)]);
charCode = this.mapCharCode(
this._buffer[1 - (col % 2)][base + Math.floor(col / 2)]
);
line += String.fromCharCode(charCode);
}
} else {
@ -498,7 +526,7 @@ export class HiresPage2D implements HiresPage {
constructor(
private vm: VideoModes,
private page: pageNo,
private page: pageNo
) {
this.imageData = this.vm.context.createImageData(560, 192);
this.imageData.data.fill(0xff);
@ -509,7 +537,9 @@ export class HiresPage2D implements HiresPage {
}
private _drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = data[off + 4] = c0;
data[off + 1] = data[off + 5] = c1;
@ -517,7 +547,9 @@ export class HiresPage2D implements HiresPage {
}
private _drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = c0;
data[off + 1] = c1;
@ -529,7 +561,9 @@ export class HiresPage2D implements HiresPage {
//
private _draw3Pixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = data[off + 4] = data[off + 8] = c0;
data[off + 1] = data[off + 5] = data[off + 9] = c1;
@ -537,7 +571,9 @@ export class HiresPage2D implements HiresPage {
}
private _draw4Pixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = data[off + 4] = data[off + 8] = data[off + 12] = c0;
data[off + 1] = data[off + 5] = data[off + 9] = data[off + 13] = c1;
@ -562,18 +598,23 @@ export class HiresPage2D implements HiresPage {
};
}
private _start() { return (0x20 * this.page); }
private _start() {
return 0x20 * this.page;
}
private _end() { return (0x020 * this.page) + 0x1f; }
private _end() {
return 0x020 * this.page + 0x1f;
}
private _read(page: byte, off: byte, bank: bank) {
const addr = (page << 8) | off, base = addr & 0x1FFF;
const addr = (page << 8) | off,
base = addr & 0x1fff;
return this._buffer[bank][base];
}
private _write(page: byte, off: byte, val: byte, bank: bank) {
const addr = (page << 8) | off;
const base = addr & 0x1FFF;
const base = addr & 0x1fff;
if (this._buffer[bank][base] === val && !this._refreshing) {
return;
@ -584,7 +625,7 @@ export class HiresPage2D implements HiresPage {
const adj = off - col;
// 000001cd eabab000 -> 000abcde
const ab = (adj & 0x18);
const ab = adj & 0x18;
const cd = (page & 0x03) << 1;
const e = adj >> 7;
@ -593,17 +634,25 @@ export class HiresPage2D implements HiresPage {
const data = this.imageData.data;
let dx, dy;
if ((rowa < 24) && (col < 40) && (this.vm.hiresMode || this._refreshing)) {
let y = rowa << 4 | rowb << 1;
if (y < this.dirty.top) { this.dirty.top = y; }
if (rowa < 24 && col < 40 && (this.vm.hiresMode || this._refreshing)) {
let y = (rowa << 4) | (rowb << 1);
if (y < this.dirty.top) {
this.dirty.top = y;
}
y += 1;
if (y > this.dirty.bottom) { this.dirty.bottom = y; }
if (y > this.dirty.bottom) {
this.dirty.bottom = y;
}
let x = col * 14 - 2;
if (x < this.dirty.left) { this.dirty.left = x; }
if (x < this.dirty.left) {
this.dirty.left = x;
}
x += 18;
if (x > this.dirty.right) { this.dirty.right = x; }
if (x > this.dirty.right) {
this.dirty.right = x;
}
dy = rowa << 4 | rowb << 1;
dy = (rowa << 4) | (rowb << 1);
let bz, b0, b1, b2, b3, b4, c;
if (this.oneSixtyMode && !this.vm.monoMode) {
// 1 byte = two pixels, but 3:4 ratio
@ -626,7 +675,9 @@ export class HiresPage2D implements HiresPage {
// 76543210 76543210 76543210 76543210
// 1111222 2333344 4455556 6667777
const mod = col % 2, mcol = col - mod, baseOff = base - mod;
const mod = col % 2,
mcol = col - mod,
baseOff = base - mod;
bz = this._buffer[0][baseOff - 1];
b0 = this._buffer[1][baseOff];
b1 = this._buffer[0][baseOff];
@ -635,14 +686,14 @@ export class HiresPage2D implements HiresPage {
b4 = this._buffer[1][baseOff + 2];
c = [
0,
((b0 & 0x0f) >> 0), // 0
(b0 & 0x0f) >> 0, // 0
((b0 & 0x70) >> 4) | ((b1 & 0x01) << 3), // 1
((b1 & 0x1e) >> 1), // 2
(b1 & 0x1e) >> 1, // 2
((b1 & 0x60) >> 5) | ((b2 & 0x03) << 2), // 3
((b2 & 0x3c) >> 2), // 4
(b2 & 0x3c) >> 2, // 4
((b2 & 0x40) >> 6) | ((b3 & 0x07) << 1), // 5
((b3 & 0x78) >> 3), // 6
0
(b3 & 0x78) >> 3, // 6
0,
]; // 7
const hb = [
0,
@ -653,7 +704,7 @@ export class HiresPage2D implements HiresPage {
b2 & 0x80, // 4
b3 & 0x80, // 5
b3 & 0x80, // 6
0
0,
]; // 7
if (col > 0) {
c[0] = (bz & 0x78) >> 3;
@ -695,16 +746,17 @@ export class HiresPage2D implements HiresPage {
} else if (this.colorDHRMode) {
this._drawHalfPixel(data, offset, dcolor);
} else if (
((c[idx] !== c[idx - 1]) && (c[idx] !== c[idx + 1])) &&
(((bits & 0x1c) === 0x1c) ||
((bits & 0x70) === 0x70) ||
((bits & 0x38) === 0x38))
c[idx] !== c[idx - 1] &&
c[idx] !== c[idx + 1] &&
((bits & 0x1c) === 0x1c ||
(bits & 0x70) === 0x70 ||
(bits & 0x38) === 0x38)
) {
this._drawHalfPixel(data, offset, whiteCol);
} else if (
(bits & 0x38) ||
(c[idx] === c[idx + 1]) ||
(c[idx] === c[idx - 1])
bits & 0x38 ||
c[idx] === c[idx + 1] ||
c[idx] === c[idx - 1]
) {
this._drawHalfPixel(data, offset, dcolor);
} else if (bits & 0x28) {
@ -733,11 +785,13 @@ export class HiresPage2D implements HiresPage {
b0 = col > 0 ? this._buffer[0][base - 1] : 0;
b2 = col < 39 ? this._buffer[0][base + 1] : 0;
val |= (b2 & 0x3) << 7;
let v0 = b0 & 0x20, v1 = b0 & 0x40, v2 = val & 0x1,
let v0 = b0 & 0x20,
v1 = b0 & 0x40,
v2 = val & 0x1,
odd = !(col & 0x1),
color;
const oddCol = (hbs ? orangeCol : greenCol);
const evenCol = (hbs ? blueCol : violetCol);
const oddCol = hbs ? orangeCol : greenCol;
const evenCol = hbs ? blueCol : violetCol;
let offset = dx * 4 + dy * 280 * 4;
@ -785,7 +839,8 @@ export class HiresPage2D implements HiresPage {
}
refresh() {
this.highColorHGRMode = !this.vm.an3State && this.vm.hiresMode && !this.vm._80colMode;
this.highColorHGRMode =
!this.vm.an3State && this.vm.hiresMode && !this.vm._80colMode;
this.oneSixtyMode = this.vm.flag === 1 && this.vm.doubleHiresMode;
this.mixedDHRMode = this.vm.flag === 2 && this.vm.doubleHiresMode;
this.monoDHRMode = this.vm.flag === 3 && this.vm.doubleHiresMode;
@ -824,7 +879,7 @@ export class HiresPage2D implements HiresPage {
buffer: [
new Uint8Array(this._buffer[0]),
new Uint8Array(this._buffer[1]),
]
],
};
}
@ -884,7 +939,8 @@ export class VideoModes2D implements VideoModes {
}
_refresh() {
this.doubleHiresMode = !this.an3State && this.hiresMode && this._80colMode;
this.doubleHiresMode =
!this.an3State && this.hiresMode && this._80colMode;
this._refreshFlag = true;
}
@ -936,7 +992,9 @@ export class VideoModes2D implements VideoModes {
}
_80col(on: boolean) {
if (!this.e) { return; }
if (!this.e) {
return;
}
const old = this._80colMode;
this._80colMode = on;
@ -947,7 +1005,9 @@ export class VideoModes2D implements VideoModes {
}
altChar(on: boolean) {
if (!this.e) { return; }
if (!this.e) {
return;
}
const old = this.altCharMode;
this.altCharMode = on;
@ -969,13 +1029,16 @@ export class VideoModes2D implements VideoModes {
}
an3(on: boolean) {
if (!this.e) { return; }
if (!this.e) {
return;
}
const old = this.an3State;
this.an3State = on;
if (on) {
this.flag = ((this.flag << 1) | (this._80colMode ? 0x0 : 0x1)) & 0x3;
this.flag =
((this.flag << 1) | (this._80colMode ? 0x0 : 0x1)) & 0x3;
}
if (old !== on) {
@ -1056,8 +1119,14 @@ export class VideoModes2D implements VideoModes {
const imageData = this.buildScreen(mainData, mixData);
this._screenContext.drawImage(
imageData,
0, 0, 560, 192,
this._left, this._top, 560, 384
0,
0,
560,
192,
this._left,
this._top,
560,
384
);
blitted = true;
}
@ -1076,10 +1145,12 @@ export class VideoModes2D implements VideoModes {
}
if (altData) {
blitted = this.updateImage(
altData,
{ top: 0, left: 0, right: 560, bottom: 192 }
);
blitted = this.updateImage(altData, {
top: 0,
left: 0,
right: 560,
bottom: 192,
});
} else if (this.hiresMode && !this.textMode) {
blitted = this.updateImage(
hgr.imageData,
@ -1088,9 +1159,7 @@ export class VideoModes2D implements VideoModes {
this.mixedMode ? gr.dirty : null
);
} else {
blitted = this.updateImage(
gr.imageData, gr.dirty
);
blitted = this.updateImage(gr.imageData, gr.dirty);
}
hgr.dirty = { ...notDirty };
gr.dirty = { ...notDirty };
@ -1109,7 +1178,7 @@ export class VideoModes2D implements VideoModes {
_80colMode: this._80colMode,
altCharMode: this.altCharMode,
an3State: this.an3State,
flag: this.flag
flag: this.flag,
};
}

View File

@ -1,7 +1,11 @@
import type { byte, Card, Restorable } from '../types';
import { debug, toHex } from '../util';
import { rom as readOnlyRom } from '../roms/cards/cffa';
import { create2MGFromBlockDisk, HeaderData, read2MGHeader } from '../formats/2mg';
import {
create2MGFromBlockDisk,
HeaderData,
read2MGHeader,
} from '../formats/2mg';
import { ProDOSVolume } from '../formats/prodos';
import createBlockDisk from '../formats/block';
import {
@ -15,9 +19,9 @@ import {
const rom = new Uint8Array(readOnlyRom);
const COMMANDS = {
ATACRead: 0x20,
ATACWrite: 0x30,
ATAIdentify: 0xEC
ATACRead: 0x20,
ATACWrite: 0x30,
ATAIdentify: 0xec,
};
// CFFA Card Settings
@ -25,68 +29,69 @@ const COMMANDS = {
const SETTINGS = {
Max32MBPartitionsDev0: 0x800,
Max32MBPartitionsDev1: 0x801,
DefaultBootDevice: 0x802,
DefaultBootPartition: 0x803,
Reserved: 0x804, // 4 bytes
WriteProtectBits: 0x808,
MenuSnagMask: 0x809,
MenuSnagKey: 0x80A,
BootTimeDelayTenths: 0x80B,
BusResetSeconds: 0x80C,
CheckDeviceTenths: 0x80D,
ConfigOptionBits: 0x80E,
BlockOffsetDev0: 0x80F, // 3 bytes
BlockOffsetDev1: 0x812, // 3 bytes
Unused: 0x815
DefaultBootDevice: 0x802,
DefaultBootPartition: 0x803,
Reserved: 0x804, // 4 bytes
WriteProtectBits: 0x808,
MenuSnagMask: 0x809,
MenuSnagKey: 0x80a,
BootTimeDelayTenths: 0x80b,
BusResetSeconds: 0x80c,
CheckDeviceTenths: 0x80d,
ConfigOptionBits: 0x80e,
BlockOffsetDev0: 0x80f, // 3 bytes
BlockOffsetDev1: 0x812, // 3 bytes
Unused: 0x815,
};
// CFFA ATA Register Locations
const LOC = {
ATADataHigh: 0x80,
SetCSMask: 0x81,
ClearCSMask: 0x82,
WriteEEPROM: 0x83,
ATADataHigh: 0x80,
SetCSMask: 0x81,
ClearCSMask: 0x82,
WriteEEPROM: 0x83,
NoWriteEEPROM: 0x84,
ATADevCtrl: 0x86,
ATAAltStatus: 0x86,
ATADataLow: 0x88,
AError: 0x89,
ASectorCnt: 0x8a,
ASector: 0x8b,
ATACylinder: 0x8c,
ATACylinderH: 0x8d,
ATAHead: 0x8e,
ATACommand: 0x8f,
ATAStatus: 0x8f
ATADevCtrl: 0x86,
ATAAltStatus: 0x86,
ATADataLow: 0x88,
AError: 0x89,
ASectorCnt: 0x8a,
ASector: 0x8b,
ATACylinder: 0x8c,
ATACylinderH: 0x8d,
ATAHead: 0x8e,
ATACommand: 0x8f,
ATAStatus: 0x8f,
};
// ATA Status Bits
const STATUS = {
BSY: 0x80, // Busy
BSY: 0x80, // Busy
DRDY: 0x40, // Drive ready. 1 when ready
DWF: 0x20, // Drive write fault. 1 when fault
DSC: 0x10, // Disk seek complete. 1 when not seeking
DRQ: 0x08, // Data request. 1 when ready to write
DWF: 0x20, // Drive write fault. 1 when fault
DSC: 0x10, // Disk seek complete. 1 when not seeking
DRQ: 0x08, // Data request. 1 when ready to write
CORR: 0x04, // Correct data. 1 on correctable error
IDX: 0x02, // 1 once per revolution
ERR: 0x01 // Error. 1 on error
IDX: 0x02, // 1 once per revolution
ERR: 0x01, // Error. 1 on error
};
// ATA Identity Block Locations
const IDENTITY = {
SectorCountLow: 58,
SectorCountHigh: 57
SectorCountLow: 58,
SectorCountHigh: 57,
};
export interface CFFAState {
disks: Array<BlockDisk | null>;
}
export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<CFFAState> {
export default class CFFA
implements Card, MassStorage<BlockFormat>, Restorable<CFFAState>
{
// CFFA internal Flags
private _disableSignalling = false;
@ -122,22 +127,22 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
// Disk data
private _partitions: Array<ProDOSVolume|null> = [
private _partitions: Array<ProDOSVolume | null> = [
// Drive 1
null,
// Drive 2
null
null,
];
private _sectors: Uint16Array[][] = [
// Drive 1
[],
// Drive 2
[]
[],
];
private _name: string[] = [];
private _metadata: Array<HeaderData|null> = [];
private _metadata: Array<HeaderData | null> = [];
constructor() {
debug('CFFA');
@ -178,7 +183,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
const statusArray = [];
let flag: keyof typeof STATUS;
for (flag in STATUS) {
if(status & STATUS[flag]) {
if (status & STATUS[flag]) {
statusArray.push(flag);
}
}
@ -199,7 +204,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
const val = this._sectors[this._drive][sector][idx * 16 + jdx];
row.push(toHex(val, 4));
const low = val & 0x7f;
const hi = val >> 8 & 0x7f;
const hi = (val >> 8) & 0x7f;
charRow.push(low > 0x1f ? String.fromCharCode(low) : '.');
charRow.push(hi > 0x1f ? String.fromCharCode(hi) : '.');
}
@ -217,106 +222,119 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
if (readMode) {
retVal = 0;
switch (off & 0x8f) {
case LOC.ATADataHigh: // 0x00
case LOC.ATADataHigh: // 0x00
retVal = this._dataHigh;
break;
case LOC.SetCSMask: // 0x01
case LOC.SetCSMask: // 0x01
this._disableSignalling = true;
break;
case LOC.ClearCSMask: // 0x02
case LOC.ClearCSMask: // 0x02
this._disableSignalling = false;
break;
case LOC.WriteEEPROM: // 0x03
case LOC.WriteEEPROM: // 0x03
this._writeEEPROM = true;
break;
case LOC.NoWriteEEPROM: // 0x04
this._writeEEPROM = false;
break;
case LOC.ATAAltStatus: // 0x06
case LOC.ATAAltStatus: // 0x06
retVal = this._altStatus;
break;
case LOC.ATADataLow: // 0x08
case LOC.ATADataLow: // 0x08
this._dataHigh = this._curSector[this._curWord] >> 8;
retVal = this._curSector[this._curWord] & 0xff;
if (!this._disableSignalling) {
this._curWord++;
}
break;
case LOC.AError: // 0x09
case LOC.AError: // 0x09
retVal = this._error;
break;
case LOC.ASectorCnt: // 0x0A
case LOC.ASectorCnt: // 0x0A
retVal = this._sectorCnt;
break;
case LOC.ASector: // 0x0B
case LOC.ASector: // 0x0B
retVal = this._sector;
break;
case LOC.ATACylinder: // 0x0C
case LOC.ATACylinder: // 0x0C
retVal = this._cylinder;
break;
case LOC.ATACylinderH: // 0x0D
case LOC.ATACylinderH: // 0x0D
retVal = this._cylinderH;
break;
case LOC.ATAHead: // 0x0E
retVal = this._head | (this._lba ? 0x40 : 0) | (this._drive ? 0x10 : 0) | 0xA0;
case LOC.ATAHead: // 0x0E
retVal =
this._head |
(this._lba ? 0x40 : 0) |
(this._drive ? 0x10 : 0) |
0xa0;
break;
case LOC.ATAStatus: // 0x0F
retVal = this._sectors[this._drive].length > 0 ? STATUS.DRDY | STATUS.DSC : 0;
case LOC.ATAStatus: // 0x0F
retVal =
this._sectors[this._drive].length > 0
? STATUS.DRDY | STATUS.DSC
: 0;
this._debug('returning status', this._statusString(retVal));
break;
default:
debug('read unknown soft switch', toHex(off));
}
if (off & 0x7) { // Anything but data high/low
if (off & 0x7) {
// Anything but data high/low
this._debug('read soft switch', toHex(off), toHex(retVal));
}
} else {
if (off & 0x7) { // Anything but data high/low
if (off & 0x7) {
// Anything but data high/low
this._debug('write soft switch', toHex(off), toHex(val));
}
switch (off & 0x8f) {
case LOC.ATADataHigh: // 0x00
case LOC.ATADataHigh: // 0x00
this._dataHigh = val;
break;
case LOC.SetCSMask: // 0x01
case LOC.SetCSMask: // 0x01
this._disableSignalling = true;
break;
case LOC.ClearCSMask: // 0x02
case LOC.ClearCSMask: // 0x02
this._disableSignalling = false;
break;
case LOC.WriteEEPROM: // 0x03
case LOC.WriteEEPROM: // 0x03
this._writeEEPROM = true;
break;
case LOC.NoWriteEEPROM: // 0x04
this._writeEEPROM = false;
break;
case LOC.ATADevCtrl: // 0x06
case LOC.ATADevCtrl: // 0x06
this._debug('devCtrl:', toHex(val));
this._interruptsEnabled = (val & 0x04) ? true : false;
this._debug('Interrupts', this._interruptsEnabled ? 'enabled' : 'disabled');
this._interruptsEnabled = val & 0x04 ? true : false;
this._debug(
'Interrupts',
this._interruptsEnabled ? 'enabled' : 'disabled'
);
if (val & 0x02) {
this._reset();
}
break;
case LOC.ATADataLow: // 0x08
this._curSector[this._curWord] = this._dataHigh << 8 | val;
case LOC.ATADataLow: // 0x08
this._curSector[this._curWord] =
(this._dataHigh << 8) | val;
this._curWord++;
break;
case LOC.ASectorCnt: // 0x0a
case LOC.ASectorCnt: // 0x0a
this._debug('setting sector count', val);
this._sectorCnt = val;
break;
case LOC.ASector: // 0x0b
case LOC.ASector: // 0x0b
this._debug('setting sector', toHex(val));
this._sector = val;
break;
case LOC.ATACylinder: // 0x0c
case LOC.ATACylinder: // 0x0c
this._debug('setting cylinder', toHex(val));
this._cylinder = val;
break;
case LOC.ATACylinderH: // 0x0d
case LOC.ATACylinderH: // 0x0d
this._debug('setting cylinder high', toHex(val));
this._cylinderH = val;
break;
@ -324,14 +342,23 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
this._head = val & 0xf;
this._lba = val & 0x40 ? true : false;
this._drive = val & 0x10 ? 1 : 0;
this._debug('setting head', toHex(val & 0xf), 'drive', this._drive);
this._debug(
'setting head',
toHex(val & 0xf),
'drive',
this._drive
);
if (!this._lba) {
console.error('CHS mode not supported');
}
break;
case LOC.ATACommand: // 0x0f
case LOC.ATACommand: // 0x0f
this._debug('command:', toHex(val));
sector = this._head << 24 | this._cylinderH << 16 | this._cylinder << 8 | this._sector;
sector =
(this._head << 24) |
(this._cylinderH << 16) |
(this._cylinder << 8) |
this._sector;
this._dumpSector(sector);
switch (val) {
@ -341,13 +368,27 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
this._curWord = 0;
break;
case COMMANDS.ATACRead:
this._debug('ATA read sector', toHex(this._cylinderH), toHex(this._cylinder), toHex(this._sector), sector);
this._curSector = this._sectors[this._drive][sector];
this._debug(
'ATA read sector',
toHex(this._cylinderH),
toHex(this._cylinder),
toHex(this._sector),
sector
);
this._curSector =
this._sectors[this._drive][sector];
this._curWord = 0;
break;
case COMMANDS.ATACWrite:
this._debug('ATA write sector', toHex(this._cylinderH), toHex(this._cylinder), toHex(this._sector), sector);
this._curSector = this._sectors[this._drive][sector];
this._debug(
'ATA write sector',
toHex(this._cylinderH),
toHex(this._cylinder),
toHex(this._sector),
sector
);
this._curSector =
this._sectors[this._drive][sector];
this._curWord = 0;
break;
default:
@ -367,49 +408,45 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
}
read(page: byte, off: byte) {
return rom[(page - 0xc0) << 8 | off];
return rom[((page - 0xc0) << 8) | off];
}
write(page: byte, off: byte, val: byte) {
if (this._writeEEPROM) {
this._debug('writing', toHex(page << 8 | off), toHex(val));
rom[(page - 0xc0) << 8 | off] - val;
this._debug('writing', toHex((page << 8) | off), toHex(val));
rom[((page - 0xc0) << 8) | off] - val;
}
}
getState() {
return {
disks: this._partitions.map(
(partition) => {
let result: BlockDisk | null = null;
if (partition) {
const disk: BlockDisk = partition.disk();
result = {
blocks: disk.blocks.map(
(block) => new Uint8Array(block)
),
encoding: ENCODING_BLOCK,
format: disk.format,
readOnly: disk.readOnly,
metadata: { ...disk.metadata },
};
}
return result;
disks: this._partitions.map((partition) => {
let result: BlockDisk | null = null;
if (partition) {
const disk: BlockDisk = partition.disk();
result = {
blocks: disk.blocks.map(
(block) => new Uint8Array(block)
),
encoding: ENCODING_BLOCK,
format: disk.format,
readOnly: disk.readOnly,
metadata: { ...disk.metadata },
};
}
)
return result;
}),
};
}
setState(state: CFFAState) {
state.disks.forEach(
(disk, idx) => {
if (disk) {
this.setBlockVolume(idx + 1, disk);
} else {
this.resetBlockVolume(idx + 1);
}
state.disks.forEach((disk, idx) => {
if (disk) {
this.setBlockVolume(idx + 1, disk);
} else {
this.resetBlockVolume(idx + 1);
}
);
});
}
resetBlockVolume(drive: number) {
@ -433,12 +470,14 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
drive = drive - 1;
// Convert 512 byte blocks into 256 word sectors
this._sectors[drive] = disk.blocks.map(function(block) {
this._sectors[drive] = disk.blocks.map(function (block) {
return new Uint16Array(block.buffer);
});
this._identity[drive][IDENTITY.SectorCountHigh] = this._sectors[0].length & 0xffff;
this._identity[drive][IDENTITY.SectorCountLow] = this._sectors[0].length >> 16;
this._identity[drive][IDENTITY.SectorCountHigh] =
this._sectors[0].length & 0xffff;
this._identity[drive][IDENTITY.SectorCountLow] =
this._sectors[0].length >> 16;
const prodos = new ProDOSVolume(disk);
@ -455,7 +494,12 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
// Assign a raw disk image to a drive. Must be 2mg or raw PO image.
setBinary(drive: number, name: string, ext: BlockFormat, rawData: ArrayBuffer) {
setBinary(
drive: number,
name: string,
ext: BlockFormat,
rawData: ArrayBuffer
) {
const volume = 254;
const readOnly = false;
@ -471,7 +515,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
rawData,
name,
volume,
readOnly
readOnly,
};
const disk = createBlockDisk(ext, options);

View File

@ -1,23 +1,40 @@
import { base64_encode } from '../base64';
import type {
byte,
Card,
ReadonlyUint8Array
} from '../types';
import type { byte, Card, ReadonlyUint8Array } from '../types';
import {
DISK_PROCESSED, DriveNumber, DRIVE_NUMBERS, FloppyDisk,
FloppyFormat, FormatWorkerMessage,
FormatWorkerResponse, isNibbleDisk, isNoFloppyDisk, isWozDisk, JSONDisk, MassStorage,
MassStorageData, NibbleDisk, NibbleFormat, NO_DISK, PROCESS_BINARY, PROCESS_JSON, PROCESS_JSON_DISK, SupportedSectors, WozDisk
DISK_PROCESSED,
DriveNumber,
DRIVE_NUMBERS,
FloppyDisk,
FloppyFormat,
FormatWorkerMessage,
FormatWorkerResponse,
isNibbleDisk,
isNoFloppyDisk,
isWozDisk,
JSONDisk,
MassStorage,
MassStorageData,
NibbleDisk,
NibbleFormat,
NO_DISK,
PROCESS_BINARY,
PROCESS_JSON,
PROCESS_JSON_DISK,
SupportedSectors,
WozDisk,
} from '../formats/types';
import {
createDisk,
createDiskFromJsonDisk
} from '../formats/create_disk';
import { createDisk, createDiskFromJsonDisk } from '../formats/create_disk';
import { jsonDecode, jsonEncode, readSector, _D13O, _DO, _PO } from '../formats/format_utils';
import {
jsonDecode,
jsonEncode,
readSector,
_D13O,
_DO,
_PO,
} from '../formats/format_utils';
import Apple2IO from '../apple2io';
@ -25,30 +42,36 @@ import { BOOTSTRAP_ROM_13, BOOTSTRAP_ROM_16 } from '../roms/cards/disk2';
import { EmptyDriver } from './drivers/EmptyDriver';
import { NibbleDiskDriver } from './drivers/NibbleDiskDriver';
import { ControllerState, DiskDriver, Drive, DriverState, Phase } from './drivers/types';
import {
ControllerState,
DiskDriver,
Drive,
DriverState,
Phase,
} from './drivers/types';
import { WozDiskDriver } from './drivers/WozDiskDriver';
/** Softswitch locations */
const LOC = {
// Disk II Controller Commands
// See Understanding the Apple IIe, Table 9.1
PHASE0OFF: 0x80, // Q0L: Phase 0 OFF
PHASE0ON: 0x81, // Q0H: Phase 0 ON
PHASE1OFF: 0x82, // Q1L: Phase 1 OFF
PHASE1ON: 0x83, // Q1H: Phase 1 ON
PHASE2OFF: 0x84, // Q2L: Phase 2 OFF
PHASE2ON: 0x85, // Q2H: Phase 2 ON
PHASE3OFF: 0x86, // Q3L: Phase 3 OFF
PHASE3ON: 0x87, // Q3H: Phase 3 ON
PHASE0OFF: 0x80, // Q0L: Phase 0 OFF
PHASE0ON: 0x81, // Q0H: Phase 0 ON
PHASE1OFF: 0x82, // Q1L: Phase 1 OFF
PHASE1ON: 0x83, // Q1H: Phase 1 ON
PHASE2OFF: 0x84, // Q2L: Phase 2 OFF
PHASE2ON: 0x85, // Q2H: Phase 2 ON
PHASE3OFF: 0x86, // Q3L: Phase 3 OFF
PHASE3ON: 0x87, // Q3H: Phase 3 ON
DRIVEOFF: 0x88, // Q4L: Drives OFF
DRIVEON: 0x89, // Q4H: Selected drive ON
DRIVE1: 0x8A, // Q5L: Select drive 1
DRIVE2: 0x8B, // Q5H: Select drive 2
DRIVEREAD: 0x8C, // Q6L: Shift while writing; read data
DRIVEWRITE: 0x8D, // Q6H: Load while writing; read write protect
DRIVEREADMODE: 0x8E, // Q7L: Read
DRIVEWRITEMODE: 0x8F // Q7H: Write
DRIVEOFF: 0x88, // Q4L: Drives OFF
DRIVEON: 0x89, // Q4H: Selected drive ON
DRIVE1: 0x8a, // Q5L: Select drive 1
DRIVE2: 0x8b, // Q5H: Select drive 2
DRIVEREAD: 0x8c, // Q6L: Shift while writing; read data
DRIVEWRITE: 0x8d, // Q6H: Load while writing; read write protect
DRIVEREADMODE: 0x8e, // Q7L: Read
DRIVEWRITEMODE: 0x8f, // Q7H: Write
} as const;
/** Logic state sequencer ROM */
@ -62,6 +85,7 @@ const LOC = {
// B LOAD XXXXXXXX YYYYYYYY
// D SL1 ABCDEFGH BCDEFGH1
// prettier-ignore
const SEQUENCER_ROM_13 = [
// See Understanding the Apple IIe, Figure 9.10 The DOS 3.2 Logic State Sequencer
// Note that the column order here is NOT the same as in Figure 9.10 for Q7 H (Write).
@ -88,6 +112,7 @@ const SEQUENCER_ROM_13 = [
0xDD, 0x4D, 0xE0, 0xE0, 0x0A, 0x0A, 0x0A, 0x0A, 0x88, 0x88, 0x08, 0x08, 0x88, 0x88, 0x08, 0x08 // F
] as const;
// prettier-ignore
const SEQUENCER_ROM_16 = [
// See Understanding the Apple IIe, Figure 9.11 The DOS 3.3 Logic State Sequencer
// Note that the column order here is NOT the same as in Figure 9.11 for Q7 H (Write).
@ -152,7 +177,7 @@ const PHASE_DELTA = [
[0, 1, 2, -1],
[-1, 0, 1, 2],
[-2, -1, 0, 1],
[1, -2, -1, 0]
[1, -2, -1, 0],
] as const;
/** Callbacks triggered by events of the drive or controller. */
@ -211,7 +236,8 @@ function getDiskState(disk: FloppyDisk): FloppyDisk {
}
if (isWozDisk(disk)) {
const { format, encoding, metadata, readOnly, trackMap, rawTracks } = disk;
const { format, encoding, metadata, readOnly, trackMap, rawTracks } =
disk;
const result: WozDisk = {
format,
encoding,
@ -235,22 +261,23 @@ function getDiskState(disk: FloppyDisk): FloppyDisk {
* Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller.
*/
export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
private drives: Record<DriveNumber, Drive> = {
1: { // Drive 1
1: {
// Drive 1
track: 0,
head: 0,
phase: 0,
readOnly: false,
dirty: false,
},
2: { // Drive 2
2: {
// Drive 2
track: 0,
head: 0,
phase: 0,
readOnly: false,
dirty: false,
}
},
};
private disks: Record<DriveNumber, FloppyDisk> = {
@ -263,7 +290,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
encoding: NO_DISK,
readOnly: false,
metadata: { name: 'Disk 2' },
}
},
};
private driver: Record<DriveNumber, DiskDriver> = {
@ -283,7 +310,11 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
private worker: Worker;
/** Builds a new Disk ][ card. */
constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors: SupportedSectors = 16) {
constructor(
private io: Apple2IO,
private callbacks: Callbacks,
private sectors: SupportedSectors = 16
) {
this.debug('Disk ][');
this.state = {
@ -382,7 +413,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
case LOC.PHASE3OFF: // 0x06
this.setPhase(3, false);
break;
case LOC.PHASE3ON: // 0x07
case LOC.PHASE3ON: // 0x07
this.setPhase(3, true);
break;
@ -414,7 +445,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
}
break;
case LOC.DRIVE1: // 0x0a
case LOC.DRIVE1: // 0x0a
this.debug('Disk 1');
state.driveNo = 1;
this.updateActiveDrive();
@ -423,7 +454,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
this.callbacks.driveLight(1, true);
}
break;
case LOC.DRIVE2: // 0x0b
case LOC.DRIVE2: // 0x0b
this.debug('Disk 2');
state.driveNo = 2;
this.updateActiveDrive();
@ -443,7 +474,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
this.curDriver.onQ6High(readMode);
break;
case LOC.DRIVEREADMODE: // 0x0e (Q7L)
case LOC.DRIVEREADMODE: // 0x0e (Q7L)
this.debug('Read Mode');
state.q7 = false;
break;
@ -551,7 +582,6 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
this.driver[driveNo].setState(state.driver);
}
setState(state: State) {
this.state = { ...state.controllerState };
for (const d of DRIVE_NUMBERS) {
@ -580,7 +610,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
rwts(driveNo: DriveNumber, track: byte, sector: byte) {
const curDisk = this.disks[driveNo];
if (!isNibbleDisk(curDisk)) {
throw new Error('Can\'t read WOZ disks');
throw new Error("Can't read WOZ disks");
}
return readSector(curDisk, track, sector);
}
@ -593,7 +623,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
type: PROCESS_JSON_DISK,
payload: {
driveNo: driveNo,
jsonDisk
jsonDisk,
},
};
this.worker.postMessage(message);
@ -611,7 +641,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
getJSON(driveNo: DriveNumber, pretty: boolean = false) {
const curDisk = this.disks[driveNo];
if (!isNibbleDisk(curDisk)) {
throw new Error('Can\'t save WOZ disks to JSON');
throw new Error("Can't save WOZ disks to JSON");
}
return jsonEncode(curDisk, pretty);
}
@ -622,7 +652,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
type: PROCESS_JSON,
payload: {
driveNo: driveNo,
json
json,
},
};
this.worker.postMessage(message);
@ -633,7 +663,12 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
return true;
}
setBinary(driveNo: DriveNumber, name: string, fmt: FloppyFormat, rawData: ArrayBuffer) {
setBinary(
driveNo: DriveNumber,
name: string,
fmt: FloppyFormat,
rawData: ArrayBuffer
) {
const readOnly = false;
const volume = 254;
const options = {
@ -650,7 +685,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
driveNo: driveNo,
fmt,
options,
}
},
};
this.worker.postMessage(message);
@ -673,19 +708,22 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
try {
this.worker = new Worker('dist/format_worker.bundle.js');
this.worker.addEventListener('message', (message: MessageEvent<FormatWorkerResponse>) => {
const { data } = message;
switch (data.type) {
case DISK_PROCESSED:
{
const { driveNo: drive, disk } = data.payload;
if (disk) {
this.insertDisk(drive, disk);
this.worker.addEventListener(
'message',
(message: MessageEvent<FormatWorkerResponse>) => {
const { data } = message;
switch (data.type) {
case DISK_PROCESSED:
{
const { driveNo: drive, disk } = data.payload;
if (disk) {
this.insertDisk(drive, disk);
}
}
}
break;
break;
}
}
});
);
} catch (e: unknown) {
console.error(e);
}
@ -696,22 +734,22 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
if (isNoFloppyDisk(disk)) {
this.driver[driveNo] = new EmptyDriver(this.drives[driveNo]);
} else if (isNibbleDisk(disk)) {
this.driver[driveNo] =
new NibbleDiskDriver(
driveNo,
this.drives[driveNo],
disk,
this.state,
() => this.updateDirty(driveNo, true));
this.driver[driveNo] = new NibbleDiskDriver(
driveNo,
this.drives[driveNo],
disk,
this.state,
() => this.updateDirty(driveNo, true)
);
} else if (isWozDisk(disk)) {
this.driver[driveNo] =
new WozDiskDriver(
driveNo,
this.drives[driveNo],
disk,
this.state,
() => this.updateDirty(driveNo, true),
this.io);
this.driver[driveNo] = new WozDiskDriver(
driveNo,
this.drives[driveNo],
disk,
this.state,
() => this.updateDirty(driveNo, true),
this.io
);
} else {
throw new Error(`Unknown disk format ${disk.encoding}`);
}
@ -736,16 +774,20 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
* an error will be thrown. Using `ext == 'nib'` will always return
* an image.
*/
getBinary(driveNo: DriveNumber, ext?: Exclude<NibbleFormat, 'woz'>): MassStorageData | null {
getBinary(
driveNo: DriveNumber,
ext?: Exclude<NibbleFormat, 'woz'>
): MassStorageData | null {
const curDisk = this.disks[driveNo];
if (!isNibbleDisk(curDisk)) {
return null;
}
const { format, readOnly, tracks, volume } = curDisk;
const { name } = curDisk.metadata;
const len = format === 'nib' ?
tracks.reduce((acc, track) => acc + track.length, 0) :
this.sectors * tracks.length * 256;
const len =
format === 'nib'
? tracks.reduce((acc, track) => acc + track.length, 0)
: this.sectors * tracks.length * 256;
const data = new Uint8Array(len);
ext = ext ?? format;
@ -767,7 +809,11 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
for (let s = 0; s < maxSector; s++) {
const _s = sectorMap[s];
const sector = readSector({ ...curDisk, format: ext }, t, _s);
const sector = readSector(
{ ...curDisk, format: ext },
t,
_s
);
data.set(sector, idx);
idx += sector.length;
}

View File

@ -1,7 +1,6 @@
import { DriveNumber, NibbleDisk, WozDisk } from '../../formats/types';
import { ControllerState, DiskDriver, Drive, DriverState } from './types';
/**
* Common logic for both `NibbleDiskDriver` and `WozDiskDriver`.
*/
@ -10,7 +9,8 @@ export abstract class BaseDiskDriver implements DiskDriver {
protected readonly driveNo: DriveNumber,
protected readonly drive: Drive,
protected readonly disk: NibbleDisk | WozDisk,
protected readonly controller: ControllerState) { }
protected readonly controller: ControllerState
) {}
/** Called frequently to ensure the disk is spinning. */
abstract tick(): void;

View File

@ -1,14 +1,14 @@
import { DiskDriver, Drive, DriverState } from './types';
/** Returned state for an empty drive. */
export interface EmptyDriverState extends DriverState { }
export interface EmptyDriverState extends DriverState {}
/**
* Driver for empty drives. This implementation does nothing except keep
* the head clamped between tracks 0 and 34.
*/
export class EmptyDriver implements DiskDriver {
constructor(private readonly drive: Drive) { }
constructor(private readonly drive: Drive) {}
tick(): void {
// do nothing

View File

@ -22,7 +22,8 @@ export class NibbleDiskDriver extends BaseDiskDriver {
drive: Drive,
readonly disk: NibbleDisk,
controller: ControllerState,
private readonly onDirty: () => void) {
private readonly onDirty: () => void
) {
super(driveNo, drive, disk, controller);
}
@ -57,7 +58,7 @@ export class NibbleDiskDriver extends BaseDiskDriver {
} else {
this.controller.latch = 0;
}
this.skip = (++this.skip % 2);
this.skip = ++this.skip % 2;
}
onQ6High(readMode: boolean): void {

View File

@ -3,7 +3,13 @@ import { DriveNumber, WozDisk } from '../../formats/types';
import { toHex } from '../../util';
import { SEQUENCER_ROM } from '../disk2';
import { BaseDiskDriver } from './BaseDiskDriver';
import { ControllerState, Drive, DriverState, LssClockCycle, LssState } from './types';
import {
ControllerState,
Drive,
DriverState,
LssClockCycle,
LssState,
} from './types';
interface WozDiskDriverState extends DriverState {
clock: LssClockCycle;
@ -32,7 +38,8 @@ export class WozDiskDriver extends BaseDiskDriver {
readonly disk: WozDisk,
controller: ControllerState,
private readonly onDirty: () => void,
private readonly io: Apple2IO) {
private readonly io: Apple2IO
) {
super(driveNo, drive, disk, controller);
// From the example in UtA2e, p. 9-29, col. 1, para. 1., this is
@ -129,7 +136,13 @@ export class WozDiskDriver extends BaseDiskDriver {
const command = SEQUENCER_ROM[controller.sectors][idx];
this.debug(`clock: ${this.clock} state: ${toHex(this.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${controller.q6} latch: ${toHex(controller.latch)}`);
this.debug(
`clock: ${this.clock} state: ${toHex(
this.state
)} pulse: ${pulse} command: ${toHex(command)} q6: ${
controller.q6
} latch: ${toHex(controller.latch)}`
);
switch (command & 0xf) {
case 0x0: // CLR
@ -140,23 +153,23 @@ export class WozDiskDriver extends BaseDiskDriver {
case 0x9: // SL0
controller.latch = (controller.latch << 1) & 0xff;
break;
case 0xA: // SR
case 0xa: // SR
controller.latch >>= 1;
if (this.isWriteProtected()) {
controller.latch |= 0x80;
}
break;
case 0xB: // LD
case 0xb: // LD
controller.latch = controller.bus;
this.debug('Loading', toHex(controller.latch), 'from bus');
break;
case 0xD: // SL1
case 0xd: // SL1
controller.latch = ((controller.latch << 1) | 0x01) & 0xff;
break;
default:
this.debug(`unknown command: ${toHex(command & 0xf)}`);
}
this.state = (command >> 4 & 0xF) as LssState;
this.state = ((command >> 4) & 0xf) as LssState;
if (this.clock === 4) {
if (this.isOn()) {

View File

@ -13,7 +13,9 @@ export interface LanguageCardState {
prewrite: boolean;
}
export default class LanguageCard implements Card, Restorable<LanguageCardState> {
export default class LanguageCard
implements Card, Restorable<LanguageCardState>
{
private bank1: RAM;
private bank2: RAM;
private ram: RAM;
@ -88,7 +90,8 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
let bankStr;
let rwStr;
if (writeSwitch) { // 0xC081, 0xC083
if (writeSwitch) {
// 0xC081, 0xC083
if (readMode) {
if (this._prewrite) {
this._writebsr = true;
@ -96,30 +99,37 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
}
this._prewrite = readMode;
if (offSwitch) { // $C083, $C08B
if (offSwitch) {
// $C083, $C08B
this._readbsr = true;
rwStr = 'Read/Write';
} else { // $C081, $C089
} else {
// $C081, $C089
this._readbsr = false;
rwStr = 'Write';
}
} else { // $C080, $C082, $C088, $C08A
} else {
// $C080, $C082, $C088, $C08A
this._writebsr = false;
this._prewrite = false;
if (offSwitch) { // $C082, $C08A
if (offSwitch) {
// $C082, $C08A
this._readbsr = false;
rwStr = 'Off';
} else { // $C080, $C088
} else {
// $C080, $C088
this._readbsr = true;
rwStr = 'Read';
}
}
if (bank1Switch) { // C08[8-C]
if (bank1Switch) {
// C08[8-C]
this._bsr2 = false;
bankStr = 'Bank 1';
} else { // C08[0-3]
} else {
// C08[0-3]
this._bsr2 = true;
bankStr = 'Bank 2';
}
@ -180,7 +190,7 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
prewrite: this._prewrite,
ram: this.ram.getState(),
bank1: this.bank1.getState(),
bank2: this.bank2.getState()
bank2: this.bank2.getState(),
};
}

View File

@ -4,16 +4,16 @@ import { debug } from '../util';
import { rom } from '../roms/cards/mouse';
const CLAMP_MIN_LOW = 0x478;
const CLAMP_MAX_LOW = 0x4F8;
const CLAMP_MAX_LOW = 0x4f8;
const CLAMP_MIN_HIGH = 0x578;
const CLAMP_MAX_HIGH = 0x5F8;
const CLAMP_MAX_HIGH = 0x5f8;
const X_LOW = 0x478;
const Y_LOW = 0x4F8;
const Y_LOW = 0x4f8;
const X_HIGH = 0x578;
const Y_HIGH = 0x5F8;
const Y_HIGH = 0x5f8;
const STATUS = 0x778;
const MODE = 0x7F8;
const MODE = 0x7f8;
const STATUS_DOWN = 0x80;
const STATUS_LAST = 0x40;
@ -38,7 +38,7 @@ const ENTRIES = {
POS_MOUSE: 0x16,
CLAMP_MOUSE: 0x17,
HOME_MOUSE: 0x18,
INIT_MOUSE: 0x19
INIT_MOUSE: 0x19,
};
interface MouseState {
@ -65,9 +65,9 @@ export default class Mouse implements Card, Restorable<MouseState> {
/** Lowest mouse Y */
private clampYMin: word = 0;
/** Highest mouse X */
private clampXMax: word = 0x3FF;
private clampXMax: word = 0x3ff;
/** Highest mouse Y */
private clampYMax: word = 0x3FF;
private clampYMax: word = 0x3ff;
/** Mouse X position */
private x: word = 0;
/** Mouse Y position */
@ -117,7 +117,7 @@ export default class Mouse implements Card, Restorable<MouseState> {
};
const clearCarry = (state: CpuState) => {
state.s &= 0xFE;
state.s &= 0xfe;
return state;
};
@ -145,7 +145,8 @@ export default class Mouse implements Card, Restorable<MouseState> {
break;
case rom[ENTRIES.READ_MOUSE]:
{
const moved = (this.lastX !== this.x) || (this.lastY !== this.y);
const moved =
this.lastX !== this.x || this.lastY !== this.y;
const status =
(this.down ? STATUS_DOWN : 0) |
(this.lastDown ? STATUS_LAST : 0) |
@ -183,13 +184,29 @@ export default class Mouse implements Card, Restorable<MouseState> {
{
const clampY = state.a;
if (clampY) {
this.clampYMin = holeRead(CLAMP_MIN_LOW) | (holeRead(CLAMP_MIN_HIGH) << 8);
this.clampYMax = holeRead(CLAMP_MAX_LOW) | (holeRead(CLAMP_MAX_HIGH) << 8);
debug('clampMouse Y', this.clampYMin, this.clampYMax);
this.clampYMin =
holeRead(CLAMP_MIN_LOW) |
(holeRead(CLAMP_MIN_HIGH) << 8);
this.clampYMax =
holeRead(CLAMP_MAX_LOW) |
(holeRead(CLAMP_MAX_HIGH) << 8);
debug(
'clampMouse Y',
this.clampYMin,
this.clampYMax
);
} else {
this.clampXMin = holeRead(CLAMP_MIN_LOW) | (holeRead(CLAMP_MIN_HIGH) << 8);
this.clampXMax = holeRead(CLAMP_MAX_LOW) | (holeRead(CLAMP_MAX_HIGH) << 8);
debug('clampMouse X', this.clampXMin, this.clampXMax);
this.clampXMin =
holeRead(CLAMP_MIN_LOW) |
(holeRead(CLAMP_MIN_HIGH) << 8);
this.clampXMax =
holeRead(CLAMP_MAX_LOW) |
(holeRead(CLAMP_MAX_HIGH) << 8);
debug(
'clampMouse X',
this.clampXMin,
this.clampXMax
);
}
state = clearCarry(state);
}
@ -229,10 +246,10 @@ export default class Mouse implements Card, Restorable<MouseState> {
if (this.mode & MODE_INT_VBL) {
this.serve |= INT_SCREEN;
}
if ((this.mode & MODE_INT_PRESS) && this.shouldIntPress) {
if (this.mode & MODE_INT_PRESS && this.shouldIntPress) {
this.serve |= INT_PRESS;
}
if ((this.mode & MODE_INT_MOVE) && this.shouldIntMove) {
if (this.mode & MODE_INT_MOVE && this.shouldIntMove) {
this.serve |= INT_MOVE;
}
if (this.serve) {
@ -255,8 +272,8 @@ export default class Mouse implements Card, Restorable<MouseState> {
setMouseXY(x: number, y: number, w: number, h: number) {
const rangeX = this.clampXMax - this.clampXMin;
const rangeY = this.clampYMax - this.clampYMin;
this.x = (x * rangeX / w + this.clampXMin) & 0xffff;
this.y = (y * rangeY / h + this.clampYMin) & 0xffff;
this.x = ((x * rangeX) / w + this.clampXMin) & 0xffff;
this.y = ((y * rangeY) / h + this.clampYMin) & 0xffff;
this.shouldIntMove = true;
}
@ -318,7 +335,7 @@ export default class Mouse implements Card, Restorable<MouseState> {
serve: this.serve,
shouldIntMove: this.shouldIntMove,
shouldIntPress: this.shouldIntPress,
slot: this.slot
slot: this.slot,
};
}
}

View File

@ -2,9 +2,7 @@ import ROM from 'js/roms/rom';
import { bit, byte } from 'js/types';
import { debug } from '../util';
const PATTERN = [
0xC5, 0x3A, 0xA3, 0x5C, 0xC5, 0x3A, 0xA3, 0x5C
];
const PATTERN = [0xc5, 0x3a, 0xa3, 0x5c, 0xc5, 0x3a, 0xa3, 0x5c];
const A0 = 0x01;
const A2 = 0x04;
@ -18,7 +16,6 @@ export default class NoSlotClock {
debug('NoSlotClock');
}
private patternMatch() {
for (let idx = 0; idx < 8; idx++) {
let byte = 0;
@ -53,7 +50,7 @@ export default class NoSlotClock {
const hour = now.getHours();
const minutes = now.getMinutes();
const seconds = now.getSeconds();
const hundredths = (now.getMilliseconds() / 10);
const hundredths = now.getMilliseconds() / 10;
this.bits = [];
@ -113,4 +110,3 @@ export default class NoSlotClock {
// Setting the state makes no sense.
}
}

View File

@ -3,7 +3,7 @@ import { Card, Restorable, byte } from '../types';
import { rom } from '../roms/cards/parallel';
const LOC = {
IOREG: 0x80
IOREG: 0x80,
} as const;
export interface ParallelState {}

View File

@ -12,7 +12,7 @@ const LOC = {
_RAMMID: 0x85,
_RAMHI: 0x86,
_RAMDATA: 0x87,
BANK: 0x8F
BANK: 0x8f,
} as const;
export class RAMFactorState {
@ -41,21 +41,21 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
}
private sethi(val: byte) {
this.ramhi = (val & 0xff);
this.ramhi = val & 0xff;
}
private setmid(val: byte) {
if (((this.rammid & 0x80) !== 0) && ((val & 0x80) === 0)) {
if ((this.rammid & 0x80) !== 0 && (val & 0x80) === 0) {
this.sethi(this.ramhi + 1);
}
this.rammid = (val & 0xff);
this.rammid = val & 0xff;
}
private setlo(val: byte) {
if (((this.ramlo & 0x80) !== 0) && ((val & 0x80) === 0)) {
if ((this.ramlo & 0x80) !== 0 && (val & 0x80) === 0) {
this.setmid(this.rammid + 1);
}
this.ramlo = (val & 0xff);
this.ramlo = val & 0xff;
}
private access(off: byte, val: byte) {
@ -105,7 +105,7 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
default:
break;
}
this.loc = (this.ramhi << 16) | (this.rammid << 8) | (this.ramlo);
this.loc = (this.ramhi << 16) | (this.rammid << 8) | this.ramlo;
/*
if (val === undefined) {
@ -123,7 +123,7 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
}
read(page: byte, off: byte) {
return rom[this.firmware << 12 | (page - 0xC0) << 8 | off];
return rom[(this.firmware << 12) | ((page - 0xc0) << 8) | off];
}
write() {
@ -138,7 +138,7 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
return {
loc: this.loc,
firmware: this.firmware,
mem: new Uint8Array(this.mem)
mem: new Uint8Array(this.mem),
};
}
@ -149,6 +149,6 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
this.ramhi = (this.loc >> 16) & 0xff;
this.rammid = (this.loc >> 8) & 0xff;
this.ramlo = (this.loc) & 0xff;
this.ramlo = this.loc & 0xff;
}
}

View File

@ -1,9 +1,20 @@
import { debug, toHex } from '../util';
import { rom as smartPortRom } from '../roms/cards/smartport';
import { Card, Restorable, byte, word, rom } from '../types';
import { MassStorage, BlockDisk, ENCODING_BLOCK, BlockFormat, MassStorageData, DiskFormat } from '../formats/types';
import {
MassStorage,
BlockDisk,
ENCODING_BLOCK,
BlockFormat,
MassStorageData,
DiskFormat,
} from '../formats/types';
import { CPU6502, CpuState, flags } from '@whscullin/cpu6502';
import { create2MGFromBlockDisk, HeaderData, read2MGHeader } from '../formats/2mg';
import {
create2MGFromBlockDisk,
HeaderData,
read2MGHeader,
} from '../formats/2mg';
import createBlockDisk from '../formats/block';
import { DriveNumber } from '../formats/types';
@ -27,7 +38,11 @@ class Address {
lo: byte;
hi: byte;
constructor(private cpu: CPU6502, a: byte | word, b?: byte) {
constructor(
private cpu: CPU6502,
a: byte,
b?: byte
) {
if (b === undefined) {
this.lo = a & 0xff;
this.hi = a >> 8;
@ -46,7 +61,10 @@ class Address {
}
inc(val: byte) {
return new Address(this.cpu, ((this.hi << 8 | this.lo) + val) & 0xffff);
return new Address(
this.cpu,
(((this.hi << 8) | this.lo) + val) & 0xffff
);
}
readByte() {
@ -57,7 +75,7 @@ class Address {
const readLo = this.readByte();
const readHi = this.inc(1).readByte();
return readHi << 8 | readLo;
return (readHi << 8) | readLo;
}
readAddress() {
@ -97,8 +115,8 @@ const BLOCK_LO = 0x46;
// const IO_ERROR = 0x27;
const NO_DEVICE_CONNECTED = 0x28;
const WRITE_PROTECTED = 0x2B;
const DEVICE_OFFLINE = 0x2F;
const WRITE_PROTECTED = 0x2b;
const DEVICE_OFFLINE = 0x2f;
// const VOLUME_DIRECTORY_NOT_FOUND = 0x45;
// const NOT_A_PRODOS_DISK = 0x52;
// const VOLUME_CONTROL_BLOCK_FULL = 0x55;
@ -123,8 +141,9 @@ const DEVICE_TYPE_SCSI_HD = 0x07;
// $0D: Printer
// $0E: Clock
// $0F: Modem
export default class SmartPort implements Card, MassStorage<BlockFormat>, Restorable<SmartPortState> {
export default class SmartPort
implements Card, MassStorage<BlockFormat>, Restorable<SmartPortState>
{
private rom: rom;
private disks: BlockDisk[] = [];
private busy: boolean[] = [];
@ -139,7 +158,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
) {
if (options?.block) {
const dumbPortRom = new Uint8Array(smartPortRom);
dumbPortRom[0x07] = 0x3C;
dumbPortRom[0x07] = 0x3c;
this.rom = dumbPortRom;
debug('DumbPort card');
} else {
@ -221,7 +240,12 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
* readBlock
*/
readBlock(state: CpuState, driveNo: DriveNumber, block: number, buffer: Address) {
readBlock(
state: CpuState,
driveNo: DriveNumber,
block: number,
buffer: Address
) {
this.debug(`read drive=${driveNo}`);
this.debug(`read buffer=${buffer.toString()}`);
this.debug(`read block=$${toHex(block)}`);
@ -249,7 +273,12 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
* writeBlock
*/
writeBlock(state: CpuState, driveNo: DriveNumber, block: number, buffer: Address) {
writeBlock(
state: CpuState,
driveNo: DriveNumber,
block: number,
buffer: Address
) {
this.debug(`write drive=${driveNo}`);
this.debug(`write buffer=${buffer.toString()}`);
this.debug(`write block=$${toHex(block)}`);
@ -347,13 +376,14 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
const blockOff = this.rom[0xff];
const smartOff = blockOff + 3;
if (off === blockOff && this.cpu.getSync()) { // Regular block device entry POINT
if (off === blockOff && this.cpu.getSync()) {
// Regular block device entry POINT
this.debug('block device entry');
cmd = this.cpu.read(0x00, COMMAND);
unit = this.cpu.read(0x00, UNIT);
const bufferAddr = new Address(this.cpu, ADDRESS_LO);
const blockAddr = new Address(this.cpu, BLOCK_LO);
const drive = (unit & 0x80) ? 2 : 1;
const drive = unit & 0x80 ? 2 : 1;
const driveSlot = (unit & 0x70) >> 4;
buffer = bufferAddr.readAddress();
@ -435,27 +465,42 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
default: // Unit 1
switch (status) {
case 0:
blocks = this.disks[unit]?.blocks.length ?? 0;
blocks =
this.disks[unit]?.blocks.length ?? 0;
buffer.writeByte(0xf0); // W/R Block device in drive
buffer.inc(1).writeByte(blocks & 0xff); // 1600 blocks
buffer.inc(2).writeByte((blocks & 0xff00) >> 8);
buffer.inc(3).writeByte((blocks & 0xff0000) >> 16);
buffer
.inc(2)
.writeByte((blocks & 0xff00) >> 8);
buffer
.inc(3)
.writeByte((blocks & 0xff0000) >> 16);
state.x = 4;
state.y = 0;
state.a = 0;
state.s &= ~flags.C;
break;
case 3:
blocks = this.disks[unit]?.blocks.length ?? 0;
blocks =
this.disks[unit]?.blocks.length ?? 0;
buffer.writeByte(0xf0); // W/R Block device in drive
buffer.inc(1).writeByte(blocks & 0xff); // Blocks low byte
buffer.inc(2).writeByte((blocks & 0xff00) >> 8); // Blocks middle byte
buffer.inc(3).writeByte((blocks & 0xff0000) >> 16); // Blocks high byte
buffer
.inc(2)
.writeByte((blocks & 0xff00) >> 8); // Blocks middle byte
buffer
.inc(3)
.writeByte((blocks & 0xff0000) >> 16); // Blocks high byte
buffer.inc(4).writeByte(ID.length); // Vendor ID length
for (let idx = 0; idx < ID.length; idx++) { // Vendor ID
buffer.inc(5 + idx).writeByte(ID.charCodeAt(idx));
for (let idx = 0; idx < ID.length; idx++) {
// Vendor ID
buffer
.inc(5 + idx)
.writeByte(ID.charCodeAt(idx));
}
buffer.inc(21).writeByte(DEVICE_TYPE_SCSI_HD); // Device Type
buffer
.inc(21)
.writeByte(DEVICE_TYPE_SCSI_HD); // Device Type
buffer.inc(22).writeByte(0x0); // Device Subtype
buffer.inc(23).writeWord(0x0101); // Version
state.x = 24;
@ -515,41 +560,38 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
getState() {
return {
disks: this.disks.map(
(disk) => {
const result: BlockDisk = {
blocks: disk.blocks.map(
(block) => new Uint8Array(block)
),
encoding: ENCODING_BLOCK,
format: disk.format,
readOnly: disk.readOnly,
metadata: { ...disk.metadata },
};
return result;
}
)
};
}
setState(state: SmartPortState) {
this.disks = state.disks.map(
(disk) => {
disks: this.disks.map((disk) => {
const result: BlockDisk = {
blocks: disk.blocks.map(
(block) => new Uint8Array(block)
),
blocks: disk.blocks.map((block) => new Uint8Array(block)),
encoding: ENCODING_BLOCK,
format: disk.format,
readOnly: disk.readOnly,
metadata: { ...disk.metadata },
};
return result;
}
);
}),
};
}
setBinary(driveNo: DriveNumber, name: string, fmt: BlockFormat, rawData: ArrayBuffer) {
setState(state: SmartPortState) {
this.disks = state.disks.map((disk) => {
const result: BlockDisk = {
blocks: disk.blocks.map((block) => new Uint8Array(block)),
encoding: ENCODING_BLOCK,
format: disk.format,
readOnly: disk.readOnly,
metadata: { ...disk.metadata },
};
return result;
});
}
setBinary(
driveNo: DriveNumber,
name: string,
fmt: BlockFormat,
rawData: ArrayBuffer
) {
let volume = 254;
let readOnly = false;
if (fmt === '2mg') {

View File

@ -4,25 +4,26 @@ import { rom } from '../roms/cards/thunderclock';
const LOC = {
CONTROL: 0x80,
AUX: 0x88
AUX: 0x88,
} as const;
const COMMANDS = {
MASK: 0x18,
REGHOLD: 0x00,
REGSHIFT: 0x08,
TIMED: 0x18
TIMED: 0x18,
} as const;
const FLAGS = {
DATA: 0x01,
CLOCK: 0x02,
STROBE: 0x04
STROBE: 0x04,
} as const;
export interface ThunderclockState {}
export default class Thunderclock implements Card, Restorable<ThunderclockState>
export default class Thunderclock
implements Card, Restorable<ThunderclockState>
{
constructor() {
debug('Thunderclock');
@ -82,7 +83,7 @@ export default class Thunderclock implements Card, Restorable<ThunderclockState>
}
private access(off: byte, val?: byte) {
switch (off & 0x8F) {
switch (off & 0x8f) {
case LOC.CONTROL:
if (val !== undefined) {
const strobe = val & FLAGS.STROBE ? true : false;
@ -105,7 +106,10 @@ export default class Thunderclock implements Card, Restorable<ThunderclockState>
this.shiftMode = false;
break;
default:
this.debug('Unknown command', toHex(this.command));
this.debug(
'Unknown command',
toHex(this.command)
);
}
}
}
@ -132,7 +136,7 @@ export default class Thunderclock implements Card, Restorable<ThunderclockState>
if (page < 0xc8) {
result = rom[off];
} else {
result = rom[(page - 0xc8) << 8 | off];
result = rom[((page - 0xc8) << 8) | off];
}
return result;
}

View File

@ -13,25 +13,25 @@ interface VideotermState {
const LOC = {
IOREG: 0x80,
IOVAL: 0x81
IOVAL: 0x81,
} as const;
const REGS = {
CURSOR_UPPER: 0x0A,
CURSOR_LOWER: 0x0B,
STARTPOS_HI: 0x0C,
STARTPOS_LO: 0x0D,
CURSOR_HI: 0x0E,
CURSOR_LO: 0x0F,
CURSOR_UPPER: 0x0a,
CURSOR_LOWER: 0x0b,
STARTPOS_HI: 0x0c,
STARTPOS_LO: 0x0d,
CURSOR_HI: 0x0e,
CURSOR_LO: 0x0f,
LIGHTPEN_HI: 0x10,
LIGHTPEN_LO: 0x11
LIGHTPEN_LO: 0x11,
} as const;
const CURSOR_MODES = {
SOLID: 0x00,
HIDDEN: 0x01,
BLINK: 0x10,
FAST_BLINK: 0x11
FAST_BLINK: 0x11,
} as const;
const BLACK: Color = [0x00, 0x00, 0x00];
@ -56,7 +56,7 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
0x00, // 0E - Cursor Hi
0x00, // 0F - Cursor Lo
0x00, // 10 - Lightpen Hi
0x00 // 11 - Lightpen Lo
0x00, // 11 - Lightpen Lo
];
private blink = false;
@ -123,7 +123,8 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
}
private refreshCursor(fromRegs: boolean) {
const addr = this.regs[REGS.CURSOR_HI] << 8 | this.regs[REGS.CURSOR_LO];
const addr =
(this.regs[REGS.CURSOR_HI] << 8) | this.regs[REGS.CURSOR_LO];
const saddr = (0x800 + addr - this.startPos) & 0x7ff;
const data = this.imageData.data;
const row = (saddr / 80) & 0xff;
@ -144,16 +145,20 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
if (blinkmode === CURSOR_MODES.HIDDEN) {
return;
}
if (this.blink || (blinkmode === CURSOR_MODES.SOLID)) {
if (this.blink || blinkmode === CURSOR_MODES.SOLID) {
this.dirty = true;
for (let idx = 0; idx < 8; idx++) {
const color = WHITE;
if (idx >= (this.regs[REGS.CURSOR_UPPER] & 0x1f) &&
idx <= (this.regs[REGS.CURSOR_LOWER] & 0x1f)) {
if (
idx >= (this.regs[REGS.CURSOR_UPPER] & 0x1f) &&
idx <= (this.regs[REGS.CURSOR_LOWER] & 0x1f)
) {
for (let jdx = 0; jdx < 7; jdx++) {
data[(y + idx) * 560 * 4 + (x + jdx) * 4] = color[0];
data[(y + idx) * 560 * 4 + (x + jdx) * 4 + 1] = color[1];
data[(y + idx) * 560 * 4 + (x + jdx) * 4 + 2] = color[2];
data[(y + idx) * 560 * 4 + (x + jdx) * 4 + 1] =
color[1];
data[(y + idx) * 560 * 4 + (x + jdx) * 4 + 2] =
color[2];
}
}
}
@ -162,8 +167,7 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
private updateStartPos() {
const startPos =
this.regs[REGS.STARTPOS_HI] << 8 |
this.regs[REGS.STARTPOS_LO];
(this.regs[REGS.STARTPOS_HI] << 8) | this.regs[REGS.STARTPOS_LO];
if (this.startPos !== startPos) {
this.startPos = startPos;
this.shouldRefresh = true;
@ -208,7 +212,7 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
}
break;
}
this.bank = (off & 0x0C) >> 2;
this.bank = (off & 0x0c) >> 2;
return result;
}
@ -218,9 +222,9 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
read(page: byte, off: byte) {
if (page < 0xcc) {
return ROM[(page & 0x03) << 8 | off];
} else if (page < 0xce){
const addr = ((page & 0x01) + (this.bank << 1)) << 8 | off;
return ROM[((page & 0x03) << 8) | off];
} else if (page < 0xce) {
const addr = (((page & 0x01) + (this.bank << 1)) << 8) | off;
return this.buffer[addr];
}
return 0;
@ -228,7 +232,7 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
write(page: byte, off: byte, val: byte) {
if (page > 0xcb && page < 0xce) {
const addr = ((page & 0x01) + (this.bank << 1)) << 8 | off;
const addr = (((page & 0x01) + (this.bank << 1)) << 8) | off;
this.updateBuffer(addr, val);
}
}

View File

@ -24,16 +24,13 @@ export const App = () => {
const system = {
...defaultSystem,
...(systemTypes[systemType] || {})
...(systemTypes[systemType] || {}),
};
return (
<div className={cs(styles.container, componentStyles.components)}>
<Header e={system.e} />
<Apple2
gl={gl}
{...system}
/>
<Apple2 gl={gl} {...system} />
</div>
);
};

View File

@ -1,6 +1,12 @@
import { h } from 'preact';
import cs from 'classnames';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import { Apple2 as Apple2Impl } from '../apple2';
import { ControlStrip } from './ControlStrip';
import { Debugger } from './debugger/Debugger';
@ -60,40 +66,50 @@ export const Apple2 = (props: Apple2Props) => {
const vm = apple2?.getVideoModes();
const rom = apple2?.getROM();
const doPaste = useCallback((event: Event) => {
if (
(document.activeElement !== screenRef.current) &&
(document.activeElement !== document.body)
) {
return;
}
if (io) {
const paste = (event.clipboardData || window.clipboardData)?.getData('text');
if (paste) {
io.setKeyBuffer(paste);
const doPaste = useCallback(
(event: Event) => {
if (
document.activeElement !== screenRef.current &&
document.activeElement !== document.body
) {
return;
}
}
event.preventDefault();
}, [io]);
if (io) {
const paste = (
event.clipboardData || window.clipboardData
)?.getData('text');
if (paste) {
io.setKeyBuffer(paste);
}
}
event.preventDefault();
},
[io]
);
const doCopy = useCallback((event: Event) => {
if (
(document.activeElement !== screenRef.current) &&
(document.activeElement !== document.body)
) {
return;
}
if (vm) {
event.clipboardData?.setData('text/plain', vm.getText());
}
event.preventDefault();
}, [vm]);
const doCopy = useCallback(
(event: Event) => {
if (
document.activeElement !== screenRef.current &&
document.activeElement !== document.body
) {
return;
}
if (vm) {
event.clipboardData?.setData('text/plain', vm.getText());
}
event.preventDefault();
},
[vm]
);
useEffect(() => {
if (screenRef.current) {
const options = {
canvas: screenRef.current,
tick: () => { /* do nothing */ },
tick: () => {
/* do nothing */
},
...props,
};
const apple2 = new Apple2Impl(options);
@ -148,18 +164,33 @@ export const Apple2 = (props: Apple2Props) => {
return (
<div className={styles.container}>
<div
className={cs(styles.outer, { apple2e: e, [styles.ready]: ready })}
className={cs(styles.outer, {
apple2e: e,
[styles.ready]: ready,
})}
>
<Screen screenRef={screenRef} />
{!e ? <LanguageCard cpu={cpu} io={io} rom={rom} slot={0} /> : null}
{!e ? (
<LanguageCard cpu={cpu} io={io} rom={rom} slot={0} />
) : null}
<Slinky io={io} slot={2} />
{!e ? <Videoterm io={io} slot={3} /> : null}
<Mouse cpu={cpu} screenRef={screenRef} io={io} slot={4} />
<ThunderClock io={io} slot={5} />
<Inset>
<Drives cpu={cpu} io={io} sectors={sectors} enhanced={enhanced} ready={drivesReady} />
<Drives
cpu={cpu}
io={io}
sectors={sectors}
enhanced={enhanced}
ready={drivesReady}
/>
</Inset>
<ControlStrip apple2={apple2} e={e} toggleDebugger={toggleDebugger} />
<ControlStrip
apple2={apple2}
e={e}
toggleDebugger={toggleDebugger}
/>
<Inset>
<Keyboard apple2={apple2} e={e} />
</Inset>

View File

@ -85,12 +85,17 @@ export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => {
id={`disk${number}`}
className={cs(styles.diskLight, { [styles.on]: on })}
/>
<ControlButton title="Load Disk" onClick={onOpenModal} icon="folder-open" />
<ControlButton title="Save Disk" onClick={onOpenDownloadModal} icon="save" />
<div
id={`disk-label${number}`}
className={styles.diskLabel}
>
<ControlButton
title="Load Disk"
onClick={onOpenModal}
icon="folder-open"
/>
<ControlButton
title="Save Disk"
onClick={onOpenDownloadModal}
icon="save"
/>
<div id={`disk-label${number}`} className={styles.diskLabel}>
{name}
</div>
</DiskDragTarget>

View File

@ -14,8 +14,10 @@ import styles from './css/BlockFileModal.module.scss';
const DISK_TYPES: FilePickerAcceptType[] = [
{
description: 'Disk Images',
accept: { 'application/octet-stream': BLOCK_FORMATS.map(x => '.' + x) },
}
accept: {
'application/octet-stream': BLOCK_FORMATS.map((x) => '.' + x),
},
},
];
interface BlockFileModalProps {
@ -25,7 +27,12 @@ interface BlockFileModalProps {
onClose: (closeBox?: boolean) => void;
}
export const BlockFileModal = ({ smartPort, driveNo: number, onClose, isOpen }: BlockFileModalProps) => {
export const BlockFileModal = ({
smartPort,
driveNo: number,
onClose,
isOpen,
}: BlockFileModalProps) => {
const [handles, setHandles] = useState<FileSystemFileHandle[]>();
const [busy, setBusy] = useState<boolean>(false);
const [empty, setEmpty] = useState<boolean>(true);
@ -41,7 +48,11 @@ export const BlockFileModal = ({ smartPort, driveNo: number, onClose, isOpen }:
hashParts[number] = '';
setBusy(true);
try {
await loadLocalBlockFile(smartPort, number, await handles[0].getFile());
await loadLocalBlockFile(
smartPort,
number,
await handles[0].getFile()
);
} catch (error) {
setError(error);
} finally {
@ -68,7 +79,9 @@ export const BlockFileModal = ({ smartPort, driveNo: number, onClose, isOpen }:
</ModalContent>
<ModalFooter>
<button onClick={doCancel}>Cancel</button>
<button onClick={noAwait(doOpen)} disabled={busy || empty}>Open</button>
<button onClick={noAwait(doOpen)} disabled={busy || empty}>
Open
</button>
</ModalFooter>
</Modal>
<ErrorModal error={error} setError={setError} />

View File

@ -38,19 +38,11 @@ export const CPUMeter = ({ apple2 }: CPUMeterProps) => {
const time = Date.now();
const delta = time - lastTime.current;
if (stats) {
setKhz(
Math.floor(
(stats.cycles - cycles) / delta
)
);
setFps(
Math.floor(
(stats.frames - frames) / delta * 1000
)
);
setKhz(Math.floor((stats.cycles - cycles) / delta));
setFps(Math.floor(((stats.frames - frames) / delta) * 1000));
setRps(
Math.floor(
(stats.renderedFrames - renderedFrames) / delta * 1000
((stats.renderedFrames - renderedFrames) / delta) * 1000
)
);
lastStats.current = { ...stats };

View File

@ -22,8 +22,19 @@ export interface ControlButtonProps {
* @param onClick Click callback
* @returns Control Button component
*/
export const ControlButton = ({ active, icon, title, onClick, ...props }: ControlButtonProps) => (
<button className={styles.iconButton} onClick={onClick} title={title} {...props} >
export const ControlButton = ({
active,
icon,
title,
onClick,
...props
}: ControlButtonProps) => (
<button
className={styles.iconButton}
onClick={onClick}
title={title}
{...props}
>
<i className={cs('fas', `fa-${icon}`, { [styles.active]: active })}></i>
</button>
);

View File

@ -33,7 +33,11 @@ interface ControlStripProps {
* @param e Whether or not this is a //e
* @returns ControlStrip component
*/
export const ControlStrip = ({ apple2, e, toggleDebugger }: ControlStripProps) => {
export const ControlStrip = ({
apple2,
e,
toggleDebugger,
}: ControlStripProps) => {
const [showOptions, setShowOptions] = useState(false);
const [io, setIO] = useState<Apple2IO>();
const options = useContext(OptionsContext);
@ -55,23 +59,22 @@ export const ControlStrip = ({ apple2, e, toggleDebugger }: ControlStripProps) =
}
}, [apple2, e, options]);
const doReset = useCallback(() =>
apple2?.reset(), [apple2]);
const doReset = useCallback(() => apple2?.reset(), [apple2]);
const doReadme = useCallback(() =>
window.open(README, '_blank'), []);
const doReadme = useCallback(() => window.open(README, '_blank'), []);
const doShowOptions = useCallback(() =>
setShowOptions(true), []);
const doShowOptions = useCallback(() => setShowOptions(true), []);
const doCloseOptions = useCallback(() =>
setShowOptions(false), []);
const doCloseOptions = useCallback(() => setShowOptions(false), []);
const doToggleFullPage = useCallback(() =>
options.setOption(
SCREEN_FULL_PAGE,
!options.getOption(SCREEN_FULL_PAGE)
), [options]);
const doToggleFullPage = useCallback(
() =>
options.setOption(
SCREEN_FULL_PAGE,
!options.getOption(SCREEN_FULL_PAGE)
),
[options]
);
useHotKey('F2', doToggleFullPage);
useHotKey('F4', doShowOptions);
@ -82,12 +85,20 @@ export const ControlStrip = ({ apple2, e, toggleDebugger }: ControlStripProps) =
<OptionsModal isOpen={showOptions} onClose={doCloseOptions} />
<Inset>
<CPUMeter apple2={apple2} />
<ControlButton onClick={toggleDebugger} title="Toggle Debugger" icon="bug" />
<ControlButton
onClick={toggleDebugger}
title="Toggle Debugger"
icon="bug"
/>
<AudioControl apple2={apple2} />
<Printer io={io} slot={1} />
<div style={{ flexGrow: 1 }} />
<ControlButton onClick={doReadme} title="About" icon="info" />
<ControlButton onClick={doShowOptions} title="Options (F4)" icon="cog" />
<ControlButton
onClick={doShowOptions}
title="Options (F4)"
icon="cog"
/>
</Inset>
{e && (
<div className={styles.reset} onClick={doReset}>

View File

@ -1,15 +1,20 @@
import { BLOCK_FORMATS, DISK_FORMATS, DriveNumber, FLOPPY_FORMATS, MassStorage } from 'js/formats/types';
import {
BLOCK_FORMATS,
DISK_FORMATS,
DriveNumber,
FLOPPY_FORMATS,
MassStorage,
} from 'js/formats/types';
import { h, JSX, RefObject } from 'preact';
import { useEffect, useRef } from 'preact/hooks';
import { loadLocalFile } from './util/files';
import { loadLocalFile } from './util/files';
import { spawn } from './util/promises';
export interface DiskDragTargetProps<T> extends JSX.HTMLAttributes<HTMLDivElement> {
export interface DiskDragTargetProps<T>
extends JSX.HTMLAttributes<HTMLDivElement> {
storage: MassStorage<T> | undefined;
driveNo?: DriveNumber;
formats: typeof FLOPPY_FORMATS
| typeof BLOCK_FORMATS
| typeof DISK_FORMATS;
formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS;
dropRef?: RefObject<HTMLElement>;
onError: (error: unknown) => void;
}
@ -32,7 +37,11 @@ export const DiskDragTarget = ({
event.preventDefault();
const dt = event.dataTransfer;
if (dt) {
if (Array.from(dt.items).every((item) => item.kind === 'file')) {
if (
Array.from(dt.items).every(
(item) => item.kind === 'file'
)
) {
dt.dropEffect = 'copy';
} else {
dt.dropEffect = 'none';
@ -54,13 +63,18 @@ export const DiskDragTarget = ({
const onDrop = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
const targetDrive = driveNo ?? 1; //TODO(whscullin) Maybe pick available drive
const targetDrive = driveNo ?? 1; //TODO(whscullin) Maybe pick available drive
const dt = event.dataTransfer;
if (dt?.files.length === 1 && storage) {
spawn(async () => {
try {
await loadLocalFile(storage, formats, targetDrive, dt.files[0]);
await loadLocalFile(
storage,
formats,
targetDrive,
dt.files[0]
);
} catch (e) {
onError(e);
}
@ -68,8 +82,18 @@ export const DiskDragTarget = ({
} else if (dt?.files.length === 2 && storage) {
spawn(async () => {
try {
await loadLocalFile(storage, formats, 1, dt.files[0]);
await loadLocalFile(storage, formats, 2, dt.files[1]);
await loadLocalFile(
storage,
formats,
1,
dt.files[0]
);
await loadLocalFile(
storage,
formats,
2,
dt.files[1]
);
} catch (e) {
onError(e);
}

View File

@ -70,7 +70,12 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
formats={FLOPPY_FORMATS}
onError={setError}
>
<FileModal disk2={disk2} driveNo={number} onClose={doClose} isOpen={modalOpen} />
<FileModal
disk2={disk2}
driveNo={number}
onClose={doClose}
isOpen={modalOpen}
/>
<DownloadModal
driveNo={number}
massStorage={disk2}
@ -79,11 +84,17 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
/>
<ErrorModal error={error} setError={setError} />
<div className={cs(styles.diskLight, { [styles.on]: on })} />
<ControlButton title="Load Disk" onClick={onOpenModal} icon="folder-open" />
<ControlButton title="Save Disk" onClick={onOpenDownloadModal} icon="save" />
<div className={styles.diskLabel}>
{label}
</div>
<ControlButton
title="Load Disk"
onClick={onOpenModal}
icon="folder-open"
/>
<ControlButton
title="Save Disk"
onClick={onOpenDownloadModal}
icon="save"
/>
<div className={styles.diskLabel}>{label}</div>
</DiskDragTarget>
);
};

View File

@ -12,7 +12,12 @@ interface DownloadModalProps {
onClose: (closeBox?: boolean) => void;
}
export const DownloadModal = ({ massStorage, driveNo, onClose, isOpen }: DownloadModalProps) => {
export const DownloadModal = ({
massStorage,
driveNo,
onClose,
isOpen,
}: DownloadModalProps) => {
const [href, setHref] = useState('');
const [downloadName, setDownloadName] = useState('');
const doCancel = useCallback(() => onClose(true), [onClose]);
@ -24,10 +29,9 @@ export const DownloadModal = ({ massStorage, driveNo, onClose, isOpen }: Downloa
const { ext, data } = storageData;
const { name } = storageData.metadata;
if (data.byteLength) {
const blob = new Blob(
[data],
{ type: 'application/octet-stream' }
);
const blob = new Blob([data], {
type: 'application/octet-stream',
});
const href = window.URL.createObjectURL(blob);
setHref(href);
setDownloadName(`${name}.${ext}`);
@ -44,22 +48,20 @@ export const DownloadModal = ({ massStorage, driveNo, onClose, isOpen }: Downloa
<Modal title="Save File" isOpen={isOpen} onClose={onClose}>
<ModalContent>
<div className={styles.modalContent}>
{href
? (
<>
<span>Disk Name: {downloadName}</span>
<a
role="button"
href={href}
download={downloadName}
>
Download
</a>
</>
) : (
<span>No Download Available</span>
)
}
{href ? (
<>
<span>Disk Name: {downloadName}</span>
<a
role="button"
href={href}
download={downloadName}
>
Download
</a>
</>
) : (
<span>No Download Available</span>
)}
</div>
</ModalContent>
<ModalFooter>

View File

@ -8,9 +8,18 @@ import { CPU6502 } from '@whscullin/cpu6502';
import { BlockDisk } from './BlockDisk';
import { ErrorModal } from './ErrorModal';
import { ProgressModal } from './ProgressModal';
import { loadHttpUnknownFile, getHashParts, loadJSON, SmartStorageBroker } from './util/files';
import {
loadHttpUnknownFile,
getHashParts,
loadJSON,
SmartStorageBroker,
} from './util/files';
import { useHash } from './hooks/useHash';
import { DISK_FORMATS, DRIVE_NUMBERS, SupportedSectors } from 'js/formats/types';
import {
DISK_FORMATS,
DRIVE_NUMBERS,
SupportedSectors,
} from 'js/formats/types';
import { spawn, Ready } from './util/promises';
import styles from './css/Drives.module.scss';
@ -74,12 +83,12 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
const [smartData1, setSmartData1] = useState<DiskIIData>({
on: false,
number: 1,
name: 'HD 1'
name: 'HD 1',
});
const [smartData2, setSmartData2] = useState<DiskIIData>({
on: false,
number: 2,
name: 'HD 2'
name: 'HD 2',
});
const hash = useHash();
@ -97,25 +106,30 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
const isJson = hashPart.match(/\.json$/i);
if (isHttp && !isJson) {
loading++;
controllers.push(spawn(async (signal) => {
try {
await loadHttpUnknownFile(
smartStorageBroker,
driveNo,
hashPart,
signal,
onProgress);
} catch (e) {
setError(e);
}
if (--loading === 0) {
ready.onReady();
}
setCurrent(0);
setTotal(0);
}));
controllers.push(
spawn(async (signal) => {
try {
await loadHttpUnknownFile(
smartStorageBroker,
driveNo,
hashPart,
signal,
onProgress
);
} catch (e) {
setError(e);
}
if (--loading === 0) {
ready.onReady();
}
setCurrent(0);
setTotal(0);
})
);
} else {
const url = isHttp ? hashPart : `json/disks/${hashPart}.json`;
const url = isHttp
? hashPart
: `json/disks/${hashPart}.json`;
loadJSON(disk2, driveNo, url).catch((e) => setError(e));
}
}
@ -123,7 +137,8 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
if (!loading) {
ready.onReady();
}
return () => controllers.forEach((controller) => controller.abort());
return () =>
controllers.forEach((controller) => controller.abort());
}
}, [hash, onProgress, ready, storageDevices]);
@ -132,10 +147,10 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
const setSmartData = [setSmartData1, setSmartData2];
const callbacks: Callbacks = {
driveLight: (drive, on) => {
setData[drive - 1]?.(data => ({ ...data, on }));
setData[drive - 1]?.((data) => ({ ...data, on }));
},
label: (drive, name, side) => {
setData[drive - 1]?.(data => ({
setData[drive - 1]?.((data) => ({
...data,
name: name ?? `Disk ${drive}`,
side,
@ -143,27 +158,31 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
},
dirty: () => {
// do nothing
}
},
};
const smartPortCallbacks: Callbacks = {
driveLight: (drive, on) => {
setSmartData[drive - 1]?.(data => ({ ...data, on }));
setSmartData[drive - 1]?.((data) => ({ ...data, on }));
},
label: (drive, name, side) => {
setSmartData[drive - 1]?.(data => ({
setSmartData[drive - 1]?.((data) => ({
...data,
name: name ?? `HD ${drive}`,
side,
}));
},
dirty: () => {/* Unused */ }
dirty: () => {
/* Unused */
},
};
if (cpu && io) {
const disk2 = new Disk2(io, callbacks, sectors);
io.setSlot(6, disk2);
const smartPort = new SmartPort(cpu, smartPortCallbacks, { block: !enhanced });
const smartPort = new SmartPort(cpu, smartPortCallbacks, {
block: !enhanced,
});
io.setSlot(7, smartPort);
const smartStorageBroker = new SmartStorageBroker(disk2, smartPort);

View File

@ -5,7 +5,7 @@ import { Modal, ModalContent, ModalFooter } from './Modal';
import styles from './css/ErrorModal.module.scss';
export interface ErrorProps {
error: unknown | undefined;
error: unknown;
setError: (error: string | undefined) => void;
}
@ -32,9 +32,7 @@ export const ErrorModal = ({ error, setError }: ErrorProps) => {
onClose={onClose}
>
<ModalContent>
<div className={styles.errorModal}>
{errorStr}
</div>
<div className={styles.errorModal}>{errorStr}</div>
</ModalContent>
<ModalFooter>
<button onClick={onClose}>OK</button>

View File

@ -1,5 +1,11 @@
import { h, Fragment } from 'preact';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import { noAwait } from './util/promises';
export interface FilePickerAcceptType {
@ -34,7 +40,9 @@ interface ExtraProps {
const InputFileChooser = ({
disabled = false,
onChange = () => { /* do nothing */ },
onChange = () => {
/* do nothing */
},
accept = [],
}: InputFileChooserProps) => {
const inputRef = useRef<HTMLInputElement>(null);
@ -69,7 +77,9 @@ const InputFileChooser = ({
// the moment, not adding the MIME type is sufficient.
const newAccept: string[] = [];
for (const type of accept) {
for (let [/* typeString */, suffixes] of Object.entries(type.accept)) {
for (let [, /* typeString */ suffixes] of Object.entries(
type.accept
)) {
// newAccept.push(typeString);
if (!Array.isArray(suffixes)) {
suffixes = [suffixes];
@ -91,11 +101,15 @@ const InputFileChooser = ({
}, [accept]);
return (
<input type="file" role='button' aria-label='Open file'
<input
type="file"
role="button"
aria-label="Open file"
ref={inputRef}
onChange={onChangeInternal}
disabled={disabled}
{...extraProps} />
{...extraProps}
/>
);
};
@ -107,8 +121,10 @@ interface FilePickerChooserProps {
const FilePickerChooser = ({
disabled = false,
onChange = () => { /* do nothing */ },
accept = [ACCEPT_EVERYTHING_TYPE]
onChange = () => {
/* do nothing */
},
accept = [ACCEPT_EVERYTHING_TYPE],
}: FilePickerChooserProps) => {
const [busy, setBusy] = useState<boolean>(false);
const [selectedFilename, setSelectedFilename] = useState<string>();
@ -138,14 +154,16 @@ const FilePickerChooser = ({
useEffect(() => {
setSelectedFilename(
fileHandles?.length
? fileHandles[0].name
: 'No file selected');
fileHandles?.length ? fileHandles[0].name : 'No file selected'
);
}, [fileHandles]);
return (
<>
<button onClick={noAwait(onClickInternal)} disabled={disabled || busy}>
<button
onClick={noAwait(onClickInternal)}
disabled={disabled || busy}
>
Choose File
</button>
&nbsp;
@ -164,7 +182,7 @@ const FilePickerChooser = ({
* Using `window.showOpenFilePicker` has the advantage of allowing read/write
* access to the file, whereas the regular input element only gives read
* access.
*
*
* The FileChooser takes an optional `accept` parameter that specifies which
* types of files can be opened. The parameter is a map of MIME type to file
* extension. If the MIME type is the empty string, t
@ -174,38 +192,48 @@ export const FileChooser = ({
control = controlDefault,
...rest
}: FileChooserProps) => {
const onChangeForInput = useCallback((files: FileList) => {
const handles: FileSystemFileHandle[] = [];
for (let i = 0; i < files.length; i++) {
const file = files.item(i);
if (file === null) {
continue;
const onChangeForInput = useCallback(
(files: FileList) => {
const handles: FileSystemFileHandle[] = [];
for (let i = 0; i < files.length; i++) {
const file = files.item(i);
if (file === null) {
continue;
}
handles.push({
kind: 'file',
name: file.name,
getFile: () => Promise.resolve(file),
createWritable: (_options) =>
Promise.reject('File not writable.'),
queryPermission: (descriptor) =>
Promise.resolve(
descriptor?.mode === 'read' ? 'granted' : 'denied'
),
requestPermission: (descriptor) =>
Promise.resolve(
descriptor?.mode === 'read' ? 'granted' : 'denied'
),
isSameEntry: (_unused) => Promise.resolve(false),
isDirectory: false,
isFile: true,
});
}
handles.push({
kind: 'file',
name: file.name,
getFile: () => Promise.resolve(file),
createWritable: (_options) => Promise.reject('File not writable.'),
queryPermission: (descriptor) => Promise.resolve(descriptor?.mode === 'read' ? 'granted' : 'denied'),
requestPermission: (descriptor) => Promise.resolve(descriptor?.mode === 'read' ? 'granted' : 'denied'),
isSameEntry: (_unused) => Promise.resolve(false),
isDirectory: false,
isFile: true,
});
}
onChange(handles);
}, [onChange]);
onChange(handles);
},
[onChange]
);
const onChangeForPicker = useCallback((fileHandles: FileSystemFileHandle[]) => {
onChange(fileHandles);
}, [onChange]);
const onChangeForPicker = useCallback(
(fileHandles: FileSystemFileHandle[]) => {
onChange(fileHandles);
},
[onChange]
);
return control === 'picker'
? (
<FilePickerChooser onChange={onChangeForPicker} {...rest} />
)
: (
<InputFileChooser onChange={onChangeForInput} {...rest} />
);
return control === 'picker' ? (
<FilePickerChooser onChange={onChangeForPicker} {...rest} />
) : (
<InputFileChooser onChange={onChangeForInput} {...rest} />
);
};

View File

@ -1,8 +1,18 @@
import { h, Fragment, JSX } from 'preact';
import { useCallback, useEffect, useState } from 'preact/hooks';
import { DiskDescriptor, DriveNumber, FLOPPY_FORMATS, NibbleFormat } from '../formats/types';
import {
DiskDescriptor,
DriveNumber,
FLOPPY_FORMATS,
NibbleFormat,
} from '../formats/types';
import { Modal, ModalContent, ModalFooter } from './Modal';
import { loadLocalNibbleFile, loadJSON, getHashParts, setHashParts } from './util/files';
import {
loadLocalNibbleFile,
loadJSON,
getHashParts,
setHashParts,
} from './util/files';
import DiskII from '../cards/disk2';
import { ErrorModal } from './ErrorModal';
@ -15,8 +25,10 @@ import styles from './css/FileModal.module.scss';
const DISK_TYPES: FilePickerAcceptType[] = [
{
description: 'Disk Images',
accept: { 'application/octet-stream': FLOPPY_FORMATS.map(x => '.' + x) },
}
accept: {
'application/octet-stream': FLOPPY_FORMATS.map((x) => '.' + x),
},
},
];
export type NibbleFileCallback = (
@ -38,7 +50,12 @@ interface IndexEntry {
category: string;
}
export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) => {
export const FileModal = ({
disk2,
driveNo,
onClose,
isOpen,
}: FileModalProps) => {
const [busy, setBusy] = useState<boolean>(false);
const [empty, setEmpty] = useState<boolean>(true);
const [category, setCategory] = useState<string>();
@ -52,7 +69,7 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
spawn(async () => {
try {
const indexResponse = await fetch('json/disks/index.json');
const index = await indexResponse.json() as IndexEntry[];
const index = (await indexResponse.json()) as IndexEntry[];
setIndex(index);
} catch (error) {
setIndex([]);
@ -70,7 +87,11 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
try {
if (handles?.length === 1) {
hashParts[driveNo] = '';
await loadLocalNibbleFile(disk2, driveNo, await handles[0].getFile());
await loadLocalNibbleFile(
disk2,
driveNo,
await handles[0].getFile()
);
}
if (filename) {
const name = filename.match(/\/([^/]+).json$/) || ['', ''];
@ -95,15 +116,16 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
const doSelectCategory = useCallback(
(event: JSX.TargetedMouseEvent<HTMLSelectElement>) =>
setCategory(event.currentTarget.value)
, []
setCategory(event.currentTarget.value),
[]
);
const doSelectFilename = useCallback(
(event: JSX.TargetedMouseEvent<HTMLSelectElement>) => {
setEmpty(!event.currentTarget.value);
setFilename(event.currentTarget.value);
}, []
},
[]
);
if (!index) {
@ -111,10 +133,7 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
}
const categories = index.reduce<Record<string, DiskDescriptor[]>>(
(
acc: Record<string, DiskDescriptor[]>,
disk: DiskDescriptor
) => {
(acc: Record<string, DiskDescriptor[]>, disk: DiskDescriptor) => {
const category = disk.category || 'Misc';
acc[category] = [disk, ...(acc[category] || [])];
@ -138,7 +157,10 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
</select>
<select multiple onChange={doSelectFilename}>
{disks.map((disk) => (
<option key={disk.filename} value={disk.filename}>
<option
key={disk.filename}
value={disk.filename}
>
{disk.name}
{disk.disk ? ` - ${disk.disk}` : ''}
</option>
@ -149,7 +171,9 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
</ModalContent>
<ModalFooter>
<button onClick={doCancel}>Cancel</button>
<button onClick={noAwait(doOpen)} disabled={busy || empty}>Open</button>
<button onClick={noAwait(doOpen)} disabled={busy || empty}>
Open
</button>
</ModalFooter>
</Modal>
<ErrorModal error={error} setError={setError} />

View File

@ -7,7 +7,7 @@ import {
keys2e,
mapMouseEvent,
keysAsTuples,
mapKeyboardEvent
mapKeyboardEvent,
} from './util/keyboard';
import styles from './css/Keyboard.module.scss';
@ -58,26 +58,21 @@ export const Key = ({
active,
pressed,
onMouseDown,
onMouseUp
onMouseUp,
}: KeyProps) => {
const keyName = lower.replace(/[&#;]/g, '');
const center =
lower === 'LOCK'
? styles.vCenter2
: (upper === lower && upper.length > 1)
? styles.vCenter
: '';
: upper === lower && upper.length > 1
? styles.vCenter
: '';
return (
<div
className={cs(
styles.key,
styles[`key-${keyName}`],
center,
{
[styles.pressed]: pressed,
[styles.active]: active,
},
)}
className={cs(styles.key, styles[`key-${keyName}`], center, {
[styles.pressed]: pressed,
[styles.active]: active,
})}
data-key1={lower}
data-key2={upper}
onMouseDown={onMouseDown}
@ -85,7 +80,12 @@ export const Key = ({
>
<div>
{buildLabel(upper)}
{upper !== lower && <><br />{buildLabel(lower)}</>}
{upper !== lower && (
<>
<br />
{buildLabel(lower)}
</>
)}
</div>
</div>
);
@ -119,15 +119,22 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
return;
}
if (document.activeElement && document.activeElement !== document.body) {
if (
document.activeElement &&
document.activeElement !== document.body
) {
return;
}
event.preventDefault();
const { key, keyCode, keyLabel } = mapKeyboardEvent(event, active.includes('LOCK'), active.includes('CTRL'));
setPressed(pressed => pressed.concat([keyLabel]));
setActive(active => active.concat([keyLabel]));
const { key, keyCode, keyLabel } = mapKeyboardEvent(
event,
active.includes('LOCK'),
active.includes('CTRL')
);
setPressed((pressed) => pressed.concat([keyLabel]));
setActive((active) => active.concat([keyLabel]));
if (key === 'RESET') {
apple2.reset();
@ -153,8 +160,8 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
return;
}
const { key, keyLabel } = mapKeyboardEvent(event);
setPressed(pressed => pressed.filter(k => k !== keyLabel));
setActive(active => active.filter(k => k !== keyLabel));
setPressed((pressed) => pressed.filter((k) => k !== keyLabel));
setActive((active) => active.filter((k) => k !== keyLabel));
const io = apple2.getIO();
if (key === 'OPEN_APPLE') {
@ -186,7 +193,7 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
setActive([...active, key]);
return true;
}
setActive(active.filter(x => x !== key));
setActive(active.filter((x) => x !== key));
return false;
};
@ -237,12 +244,12 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
true
);
apple2?.getIO().keyUp();
setPressed(pressed.filter(x => x !== keyLabel));
setPressed(pressed.filter((x) => x !== keyLabel));
},
[apple2, active, pressed]
);
const bindKey = ([lower, upper]: [string, string]) =>
const bindKey = ([lower, upper]: [string, string]) => (
<Key
lower={lower}
upper={upper}
@ -250,17 +257,14 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
pressed={pressed.includes(lower)}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
/>;
/>
);
const rows = keys.map((row, idx) =>
const rows = keys.map((row, idx) => (
<div key={idx} className={cs(styles.row, styles[`row${idx}`])}>
{row.map(bindKey)}
</div>
);
));
return (
<div className={styles.keyboard}>
{rows}
</div>
);
return <div className={styles.keyboard}>{rows}</div>;
};

View File

@ -15,11 +15,7 @@ import { ControlButton } from './ControlButton';
* @returns ModalOverlay component
*/
export const ModalOverlay = ({ children }: { children: ComponentChildren }) => {
return (
<div className={styles.modalOverlay}>
{children}
</div>
);
return <div className={styles.modalOverlay}>{children}</div>;
};
/**
@ -28,11 +24,7 @@ export const ModalOverlay = ({ children }: { children: ComponentChildren }) => {
* @returns ModalContent component
*/
export const ModalContent = ({ children }: { children: ComponentChildren }) => {
return (
<div className={styles.modalContent}>
{children}
</div>
);
return <div className={styles.modalContent}>{children}</div>;
};
/**
@ -66,9 +58,7 @@ export const ModalCloseButton = ({ onClose }: ModalCloseButtonProp) => {
const doClose = useCallback(() => onClose(true), [onClose]);
useHotKey('Escape', doClose);
return (
<ControlButton onClick={doClose} title="Close" icon="xmark" />
);
return <ControlButton onClick={doClose} title="Close" icon="xmark" />;
};
type OnCloseCallback = (closeBox?: boolean) => void;
@ -94,8 +84,7 @@ export const ModalHeader = ({ onClose, title, icon }: ModalHeaderProps) => {
return (
<div role="banner" className={styles.modalHeader}>
<span className={styles.modalTitle}>
{icon && <i className={`fa-solid fa-${icon}`} role="img" />}
{' '}
{icon && <i className={`fa-solid fa-${icon}`} role="img" />}{' '}
{title}
</span>
{onClose && <ModalCloseButton onClose={onClose} />}
@ -124,20 +113,19 @@ export interface ModalProps {
* @param onClose Close callback
* @returns Modal component
*/
export const Modal = ({
isOpen,
children,
title,
...props
}: ModalProps) => {
return (
isOpen ? createPortal((
<ModalOverlay>
<div className={cs(styles.modal, componentStyles.components)} role="dialog">
{title && <ModalHeader title={title} {...props} />}
{children}
</div>
</ModalOverlay>
), document.body) : null
);
export const Modal = ({ isOpen, children, title, ...props }: ModalProps) => {
return isOpen
? createPortal(
<ModalOverlay>
<div
className={cs(styles.modal, componentStyles.components)}
role="dialog"
>
{title && <ModalHeader title={title} {...props} />}
{children}
</div>
</ModalOverlay>,
document.body
)
: null;
};

View File

@ -33,17 +33,13 @@ const Boolean = ({ option, value, setValue }: BooleanProps) => {
const { label, name } = option;
const onChange = useCallback(
(event: JSX.TargetedMouseEvent<HTMLInputElement>) =>
setValue(name, event.currentTarget.checked)
, [name, setValue]
setValue(name, event.currentTarget.checked),
[name, setValue]
);
return (
<li>
<input
type="checkbox"
checked={value}
onChange={onChange}
/>
<input type="checkbox" checked={value} onChange={onChange} />
<label>{label}</label>
</li>
);
@ -84,9 +80,7 @@ const Select = ({ option, value, setValue }: SelectProps) => {
return (
<li>
<select onChange={onChange}>
{option.values.map(makeOption)}
</select>
<select onChange={onChange}>{option.values.map(makeOption)}</select>
<label>{label}</label>
</li>
);
@ -110,9 +104,12 @@ export interface OptionsModalProps {
export const OptionsModal = ({ isOpen, onClose }: OptionsModalProps) => {
const options = useContext(OptionsContext);
const sections = options.getSections();
const setValue = useCallback((name: string, value: string | boolean) => {
options.setOption(name, value);
}, [options]);
const setValue = useCallback(
(name: string, value: string | boolean) => {
options.setOption(name, value);
},
[options]
);
const makeOption = (option: Option) => {
const { name, type } = option;
@ -143,9 +140,7 @@ export const OptionsModal = ({ isOpen, onClose }: OptionsModalProps) => {
return (
<>
<h3>{section.name}</h3>
<ul>
{section.options.map(makeOption)}
</ul>
<ul>{section.options.map(makeOption)}</ul>
</>
);
};

View File

@ -1,5 +1,11 @@
import { h, Fragment } from 'preact';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import Apple2IO, { slot } from 'js/apple2io';
import Parallel, { ParallelOptions } from 'js/cards/parallel';
import { Modal, ModalContent, ModalFooter } from './Modal';
@ -20,32 +26,35 @@ export const Printer = ({ io, slot }: PrinterProps) => {
const rawLength = useRef(0);
const [href, setHref] = useState('');
const cbs = useMemo<ParallelOptions>(() => ({
putChar: (val: byte) => {
const ascii = val & 0x7f;
const visible = val >= 0x20;
const char = String.fromCharCode(ascii);
const cbs = useMemo<ParallelOptions>(
() => ({
putChar: (val: byte) => {
const ascii = val & 0x7f;
const visible = val >= 0x20;
const char = String.fromCharCode(ascii);
if (char === '\r') {
// Skip for once
} else if (char === '\t') {
// possibly not right due to tab stops
setContent((content) => content += ' ');
} else if (ascii === 0x04) {
setContent((content) => content = content.slice(0, -1));
return;
} else if (visible) {
setContent((content) => content += char);
}
if (char === '\r') {
// Skip for once
} else if (char === '\t') {
// possibly not right due to tab stops
setContent((content) => (content += ' '));
} else if (ascii === 0x04) {
setContent((content) => (content = content.slice(0, -1)));
return;
} else if (visible) {
setContent((content) => (content += char));
}
raw.current[rawLength.current++] = val;
if (rawLength.current > raw.current.length) {
const newRaw = new Uint8Array(raw.current.length * 2);
newRaw.set(raw.current);
raw.current = newRaw;
}
}
}), [rawLength]);
raw.current[rawLength.current++] = val;
if (rawLength.current > raw.current.length) {
const newRaw = new Uint8Array(raw.current.length * 2);
newRaw.set(raw.current);
raw.current = newRaw;
}
},
}),
[rawLength]
);
useEffect(() => {
if (io) {
@ -56,10 +65,9 @@ export const Printer = ({ io, slot }: PrinterProps) => {
useEffect(() => {
if (isOpen) {
const blob = new Blob(
[raw.current.slice(0, rawLength.current)],
{ type: 'application/octet-stream' }
);
const blob = new Blob([raw.current.slice(0, rawLength.current)], {
type: 'application/octet-stream',
});
const href = window.URL.createObjectURL(blob);
setHref(href);
}
@ -80,7 +88,7 @@ export const Printer = ({ io, slot }: PrinterProps) => {
<>
<Modal isOpen={isOpen} onClose={onClose} title="Printer">
<ModalContent>
<pre className={styles.printer} tabIndex={-1} >
<pre className={styles.printer} tabIndex={-1}>
{content}
</pre>
</ModalContent>

View File

@ -17,7 +17,9 @@ export const ProgressModal = ({ title, current, total }: ErrorProps) => {
<div className={styles.progressContainer}>
<div
className={styles.progressBar}
style={{ width: Math.floor(320 * (current / total)) }}
style={{
width: Math.floor(320 * (current / total)),
}}
/>
</div>
</ModalContent>

View File

@ -9,11 +9,7 @@ export interface TabProps {
}
export const Tab = ({ children }: TabProps) => {
return (
<div>
{children}
</div>
);
return <div>{children}</div>;
};
interface TabWrapperProps {
@ -24,7 +20,10 @@ interface TabWrapperProps {
const TabWrapper = ({ children, onClick, selected }: TabWrapperProps) => {
return (
<div onClick={onClick} className={cs(styles.tab, { [styles.selected]: selected })}>
<div
onClick={onClick}
className={cs(styles.tab, { [styles.selected]: selected })}
>
{children}
</div>
);
@ -38,10 +37,13 @@ export interface TabsProps {
export const Tabs = ({ children, setSelected }: TabsProps) => {
const [innerSelected, setInnerSelected] = useState(0);
const innerSetSelected = useCallback((idx: number) => {
setSelected(idx);
setInnerSelected(idx);
}, [setSelected]);
const innerSetSelected = useCallback(
(idx: number) => {
setSelected(idx);
setInnerSelected(idx);
},
[setSelected]
);
if (!Array.isArray(children)) {
return null;
@ -49,7 +51,7 @@ export const Tabs = ({ children, setSelected }: TabsProps) => {
return (
<div className={styles.tabs}>
{children.map((child, idx) =>
{children.map((child, idx) => (
<TabWrapper
key={idx}
onClick={() => innerSetSelected(idx)}
@ -57,7 +59,7 @@ export const Tabs = ({ children, setSelected }: TabsProps) => {
>
{child}
</TabWrapper>
)}
))}
</div>
);
};

View File

@ -9,7 +9,7 @@ export interface VideotermProps {
slot: slot;
}
export const Videoterm = ({ io, slot }: VideotermProps ) => {
export const Videoterm = ({ io, slot }: VideotermProps) => {
useEffect(() => {
if (io) {
const videoterm = new VideotermImpl();

View File

@ -6,7 +6,7 @@
.diskLight {
margin: 5px;
background-image: url("../../../css/green-off-16.png");
background-image: url('../../../css/green-off-16.png');
background-size: 16px 16px;
flex-shrink: 0;
width: 16px;
@ -14,7 +14,7 @@
}
.diskLight.on {
background-image: url("../../../css/green-on-16.png");
background-image: url('../../../css/green-on-16.png');
}
.diskLabel {
@ -29,12 +29,12 @@
user-select: none;
}
@media only screen and (min-resolution: 1.25dppx) {
@media only screen and (resolution >= 1.25dppx) {
.diskLight {
background-image: url("../../../css/green-off-32.png");
background-image: url('../../../css/green-off-32.png');
}
.diskLight.on {
background-image: url("../../../css/green-on-32.png");
background-image: url('../../../css/green-on-32.png');
}
}

View File

@ -2,13 +2,13 @@
font-size: 14px;
font-family: sans-serif;
a[role="button"] {
a[role='button'] {
text-decoration: none;
}
button,
a[role="button"],
input[type="file"]::file-selector-button {
a[role='button'],
input[type='file']::file-selector-button {
background: #44372c;
color: #fff;
padding: 2px 8px;
@ -24,26 +24,26 @@
}
button:hover,
a[role="button"]:hover,
input[type="file"]::file-selector-button {
a[role='button']:hover,
input[type='file']::file-selector-button {
background-color: #55473d;
border: 1px outset #66594e;
}
button:active,
a[role="button"]:active,
input[type="file"]::file-selector-button {
a[role='button']:active,
input[type='file']::file-selector-button {
background-color: #22150a;
border: 1px outset #44372c;
}
button:focus,
a[role="button"]:focus,
input[type="file"]::file-selector-button {
a[role='button']:focus,
input[type='file']::file-selector-button {
outline: none;
}
input[type="checkbox"] {
input[type='checkbox'] {
appearance: none;
background-color: #65594d;
border: 1px inset #65594d;
@ -54,14 +54,14 @@
position: relative;
}
input[type="checkbox"]:checked {
input[type='checkbox']:checked {
background-color: #65594d;
border: 1px inset #65594d;
color: #0d0;
}
input[type="checkbox"]:checked::after {
content: "\2716";
input[type='checkbox']:checked::after {
content: '\2716';
font-size: 12px;
position: absolute;
top: 0;

View File

@ -8,7 +8,9 @@
/* border: 5px outset #66594E; */
border-radius: 3px;
color: white;
font: 9px Helvetica, sans-serif;
font:
9px Helvetica,
sans-serif;
height: 36px;
padding: 0;
margin-left: 10px;

View File

@ -6,7 +6,7 @@
.diskLight {
margin: 5px;
background-image: url("../../../css/red-off-16.png");
background-image: url('../../../css/red-off-16.png');
background-size: 16px 16px;
flex-shrink: 0;
width: 16px;
@ -14,7 +14,7 @@
}
.diskLight.on {
background-image: url("../../../css/red-on-16.png");
background-image: url('../../../css/red-on-16.png');
}
.diskLabel {
@ -29,12 +29,12 @@
user-select: none;
}
@media only screen and (min-resolution: 1.25dppx) {
@media only screen and (resolution >= 1.25dppx) {
.diskLight {
background-image: url("../../../css/red-off-32.png");
background-image: url('../../../css/red-off-32.png');
}
.diskLight.on {
background-image: url("../../../css/red-on-32.png");
background-image: url('../../../css/red-on-32.png');
}
}

View File

@ -26,7 +26,7 @@
margin: 0;
padding: 3px 0 0 10;
color: black;
font-family: "Adobe Garamond Pro", Garamond, Times, serif;
font-family: 'Adobe Garamond Pro', Garamond, Times, serif;
font-size: 13px;
font-weight: normal;
user-select: none;

View File

@ -177,7 +177,7 @@
}
.key-OPEN_APPLE div {
background-image: url("../../../img/open-apple24.png");
background-image: url('../../../img/open-apple24.png');
width: 24px;
height: 24px;
bottom: 1px;
@ -185,7 +185,7 @@
}
.key-CLOSED_APPLE div {
background-image: url("../../../img/closed-apple24.png");
background-image: url('../../../img/closed-apple24.png');
width: 24px;
height: 24px;
bottom: 1px;
@ -201,9 +201,9 @@
}
.key-OPEN_APPLE.active div {
background-image: url("../../../img/open-apple24-green.png");
background-image: url('../../../img/open-apple24-green.png');
}
.key-CLOSED_APPLE.active div {
background-image: url("../../../img/closed-apple24-green.png");
background-image: url('../../../img/closed-apple24-green.png');
}

View File

@ -1,9 +1,6 @@
.modalOverlay {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
@ -63,7 +60,7 @@
text-align: right;
user-select: none;
a[role="button"],
a[role='button'],
button {
margin: 0 0 0 5px;
min-width: 75px;

View File

@ -1,5 +1,5 @@
:global(.mono) {
filter: url("#green");
filter: url('#green');
}
.display {
@ -14,10 +14,7 @@
padding: 0;
border: 0;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
inset: 0;
justify-content: center;
align-items: center;
margin: auto !important;
@ -48,31 +45,26 @@
:global(.scanlines)::after {
display: block;
pointer-events: none;
background-image:
repeating-linear-gradient(
to bottom,
transparent 0,
transparent 1px,
rgb(0 0 0 / 50%) 1px,
rgb(0 0 0 / 50%) 2px
);
content: "";
background-image: repeating-linear-gradient(
to bottom,
transparent 0,
transparent 1px,
rgb(0 0 0 / 50%) 1px,
rgb(0 0 0 / 50%) 2px
);
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
inset: 0;
}
:global(.full-page) :global(.scanlines)::after {
background-image:
repeating-linear-gradient(
to bottom,
transparent 0,
transparent 0.25vh,
rgb(0 0 0 / 50%) 0.25vh,
rgb(0 0 0 / 50%) 0.5vh
);
background-image: repeating-linear-gradient(
to bottom,
transparent 0,
transparent 0.25vh,
rgb(0 0 0 / 50%) 0.25vh,
rgb(0 0 0 / 50%) 0.5vh
);
}
.screen {

View File

@ -42,12 +42,23 @@ const formatArray = (value: unknown): string => {
const Variable = ({ variable }: { variable: ApplesoftVariable }) => {
const { name, type, sizes, value } = variable;
const isArray = !!sizes;
const arrayStr = isArray ? `(${sizes.map((size) => size - 1).join(',')})` : '';
const arrayStr = isArray
? `(${sizes.map((size) => size - 1).join(',')})`
: '';
return (
<tr>
<td>{name}{TYPE_SYMBOL[type]}{arrayStr}</td>
<td>{TYPE_NAME[type]}{isArray ? ' Array' : ''}</td>
<td><pre tabIndex={-1}>{isArray ? formatArray(value) : value}</pre></td>
<td>
{name}
{TYPE_SYMBOL[type]}
{arrayStr}
</td>
<td>
{TYPE_NAME[type]}
{isArray ? ' Array' : ''}
</td>
<td>
<pre tabIndex={-1}>{isArray ? formatArray(value) : value}</pre>
</td>
</tr>
);
};
@ -57,7 +68,7 @@ export const Applesoft = ({ apple2 }: ApplesoftProps) => {
const [data, setData] = useState<ApplesoftData>({
listing: '',
variables: [],
internals: {}
internals: {},
});
const [heap, setHeap] = useState<ApplesoftHeap>();
const cpu = apple2?.getCPU();
@ -72,18 +83,19 @@ export const Applesoft = ({ apple2 }: ApplesoftProps) => {
const animate = useCallback(() => {
if (cpu && heap) {
try {
const decompiler = ApplesoftDecompiler.decompilerFromMemory(cpu);
const decompiler =
ApplesoftDecompiler.decompilerFromMemory(cpu);
setData({
variables: heap.dumpVariables(),
internals: heap.dumpInternals(),
listing: decompiler.decompile()
listing: decompiler.decompile(),
});
} catch (error) {
if (error instanceof Error) {
setData({
variables: [],
internals: {},
listing: error.message
listing: error.message,
});
} else {
throw error;
@ -103,7 +115,9 @@ export const Applesoft = ({ apple2 }: ApplesoftProps) => {
return (
<div className={styles.column}>
<span className={debuggerStyles.subHeading}>Listing</span>
<pre className={styles.listing} tabIndex={-1}>{listing}</pre>
<pre className={styles.listing} tabIndex={-1}>
{listing}
</pre>
<span className={debuggerStyles.subHeading}>Variables</span>
<div className={styles.variables}>
<table>
@ -112,7 +126,9 @@ export const Applesoft = ({ apple2 }: ApplesoftProps) => {
<th>Type</th>
<th>Value</th>
</tr>
{variables.map((variable, idx) => <Variable key={idx} variable={variable} />)}
{variables.map((variable, idx) => (
<Variable key={idx} variable={variable} />
))}
</table>
</div>
<span className={debuggerStyles.subHeading}>Internals</span>

View File

@ -60,7 +60,7 @@ export const CPU = ({ apple2 }: CPUProps) => {
stack: debug.getStack(38),
trace: debug.getTrace(16),
zeroPage: debug.dumpPage(0),
memory: debug.dumpPage(parseInt(memoryPage, 16) || 0)
memory: debug.dumpPage(parseInt(memoryPage, 16) || 0),
});
}
animationRef.current = requestAnimationFrame(animate);
@ -83,46 +83,51 @@ export const CPU = ({ apple2 }: CPUProps) => {
debug?.step();
}, [debug]);
const doLoadAddress = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
setLoadAddress(event.currentTarget.value);
}, []);
const doRunCheck = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
setRun(event.currentTarget.checked);
}, []);
const doLoadAddress = useCallback(
(event: JSX.TargetedEvent<HTMLInputElement>) => {
setLoadAddress(event.currentTarget.value);
},
[]
);
const doRunCheck = useCallback(
(event: JSX.TargetedEvent<HTMLInputElement>) => {
setRun(event.currentTarget.checked);
},
[]
);
const doMemoryPage = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
setMemoryPage(event.currentTarget.value);
}, []);
const doMemoryPage = useCallback(
(event: JSX.TargetedEvent<HTMLInputElement>) => {
setMemoryPage(event.currentTarget.value);
},
[]
);
const doChooseFile = useCallback((handles: FileSystemFileHandle[]) => {
if (debug && handles.length === 1) {
spawn(async () => {
const file = await handles[0].getFile();
let atAddress = parseInt(loadAddress, 16) || 0x800;
const doChooseFile = useCallback(
(handles: FileSystemFileHandle[]) => {
if (debug && handles.length === 1) {
spawn(async () => {
const file = await handles[0].getFile();
let atAddress = parseInt(loadAddress, 16) || 0x800;
const matches = file.name.match(CIDERPRESS_EXTENSION);
if (matches && matches.length === 3) {
const [, , aux] = matches;
atAddress = parseInt(aux, 16);
}
const matches = file.name.match(CIDERPRESS_EXTENSION);
if (matches && matches.length === 3) {
const [, , aux] = matches;
atAddress = parseInt(aux, 16);
}
await loadLocalBinaryFile(file, atAddress, debug);
setLoadAddress(toHex(atAddress, 4));
if (run) {
debug?.runAt(atAddress);
}
});
}
}, [debug, loadAddress, run]);
await loadLocalBinaryFile(file, atAddress, debug);
setLoadAddress(toHex(atAddress, 4));
if (run) {
debug?.runAt(atAddress);
}
});
}
},
[debug, loadAddress, run]
);
const {
memory,
registers,
running,
stack,
trace,
zeroPage
} = data;
const { memory, registers, running, stack, trace, zeroPage } = data;
const memoryPageValid = VALID_PAGE.test(memoryPage);
const loadAddressValid = VALID_ADDRESS.test(loadAddress);
@ -156,9 +161,7 @@ export const CPU = ({ apple2 }: CPUProps) => {
<div className={debuggerStyles.row}>
<div className={debuggerStyles.column}>
<span className={debuggerStyles.subHeading}>Registers</span>
<pre tabIndex={-1}>
{registers}
</pre>
<pre tabIndex={-1}>{registers}</pre>
<span className={debuggerStyles.subHeading}>Trace</span>
<pre className={styles.trace} tabIndex={-1}>
{trace}
@ -177,7 +180,9 @@ export const CPU = ({ apple2 }: CPUProps) => {
</div>
<div>
<hr />
<span className={debuggerStyles.subHeading}>Memory Page: $ </span>
<span className={debuggerStyles.subHeading}>
Memory Page: ${' '}
</span>
<input
value={memoryPage}
onChange={doMemoryPage}
@ -199,9 +204,9 @@ export const CPU = ({ apple2 }: CPUProps) => {
onChange={doLoadAddress}
className={cs({ [styles.invalid]: !loadAddressValid })}
/>
{loadAddressValid ? null : ERROR_ICON}
{' '}
<input type="checkbox" checked={run} onChange={doRunCheck} />Run
{loadAddressValid ? null : ERROR_ICON}{' '}
<input type="checkbox" checked={run} onChange={doRunCheck} />
Run
<div className={styles.fileChooser}>
<FileChooser onChange={doChooseFile} />
</div>

View File

@ -2,7 +2,15 @@ import { h, Fragment } from 'preact';
import { useMemo } from 'preact/hooks';
import cs from 'classnames';
import { Apple2 as Apple2Impl } from 'js/apple2';
import { BlockDisk, DiskFormat, DriveNumber, FloppyDisk, isBlockDiskFormat, isNibbleDisk, MassStorage } from 'js/formats/types';
import {
BlockDisk,
DiskFormat,
DriveNumber,
FloppyDisk,
isBlockDiskFormat,
isNibbleDisk,
MassStorage,
} from 'js/formats/types';
import { slot } from 'js/apple2io';
import DiskII from 'js/cards/disk2';
import SmartPort from 'js/cards/smartport';
@ -18,7 +26,11 @@ import { toHex } from 'js/util';
import styles from './css/Disks.module.scss';
import debuggerStyles from './css/Debugger.module.scss';
import { useCallback, useState } from 'preact/hooks';
import { DOS33, FileEntry as DOSEntry, isMaybeDOS33 } from 'js/formats/dos/dos33';
import {
DOS33,
FileEntry as DOSEntry,
isMaybeDOS33,
} from 'js/formats/dos/dos33';
import createDiskFromDOS from 'js/formats/do';
import { FileData, FileViewer } from './FileViewer';
@ -29,7 +41,10 @@ import { FileData, FileViewer } from './FileViewer';
* @returns Short string date
*/
const formatDate = (date: Date) => {
return date.toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'short' });
return date.toLocaleString(undefined, {
dateStyle: 'short',
timeStyle: 'short',
});
};
/**
@ -39,7 +54,7 @@ const formatDate = (date: Date) => {
* @returns true if is BlockDisk
*/
function isBlockDisk(disk: FloppyDisk | BlockDisk): disk is BlockDisk {
return !!((disk as BlockDisk).blocks);
return !!(disk as BlockDisk).blocks;
}
/**
@ -80,14 +95,17 @@ const FileListing = ({ depth, fileEntry, setFileData }: FileListingProps) => {
onClick={doSetFileData}
>
{'| '.repeat(depth)}
{deleted ?
<i className="fas fa-file-circle-xmark" /> :
{deleted ? (
<i className="fas fa-file-circle-xmark" />
) : (
<i className="fas fa-file" />
}
{' '}
)}{' '}
{fileEntry.name}
</td>
<td>{FILE_TYPES[fileEntry.fileType] ?? `$${toHex(fileEntry.fileType)}`}</td>
<td>
{FILE_TYPES[fileEntry.fileType] ??
`$${toHex(fileEntry.fileType)}`}
</td>
<td>{`$${toHex(fileEntry.auxType, 4)}`}</td>
<td>{fileEntry.blocksUsed}</td>
<td>{formatDate(fileEntry.creation)}</td>
@ -115,7 +133,12 @@ interface DirectoryListingProps {
* @param dirEntry Current directory entry to display
* @returns DirectoryListing component
*/
const DirectoryListing = ({ volume, depth, dirEntry, setFileData }: DirectoryListingProps) => {
const DirectoryListing = ({
volume,
depth,
dirEntry,
setFileData,
}: DirectoryListingProps) => {
const [open, setOpen] = useState(depth === 0);
return (
<>
@ -126,8 +149,12 @@ const DirectoryListing = ({ volume, depth, dirEntry, setFileData }: DirectoryLis
title={dirEntry.name}
>
{'| '.repeat(depth)}
<i className={cs('fas', { 'fa-folder-open': open, 'fa-folder-closed': !open })} />
{' '}
<i
className={cs('fas', {
'fa-folder-open': open,
'fa-folder-closed': !open,
})}
/>{' '}
{dirEntry.name}
</td>
<td></td>
@ -136,26 +163,31 @@ const DirectoryListing = ({ volume, depth, dirEntry, setFileData }: DirectoryLis
<td>{formatDate(dirEntry.creation)}</td>
<td></td>
</tr>
{open && dirEntry.entries.map((fileEntry, idx) => {
if (fileEntry.storageType === STORAGE_TYPES.DIRECTORY) {
const dirEntry = new Directory(volume, fileEntry);
return <DirectoryListing
key={idx}
depth={depth + 1}
volume={volume}
dirEntry={dirEntry}
setFileData={setFileData}
/>;
} else {
return <FileListing
key={idx}
depth={depth + 1}
volume={volume}
fileEntry={fileEntry}
setFileData={setFileData}
/>;
}
})}
{open &&
dirEntry.entries.map((fileEntry, idx) => {
if (fileEntry.storageType === STORAGE_TYPES.DIRECTORY) {
const dirEntry = new Directory(volume, fileEntry);
return (
<DirectoryListing
key={idx}
depth={depth + 1}
volume={volume}
dirEntry={dirEntry}
setFileData={setFileData}
/>
);
} else {
return (
<FileListing
key={idx}
depth={depth + 1}
volume={volume}
fileEntry={fileEntry}
setFileData={setFileData}
/>
);
}
})}
</>
);
};
@ -187,9 +219,12 @@ const CatalogEntry = ({ dos, fileEntry, setFileData }: CatalogEntryProps) => {
return (
<tr onClick={doSetFileData}>
<td className={cs(styles.filename, { [styles.deleted]: fileEntry.deleted })}>
{fileEntry.locked && <i className="fas fa-lock" />}
{' '}
<td
className={cs(styles.filename, {
[styles.deleted]: fileEntry.deleted,
})}
>
{fileEntry.locked && <i className="fas fa-lock" />}{' '}
{fileEntry.name}
</td>
<td>{fileEntry.type}</td>
@ -303,12 +338,20 @@ const DiskInfo = ({ massStorage, driveNo, setFileData }: DiskInfoProps) => {
<table>
<thead>
<tr>
<th className={styles.filename}>Filename</th>
<th className={styles.filename}>
Filename
</th>
<th className={styles.type}>Type</th>
<th className={styles.aux}>Aux</th>
<th className={styles.blocks}>Blocks</th>
<th className={styles.created}>Created</th>
<th className={styles.modified}>Modified</th>
<th className={styles.blocks}>
Blocks
</th>
<th className={styles.created}>
Created
</th>
<th className={styles.modified}>
Modified
</th>
</tr>
</thead>
<tbody>
@ -321,9 +364,13 @@ const DiskInfo = ({ massStorage, driveNo, setFileData }: DiskInfoProps) => {
</tbody>
<tfoot>
<tr>
<td colSpan={1}>Blocks Free: {freeCount}</td>
<td colSpan={1}>
Blocks Free: {freeCount}
</td>
<td colSpan={3}>Used: {usedCount}</td>
<td colSpan={2}>Total: {totalBlocks}</td>
<td colSpan={2}>
Total: {totalBlocks}
</td>
</tr>
</tfoot>
</table>
@ -337,7 +384,9 @@ const DiskInfo = ({ massStorage, driveNo, setFileData }: DiskInfoProps) => {
<table>
<thead>
<tr>
<th className={styles.filename}>Filename</th>
<th className={styles.filename}>
Filename
</th>
<th className={styles.type}>Type</th>
<th className={styles.sectors}>Sectors</th>
<th></th>
@ -409,11 +458,19 @@ export const Disks = ({ apple2 }: DisksProps) => {
<div className={debuggerStyles.subHeading}>
{card.constructor.name} - 1
</div>
<DiskInfo massStorage={card} driveNo={1} setFileData={setFileData} />
<DiskInfo
massStorage={card}
driveNo={1}
setFileData={setFileData}
/>
<div className={debuggerStyles.subHeading}>
{card.constructor.name} - 2
</div>
<DiskInfo massStorage={card} driveNo={2} setFileData={setFileData} />
<DiskInfo
massStorage={card}
driveNo={2}
setFileData={setFileData}
/>
</div>
))}
<FileViewer fileData={fileData} onClose={onClose} />

View File

@ -56,7 +56,14 @@ const HiresPreview = ({ binary }: { binary: Uint8Array }) => {
vm.blit();
}
return <canvas ref={canvasRef} width={560} height={384} className={styles.hiresPreview} />;
return (
<canvas
ref={canvasRef}
width={560}
height={384}
className={styles.hiresPreview}
/>
);
};
/**
@ -96,7 +103,14 @@ const DoubleHiresPreview = ({ binary }: { binary: Uint8Array }) => {
vm.blit();
}
return <canvas ref={canvasRef} width={560} height={384} className={styles.hiresPreview} />;
return (
<canvas
ref={canvasRef}
width={560}
height={384}
className={styles.hiresPreview}
/>
);
};
/**
@ -114,16 +128,14 @@ export const FileViewer = ({ fileData, onClose }: FileViewerProps) => {
useEffect(() => {
if (fileData) {
const { binary, text } = fileData;
const binaryBlob = new Blob(
[binary],
{ type: 'application/octet-stream' }
);
const binaryBlob = new Blob([binary], {
type: 'application/octet-stream',
});
const binaryHref = window.URL.createObjectURL(binaryBlob);
setBinaryHref(binaryHref);
const textBlob = new Blob(
[text],
{ type: 'application/octet-stream' }
);
const textBlob = new Blob([text], {
type: 'application/octet-stream',
});
const textHref = window.URL.createObjectURL(textBlob);
setTextHref(textHref);
}
@ -142,7 +154,7 @@ export const FileViewer = ({ fileData, onClose }: FileViewerProps) => {
<div className={styles.fileViewer}>
<HiresPreview binary={binary} />
<DoubleHiresPreview binary={binary} />
<pre className={styles.textViewer} tabIndex={-1} >
<pre className={styles.textViewer} tabIndex={-1}>
{text}
</pre>
</div>

View File

@ -54,8 +54,8 @@ interface Banks {
* @returns LC read/write state
*/
const calcLC = (mmu: MMU, altzp: boolean) => {
const read = mmu.readbsr && (mmu.altzp === altzp);
const write = mmu.writebsr && (mmu.altzp === altzp);
const read = mmu.readbsr && mmu.altzp === altzp;
const write = mmu.writebsr && mmu.altzp === altzp;
return {
read,
write,
@ -170,7 +170,7 @@ const calcLanguageCard = (card: LanguageCard): LC => {
rom: {
read: !card.readbsr,
write: !card.writebsr,
}
},
};
};
@ -207,16 +207,14 @@ interface LanguageCardMapProps {
const LanguageCardMap = ({ lc, children }: LanguageCardMapProps) => {
return (
<div className={cs(styles.bank)}>
<div className={cs(styles.lc, rw(lc))}>
{children} LC
</div>
<div className={cs(styles.lc, rw(lc))}>{children} LC</div>
<div className={styles.lcbanks}>
<div className={cs(styles.lcbank, styles.lcbank0, rw(lc.bank0))}>
<div
className={cs(styles.lcbank, styles.lcbank0, rw(lc.bank0))}
>
Bank 0
</div>
<div className={cs(styles.lcbank, rw(lc.bank1))}>
Bank 1
</div>
<div className={cs(styles.lcbank, rw(lc.bank1))}>Bank 1</div>
</div>
</div>
);
@ -238,10 +236,14 @@ const Legend = () => {
<div className={cs(styles.write, styles.legend)}> </div> Write
</div>
<div>
<div className={cs(styles.write, styles.read, styles.legend)}> </div> Read/Write
<div className={cs(styles.write, styles.read, styles.legend)}>
{' '}
</div>{' '}
Read/Write
</div>
<div>
<div className={cs(styles.inactive, styles.legend)}> </div> Inactive
<div className={cs(styles.inactive, styles.legend)}> </div>{' '}
Inactive
</div>
</div>
);
@ -291,12 +293,8 @@ export const Memory = ({ apple2 }: MemoryProps) => {
<div className={styles.memory}>
<div className={debuggerStyles.heading}>MMU</div>
<div className={cs(styles.upperMemory, debuggerStyles.row)}>
<LanguageCardMap lc={banks.aux.lc}>
Aux
</LanguageCardMap>
<LanguageCardMap lc={banks.main.lc}>
Main
</LanguageCardMap>
<LanguageCardMap lc={banks.aux.lc}>Aux</LanguageCardMap>
<LanguageCardMap lc={banks.main.lc}>Main</LanguageCardMap>
<div className={cs(styles.bank)}>
<div className={cs(styles.rom, rw(banks.main.lc.rom))}>
ROM
@ -304,9 +302,7 @@ export const Memory = ({ apple2 }: MemoryProps) => {
</div>
</div>
<div className={cs(debuggerStyles.row)}>
<div className={cs(styles.io, rw(banks.io))}>
IO
</div>
<div className={cs(styles.io, rw(banks.io))}>IO</div>
<div className={cs(styles.intcxrom, rw(banks.intcxrom))}>
CXROM
</div>
@ -348,9 +344,7 @@ export const Memory = ({ apple2 }: MemoryProps) => {
<div className={cs(debuggerStyles.row, styles.languageCard)}>
<LanguageCardMap lc={lc} />
<div className={cs(styles.bank)}>
<div className={cs(styles.rom, rw(lc.rom))}>
ROM
</div>
<div className={cs(styles.rom, rw(lc.rom))}>ROM</div>
</div>
</div>
<hr />

View File

@ -62,13 +62,21 @@ export const VideoModes = ({ apple2 }: VideoModesProps) => {
return (
<div className={styles.pages}>
<div className={debuggerStyles.row}>
<div className={cs(styles.page, { [styles.active]: (text || !hires) && !page2 })}>
<div
className={cs(styles.page, {
[styles.active]: (text || !hires) && !page2,
})}
>
<div className={debuggerStyles.heading}>
Text/Lores Page 1
</div>
<canvas width="560" height="192" ref={canvas1} />
</div>
<div className={cs(styles.page, { [styles.active]: (text || !hires) && page2 })}>
<div
className={cs(styles.page, {
[styles.active]: (text || !hires) && page2,
})}
>
<div className={debuggerStyles.heading}>
Text/Lores Page 2
</div>
@ -76,16 +84,20 @@ export const VideoModes = ({ apple2 }: VideoModesProps) => {
</div>
</div>
<div className={debuggerStyles.row}>
<div className={cs(styles.page, { [styles.active]: (!text && hires) && !page2 })}>
<div className={debuggerStyles.heading}>
Hires Page 1
</div>
<div
className={cs(styles.page, {
[styles.active]: !text && hires && !page2,
})}
>
<div className={debuggerStyles.heading}>Hires Page 1</div>
<canvas width="560" height="192" ref={canvas3} />
</div>
<div className={cs(styles.page, { [styles.active]: (!text && hires) && page2 })}>
<div className={debuggerStyles.heading}>
Hires Page 2
</div>
<div
className={cs(styles.page, {
[styles.active]: !text && hires && page2,
})}
>
<div className={debuggerStyles.heading}>Hires Page 2</div>
<canvas width="560" height="192" ref={canvas4} />
</div>
</div>

View File

@ -45,7 +45,7 @@
font-size: 12px;
}
.debugger input[type="text"] {
.debugger input[type='text'] {
border: 1px inset;
}

View File

@ -7,7 +7,10 @@ import { useEffect } from 'preact/hooks';
* @param key KeyboardEvent key value to match
* @param callback Invoked when key is pressed
*/
export const useHotKey = (key: string, callback: (ev: KeyboardEvent) => void) => {
export const useHotKey = (
key: string,
callback: (ev: KeyboardEvent) => void
) => {
useEffect(() => {
const onKeyDown = (ev: KeyboardEvent) => {
if (ev.key === key) {

View File

@ -46,10 +46,10 @@ export const getNameAndExtension = (url: string) => {
};
export const loadLocalFile = (
storage: MassStorage<FloppyFormat|BlockFormat>,
storage: MassStorage<FloppyFormat | BlockFormat>,
formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS,
driveNo: DriveNumber,
file: File,
file: File
) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
@ -80,7 +80,11 @@ export const loadLocalFile = (
* @param file Browser File object to load
* @returns true if successful
*/
export const loadLocalBlockFile = (smartPort: SmartPort, driveNo: DriveNumber, file: File) => {
export const loadLocalBlockFile = (
smartPort: SmartPort,
driveNo: DriveNumber,
file: File
) => {
return loadLocalFile(smartPort, BLOCK_FORMATS, driveNo, file);
};
@ -93,7 +97,11 @@ export const loadLocalBlockFile = (smartPort: SmartPort, driveNo: DriveNumber, f
* @param file Browser File object to load
* @returns true if successful
*/
export const loadLocalNibbleFile = (disk2: Disk2, driveNo: DriveNumber, file: File) => {
export const loadLocalNibbleFile = (
disk2: Disk2,
driveNo: DriveNumber,
file: File
) => {
return loadLocalFile(disk2, FLOPPY_FORMATS, driveNo, file);
};
@ -110,13 +118,13 @@ export const loadLocalNibbleFile = (disk2: Disk2, driveNo: DriveNumber, file: Fi
export const loadJSON = async (
disk2: Disk2,
driveNo: DriveNumber,
url: string,
url: string
) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Error loading: ${response.statusText}`);
}
const data = await response.json() as JSONDisk;
const data = (await response.json()) as JSONDisk;
if (!includes(FLOPPY_FORMATS, data.type)) {
throw new Error(`Type "${data.type}" not recognized.`);
}
@ -127,7 +135,7 @@ export const loadJSON = async (
export const loadHttpFile = async (
url: string,
signal?: AbortSignal,
onProgress?: ProgressCallback,
onProgress?: ProgressCallback
): Promise<ArrayBuffer> => {
const response = await fetch(url, signal ? { signal } : {});
if (!response.ok) {
@ -137,7 +145,10 @@ export const loadHttpFile = async (
throw new Error('Error loading: no body');
}
const reader = response.body.getReader();
const contentLength = parseInt(response.headers.get('content-length') || '0', 10);
const contentLength = parseInt(
response.headers.get('content-length') || '0',
10
);
let received = 0;
const chunks: Uint8Array[] = [];
@ -223,7 +234,7 @@ export const loadHttpUnknownFile = async (
driveNo: DriveNumber,
url: string,
signal?: AbortSignal,
onProgress?: ProgressCallback,
onProgress?: ProgressCallback
) => {
const data = await loadHttpFile(url, signal, onProgress);
const { name, ext } = getNameAndExtension(url);
@ -231,9 +242,17 @@ export const loadHttpUnknownFile = async (
};
export class SmartStorageBroker implements MassStorage<unknown> {
constructor(private disk2: Disk2, private smartPort: SmartPort) {}
constructor(
private disk2: Disk2,
private smartPort: SmartPort
) {}
setBinary(driveNo: DriveNumber, name: string, ext: string, data: ArrayBuffer): boolean {
setBinary(
driveNo: DriveNumber,
name: string,
ext: string,
data: ArrayBuffer
): boolean {
if (includes(DISK_FORMATS, ext)) {
if (data.byteLength >= 800 * 1024) {
if (includes(BLOCK_FORMATS, ext)) {
@ -265,7 +284,11 @@ export class SmartStorageBroker implements MassStorage<unknown> {
* @param debug Debugger object
* @returns resolves to true if successful
*/
export const loadLocalBinaryFile = (file: File, address: word, debug: Debugger) => {
export const loadLocalBinaryFile = (
file: File,
address: word,
debug: Debugger
) => {
return new Promise((resolve, _reject) => {
const fileReader = new FileReader();
fileReader.onload = function () {

View File

@ -2,42 +2,46 @@ import { JSX } from 'preact';
import { DeepMemberOf, KnownKeys } from '../../types';
export const SPECIAL_KEY_MAP = {
'Shift': 'SHIFT',
'Enter': 'RETURN',
'CapsLock': 'LOCK',
'Control': 'CTRL',
'Escape': 'ESC',
'Delete': 'RESET',
'Tab': 'TAB',
'Backspace': 'DELETE',
'ArrowUp': '&uarr;',
'ArrowDown': '&darr;',
'ArrowRight': '&rarr;',
'ArrowLeft': '&larr;',
Shift: 'SHIFT',
Enter: 'RETURN',
CapsLock: 'LOCK',
Control: 'CTRL',
Escape: 'ESC',
Delete: 'RESET',
Tab: 'TAB',
Backspace: 'DELETE',
ArrowUp: '&uarr;',
ArrowDown: '&darr;',
ArrowRight: '&rarr;',
ArrowLeft: '&larr;',
// UiKit symbols
'UIKeyInputLeftArrow': '&larr;',
'UIKeyInputRightArrow': '&rarr;',
'UIKeyInputUpArrow': '&uarr;',
'UIKeyInputDownArrow': '&darr;',
'UIKeyInputEscape': 'ESC',
UIKeyInputLeftArrow: '&larr;',
UIKeyInputRightArrow: '&rarr;',
UIKeyInputUpArrow: '&uarr;',
UIKeyInputDownArrow: '&darr;',
UIKeyInputEscape: 'ESC',
} as const;
export const isSpecialKey = (k: string): k is KnownKeys<typeof SPECIAL_KEY_MAP> => {
export const isSpecialKey = (
k: string
): k is KnownKeys<typeof SPECIAL_KEY_MAP> => {
return k in SPECIAL_KEY_MAP;
};
export const SPECIAL_KEY_CODE = {
'TAB': 9,
'RETURN': 13,
'ESC': 27,
TAB: 9,
RETURN: 13,
ESC: 27,
'&uarr;': 11,
'&darr;': 10,
'&rarr;': 21,
'&larr;': 8,
'DELETE': 127,
DELETE: 127,
} as const;
export const hasSpecialKeyCode = (k: string): k is KnownKeys<typeof SPECIAL_KEY_CODE> => {
export const hasSpecialKeyCode = (
k: string
): k is KnownKeys<typeof SPECIAL_KEY_CODE> => {
return k in SPECIAL_KEY_CODE;
};
@ -47,17 +51,74 @@ export const hasSpecialKeyCode = (k: string): k is KnownKeys<typeof SPECIAL_KEY_
export const keys2 = [
[
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', ':', '-', 'RESET'],
['ESC', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'REPT', 'RETURN'],
['CTRL', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', '&larr;', '&rarr;'],
[
'ESC',
'Q',
'W',
'E',
'R',
'T',
'Y',
'U',
'I',
'O',
'P',
'REPT',
'RETURN',
],
[
'CTRL',
'A',
'S',
'D',
'F',
'G',
'H',
'J',
'K',
'L',
';',
'&larr;',
'&rarr;',
],
['SHIFT', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', 'SHIFT'],
['POWER', '&nbsp;']
], [
['!', '"', '#', '$', '%', '&', '\'', '(', ')', '0', '*', '=', 'RESET'],
['ESC', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', '@', 'REPT', 'RETURN'],
['CTRL', 'A', 'S', 'D', 'F', 'BELL', 'H', 'J', 'K', 'L', '+', '&larr;', '&rarr;'],
['POWER', '&nbsp;'],
],
[
['!', '"', '#', '$', '%', '&', "'", '(', ')', '0', '*', '=', 'RESET'],
[
'ESC',
'Q',
'W',
'E',
'R',
'T',
'Y',
'U',
'I',
'O',
'@',
'REPT',
'RETURN',
],
[
'CTRL',
'A',
'S',
'D',
'F',
'BELL',
'H',
'J',
'K',
'L',
'+',
'&larr;',
'&rarr;',
],
['SHIFT', 'Z', 'X', 'C', 'V', 'B', '^', ']', '<', '>', '?', 'SHIFT'],
['POWER', '&nbsp;']
]
['POWER', '&nbsp;'],
],
] as const;
export type Key2 = DeepMemberOf<typeof keys2>;
@ -67,43 +128,154 @@ export type Key2 = DeepMemberOf<typeof keys2>;
*/
export const keys2e = [
[
['ESC', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 'DELETE'],
['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']', '\\'],
['CTRL', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', '"', 'RETURN'],
[
'ESC',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'0',
'-',
'=',
'DELETE',
],
[
'TAB',
'Q',
'W',
'E',
'R',
'T',
'Y',
'U',
'I',
'O',
'P',
'[',
']',
'\\',
],
[
'CTRL',
'A',
'S',
'D',
'F',
'G',
'H',
'J',
'K',
'L',
';',
'"',
'RETURN',
],
['SHIFT', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', 'SHIFT'],
['LOCK', '`', 'POW', 'OPEN_APPLE', '&nbsp;', 'CLOSED_APPLE', '&larr;', '&rarr;', '&darr;', '&uarr;']
], [
['ESC', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', 'DELETE'],
['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|'],
['CTRL', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '\'', 'RETURN'],
[
'LOCK',
'`',
'POW',
'OPEN_APPLE',
'&nbsp;',
'CLOSED_APPLE',
'&larr;',
'&rarr;',
'&darr;',
'&uarr;',
],
],
[
[
'ESC',
'!',
'@',
'#',
'$',
'%',
'^',
'&',
'*',
'(',
')',
'_',
'+',
'DELETE',
],
[
'TAB',
'Q',
'W',
'E',
'R',
'T',
'Y',
'U',
'I',
'O',
'P',
'{',
'}',
'|',
],
[
'CTRL',
'A',
'S',
'D',
'F',
'G',
'H',
'J',
'K',
'L',
':',
"'",
'RETURN',
],
['SHIFT', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', 'SHIFT'],
['CAPS', '~', 'POW', 'OPEN_APPLE', '&nbsp;', 'CLOSED_APPLE', '&larr;', '&rarr;', '&darr;', '&uarr;']
]
[
'CAPS',
'~',
'POW',
'OPEN_APPLE',
'&nbsp;',
'CLOSED_APPLE',
'&larr;',
'&rarr;',
'&darr;',
'&uarr;',
],
],
] as const;
/** Shifted */
const SHIFTED = {
'!' : '1' ,
'@' : '2' ,
'#' : '3' ,
'$' : '4' ,
'%' : '5' ,
'^' : '6' ,
'&' : '7' ,
'*' : '8' ,
'(' : '9' ,
')' : '0' ,
'_' : '-' ,
'+' : '=' ,
'{' : '[' ,
'}' : ']' ,
'|' : '\\',
':' : ';' ,
'\'' : '"',
'<' : ',' ,
'>' : '.' ,
'?' : '/' ,
'~' : '`' ,
'!': '1',
'@': '2',
'#': '3',
$: '4',
'%': '5',
'^': '6',
'&': '7',
'*': '8',
'(': '9',
')': '0',
_: '-',
'+': '=',
'{': '[',
'}': ']',
'|': '\\',
':': ';',
"'": '"',
'<': ',',
'>': '.',
'?': '/',
'~': '`',
} as const;
export const isShiftyKey = (k: string): k is KnownKeys<typeof SHIFTED> => {
@ -127,7 +299,11 @@ export type KeyFunction = (key: KeyboardEvent) => void;
* * `keyLabel`: the label on the keycap
* * `keyCode`: the corresponding byte for the Apple II
*/
export const mapKeyboardEvent = (event: KeyboardEvent, caps: boolean = false, control: boolean = false) => {
export const mapKeyboardEvent = (
event: KeyboardEvent,
caps: boolean = false,
control: boolean = false
) => {
let key: string;
if (isSpecialKey(event.key)) {
key = SPECIAL_KEY_MAP[event.key];
@ -149,17 +325,17 @@ export const mapKeyboardEvent = (event: KeyboardEvent, caps: boolean = false, co
let keyCode = 0xff;
if (hasSpecialKeyCode(key)) {
keyCode = SPECIAL_KEY_CODE[key];
} else if (key.length === 1) {
} else if (key.length === 1) {
keyCode = key.charCodeAt(0);
}
if ((caps || control) && keyCode >= 0x61 && keyCode <= 0x7A) {
if ((caps || control) && keyCode >= 0x61 && keyCode <= 0x7a) {
keyCode -= 0x20;
}
if (control && keyCode >= 0x40 && keyCode < 0x60) {
keyCode -= 0x40;
}
return { key, keyLabel, keyCode};
return { key, keyLabel, keyCode };
};
/**
@ -178,7 +354,7 @@ export const mapMouseEvent = (
shifted: boolean,
controlled: boolean,
caps: boolean,
e: boolean,
e: boolean
) => {
const keyLabel = event.currentTarget?.dataset.key2 ?? '';
let key = event.currentTarget?.dataset[shifted ? 'key2' : 'key1'] ?? '';
@ -222,10 +398,7 @@ export const mapMouseEvent = (
if (key.length === 1) {
if (controlled && key >= '@' && key <= '_') {
keyCode = key.charCodeAt(0) - 0x40;
} else if (
e && !shifted && !caps &&
key >= 'A' && key <= 'Z'
) {
} else if (e && !shifted && !caps && key >= 'A' && key <= 'Z') {
keyCode = key.charCodeAt(0) + 0x20;
} else {
keyCode = key.charCodeAt(0);
@ -241,7 +414,9 @@ export const mapMouseEvent = (
* @param inKeys keys2 or keys2e
* @returns Keys remapped
*/
export const keysAsTuples = (inKeys: typeof keys2e | typeof keys2): string[][][] => {
export const keysAsTuples = (
inKeys: typeof keys2e | typeof keys2
): string[][][] => {
const rows = [];
for (let idx = 0; idx < inKeys[0].length; idx++) {
const upper = inKeys[0][idx];

View File

@ -1,13 +1,16 @@
/**
* Converts a function type returning a `Promise` to a function type returning `void`.
*/
export type NoAwait<F extends (...args: unknown[]) => Promise<unknown>> =
(...args: Parameters<F>) => void;
export type NoAwait<F extends (...args: unknown[]) => Promise<unknown>> = (
...args: Parameters<F>
) => void;
/**
* Signals that the argument returns a `Promise` that is intentionally not being awaited.
*/
export function noAwait<F extends (...args: unknown[]) => Promise<unknown>>(f: F): NoAwait<F> {
export function noAwait<F extends (...args: unknown[]) => Promise<unknown>>(
f: F
): NoAwait<F> {
return f as NoAwait<F>;
}
@ -19,7 +22,9 @@ export function noAwait<F extends (...args: unknown[]) => Promise<unknown>>(f: F
* function to return `true`. This can be used in `useEffect` calls as the
* cleanup function.
*/
export function spawn(f: (abortSignal: AbortSignal) => Promise<unknown>): AbortController {
export function spawn(
f: (abortSignal: AbortSignal) => Promise<unknown>
): AbortController {
const abortController = new AbortController();
noAwait(f)(abortController.signal);
return abortController;

View File

@ -21,7 +21,7 @@ export const systemTypes: Record<string, Partial<SystemType>> = {
apple2e: {
rom: 'apple2e',
characterRom: 'apple2e_char',
enhanced: false
enhanced: false,
},
apple2rm: {
characterRom: 'rmfont_char',
@ -58,7 +58,7 @@ export const systemTypes: Record<string, Partial<SystemType>> = {
characterRom: 'pigfont_char',
e: false,
},
apple2lc:{
apple2lc: {
rom: 'fpbasic',
characterRom: 'apple2lc_char',
e: false,
@ -67,5 +67,5 @@ export const systemTypes: Record<string, Partial<SystemType>> = {
rom: 'fpbasic',
characterRom: 'apple2_char',
e: false,
}
},
} as const;

View File

@ -12,18 +12,20 @@ export interface DebuggerContainer {
type symbols = { [key: number]: string };
type breakpointFn = (info: DebugInfo) => boolean;
const alwaysBreak = (_info: DebugInfo) => { return true; };
const alwaysBreak = (_info: DebugInfo) => {
return true;
};
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' : '-',
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('');
export default class Debugger {
@ -33,7 +35,10 @@ export default class Debugger {
private breakpoints: Map<word, breakpointFn> = new Map();
private symbols: symbols = {};
constructor(private cpu: CPU6502, private container: DebuggerContainer) {}
constructor(
private cpu: CPU6502,
private container: DebuggerContainer
) {}
stepCycles(cycles: number) {
this.cpu.stepCyclesDebug(this.verbose ? 1 : cycles, () => {
@ -79,8 +84,7 @@ export default class Debugger {
this.cpu.setPC(address);
};
isRunning = () =>
this.container.isRunning();
isRunning = () => this.container.isRunning();
setVerbose = (verbose: boolean) => {
this.verbose = verbose;
@ -91,7 +95,10 @@ export default class Debugger {
};
getTrace = (count?: number) => {
return this.trace.slice(count ? -count : undefined).map(this.printDebugInfo).join('\n');
return this.trace
.slice(count ? -count : undefined)
.map(this.printDebugInfo)
.join('\n');
};
printTrace = (count?: number) => {
@ -105,7 +112,7 @@ export default class Debugger {
let max = 255;
let min = 0;
if (size) {
if ((sp - 3) >= (255 - size)) {
if (sp - 3 >= 255 - size) {
min = Math.max(255 - size + 1, 0);
} else {
max = Math.min(sp + size - 4, 255);
@ -117,7 +124,7 @@ export default class Debugger {
const isSP = addr === sp ? '*' : ' ';
const addrStr = `$${toHex(0x0100 + addr)}`;
const valStr = toHex(this.cpu.read(0x01, addr));
if (!size || ((sp + size > addr) && (addr > sp - size))) {
if (!size || (sp + size > addr && addr > sp - size)) {
stack.push(`${isSP} ${addrStr} ${valStr}`);
}
}
@ -134,7 +141,7 @@ export default class Debugger {
};
listBreakpoints = () => {
for(const [addr, fn] of this.breakpoints.entries()) {
for (const [addr, fn] of this.breakpoints.entries()) {
debug(toHex(addr, 4), fn);
}
};
@ -149,12 +156,13 @@ export default class Debugger {
return [
toHex(pc, 4),
'- ', symbol,
'- ',
symbol,
this.dumpRegisters(info),
' ',
this.dumpRawOp(cmd),
' ',
this.dumpOp(pc, cmd)
this.dumpOp(pc, cmd),
].join('');
};
@ -296,9 +304,8 @@ export default class Debugger {
const addr = (msb << 8) | lsb;
let val;
let off;
const toHexOrSymbol = (v: word, n?: number) => (
this.symbols[v] || ('$' + toHex(v, n))
);
const toHexOrSymbol = (v: word, n?: number) =>
this.symbols[v] || '$' + toHex(v, n);
let result = op.name + ' ';
switch (op.mode) {
@ -360,7 +367,10 @@ export default class Debugger {
off -= 256;
}
pc += off + 2;
result += `${toHexOrSymbol(val)},${toHexOrSymbol(pc, 4)} (${off})`;
result += `${toHexOrSymbol(val)},${toHexOrSymbol(
pc,
4
)} (${off})`;
break;
default:
break;

View File

@ -20,9 +20,9 @@ const OFFSETS = {
/** Header length (2 bytes) */
HEADER_LENGTH: 0x08,
/** Version number (2 bytes). (Version of what? Format? Image?). */
VERSION: 0x0A,
VERSION: 0x0a,
/** Image format ID (4 bytes) */
FORMAT: 0x0C,
FORMAT: 0x0c,
/** Flags and DOS 3.3 volume number */
FLAGS: 0x10,
/**
@ -40,7 +40,7 @@ const OFFSETS = {
* Length of disk data in bytes (4 bytes). (143,360 bytes for 5.25"
* floppies; 512 × blocks for ProDOS volumes.)
*/
DATA_LENGTH: 0x1C,
DATA_LENGTH: 0x1c,
/**
* Comment start in bytes from the beginning of the image file (4 bytes).
* Must be zero if there is no comment. The comment must come after the
@ -61,15 +61,15 @@ const OFFSETS = {
* Creator data length in bytes (4 bytes). Must be zero if there is no
* creator data.
*/
CREATOR_DATA_LENGTH: 0x2C,
CREATOR_DATA_LENGTH: 0x2c,
/** Padding (16 bytes). Must be zero. */
PADDING: 0x30,
} as const;
const FLAGS = {
READ_ONLY: 0x80000000,
READ_ONLY: 0x80000000,
VOLUME_VALID: 0x00000100,
VOLUME_MASK: 0x000000FF
VOLUME_MASK: 0x000000ff,
} as const;
export enum FORMAT {
@ -92,16 +92,22 @@ export interface HeaderData {
export function read2MGHeader(rawData: ArrayBuffer): HeaderData {
const prefix = new DataView(rawData);
const decoder = new TextDecoder('ascii');
const signature = decoder.decode(rawData.slice(OFFSETS.SIGNATURE, OFFSETS.SIGNATURE + 4));
const signature = decoder.decode(
rawData.slice(OFFSETS.SIGNATURE, OFFSETS.SIGNATURE + 4)
);
if (signature !== '2IMG') {
throw new Error(`Unrecognized 2mg signature: ${signature}`);
}
const creator = decoder.decode(rawData.slice(OFFSETS.CREATOR, OFFSETS.CREATOR + 4));
const creator = decoder.decode(
rawData.slice(OFFSETS.CREATOR, OFFSETS.CREATOR + 4)
);
const headerLength = prefix.getInt16(OFFSETS.HEADER_LENGTH, true);
if (headerLength !== 64) {
throw new Error(`2mg header length is incorrect ${headerLength} !== 64`);
throw new Error(
`2mg header length is incorrect ${headerLength} !== 64`
);
}
const format = prefix.getInt32(OFFSETS.FORMAT, true);
const format = prefix.getInt32(OFFSETS.FORMAT, true) as FORMAT;
const flags = prefix.getInt32(OFFSETS.FLAGS, true);
const blocks = prefix.getInt32(OFFSETS.BLOCKS, true);
const offset = prefix.getInt32(OFFSETS.DATA_OFFSET, true);
@ -109,43 +115,67 @@ export function read2MGHeader(rawData: ArrayBuffer): HeaderData {
const commentOffset = prefix.getInt32(OFFSETS.COMMENT, true);
const commentLength = prefix.getInt32(OFFSETS.COMMENT_LENGTH, true);
const creatorDataOffset = prefix.getInt32(OFFSETS.CREATOR_DATA, true);
const creatorDataLength = prefix.getInt32(OFFSETS.CREATOR_DATA_LENGTH, true);
const creatorDataLength = prefix.getInt32(
OFFSETS.CREATOR_DATA_LENGTH,
true
);
// Though the spec says that it should be zero if the format is not
// ProDOS, we don't check that since we know that it is violated.
// However we do check that it's correct if the image _is_ ProDOS.
if (format === FORMAT.ProDOS && blocks * 512 !== bytes) {
throw new Error(`2mg blocks does not match disk data length: ${blocks} * 512 !== ${bytes}`);
throw new Error(
`2mg blocks does not match disk data length: ${blocks} * 512 !== ${bytes}`
);
}
if (offset < headerLength) {
throw new Error(`2mg data offset is less than header length: ${offset} < ${headerLength}`);
throw new Error(
`2mg data offset is less than header length: ${offset} < ${headerLength}`
);
}
if (offset + bytes > prefix.byteLength) {
throw new Error(`2mg data extends beyond disk image: ${offset} + ${bytes} > ${prefix.byteLength}`);
throw new Error(
`2mg data extends beyond disk image: ${offset} + ${bytes} > ${prefix.byteLength}`
);
}
const dataEnd = offset + bytes;
if (commentOffset && commentOffset < dataEnd) {
throw new Error(`2mg comment is before the end of the disk data: ${commentOffset} < ${offset} + ${bytes}`);
throw new Error(
`2mg comment is before the end of the disk data: ${commentOffset} < ${offset} + ${bytes}`
);
}
const commentEnd = commentOffset ? commentOffset + commentLength : dataEnd;
if (commentEnd > prefix.byteLength) {
throw new Error(`2mg comment extends beyond disk image: ${commentEnd} > ${prefix.byteLength}`);
throw new Error(
`2mg comment extends beyond disk image: ${commentEnd} > ${prefix.byteLength}`
);
}
if (creatorDataOffset && creatorDataOffset < commentEnd) {
throw new Error(`2mg creator data is before the end of the comment: ${creatorDataOffset} < ${commentEnd}`);
throw new Error(
`2mg creator data is before the end of the comment: ${creatorDataOffset} < ${commentEnd}`
);
}
const creatorDataEnd = creatorDataOffset ? creatorDataOffset + creatorDataLength : commentEnd;
const creatorDataEnd = creatorDataOffset
? creatorDataOffset + creatorDataLength
: commentEnd;
if (creatorDataEnd > prefix.byteLength) {
throw new Error(`2mg creator data extends beyond disk image: ${creatorDataEnd} > ${prefix.byteLength}`);
throw new Error(
`2mg creator data extends beyond disk image: ${creatorDataEnd} > ${prefix.byteLength}`
);
}
const extras: { comment?: string; creatorData?: ReadonlyUint8Array } = {};
if (commentOffset) {
extras.comment = new TextDecoder('utf-8').decode(
new Uint8Array(rawData, commentOffset, commentLength));
new Uint8Array(rawData, commentOffset, commentLength)
);
}
if (creatorDataOffset) {
extras.creatorData = new Uint8Array(rawData, creatorDataOffset, creatorDataLength);
extras.creatorData = new Uint8Array(
rawData,
creatorDataOffset,
creatorDataLength
);
}
const readOnly = (flags & FLAGS.READ_ONLY) !== 0;
@ -161,7 +191,7 @@ export function read2MGHeader(rawData: ArrayBuffer): HeaderData {
offset,
readOnly,
volume,
...extras
...extras,
};
}
@ -177,7 +207,10 @@ export function read2MGHeader(rawData: ArrayBuffer): HeaderData {
* @returns 2mg prefix and suffix for creating a 2mg disk image
*/
export const create2MGFragments = (headerData: HeaderData | null, { blocks } : { blocks: number }) => {
export const create2MGFragments = (
headerData: HeaderData | null,
{ blocks }: { blocks: number }
) => {
if (!headerData) {
headerData = {
bytes: blocks * 512,
@ -197,7 +230,9 @@ export const create2MGFragments = (headerData: HeaderData | null, { blocks } : {
const prefix = new Uint8Array(64);
const prefixView = new DataView(prefix.buffer);
const volumeFlags = headerData.volume ? headerData.volume | FLAGS.VOLUME_VALID : 0;
const volumeFlags = headerData.volume
? headerData.volume | FLAGS.VOLUME_VALID
: 0;
const readOnlyFlag = headerData.readOnly ? FLAGS.READ_ONLY : 0;
const flags = volumeFlags | readOnlyFlag;
const prefixLength = prefix.length;
@ -252,8 +287,13 @@ export const create2MGFragments = (headerData: HeaderData | null, { blocks } : {
* @returns 2MS
*/
export const create2MGFromBlockDisk = (headerData: HeaderData | null, { blocks }: BlockDisk): ArrayBuffer => {
const { prefix, suffix } = create2MGFragments(headerData, { blocks: blocks.length });
export const create2MGFromBlockDisk = (
headerData: HeaderData | null,
{ blocks }: BlockDisk
): ArrayBuffer => {
const { prefix, suffix } = create2MGFragments(headerData, {
blocks: blocks.length,
});
const imageLength = prefix.length + blocks.length * 512 + suffix.length;
const byteArray = new Uint8Array(imageLength);
@ -292,7 +332,7 @@ export default function createDiskFrom2MG(options: DiskOptions) {
disk = Nibble(options);
break;
case FORMAT.DOS: // dsk
default: // Something hinky, assume 'dsk'
default: // Something hinky, assume 'dsk'
disk = DOS(options);
break;
}

View File

@ -4,7 +4,10 @@ import { DiskOptions, BlockDisk, ENCODING_BLOCK, BlockFormat } from './types';
* Returns a `Disk` object for a block volume with block-ordered data.
* @param options the disk image and options
*/
export default function createBlockDisk(fmt: BlockFormat, options: DiskOptions): BlockDisk {
export default function createBlockDisk(
fmt: BlockFormat,
options: DiskOptions
): BlockDisk {
const { rawData, readOnly, name } = options;
if (!rawData) {
@ -13,7 +16,7 @@ export default function createBlockDisk(fmt: BlockFormat, options: DiskOptions):
const blocks = [];
let offset = 0;
while (offset < rawData.byteLength) {
while (offset < rawData.byteLength) {
blocks.push(new Uint8Array(rawData.slice(offset, offset + 0x200)));
offset += 0x200;
}

View File

@ -1,6 +1,16 @@
import { includes, memory } from '../types';
import { base64_decode } from '../base64';
import { BitstreamFormat, DiskOptions, FloppyDisk, FloppyFormat, JSONDisk, NibbleDisk, NibbleFormat, NIBBLE_FORMATS, WozDisk } from './types';
import {
BitstreamFormat,
DiskOptions,
FloppyDisk,
FloppyFormat,
JSONDisk,
NibbleDisk,
NibbleFormat,
NIBBLE_FORMATS,
WozDisk,
} from './types';
import createDiskFrom2MG from './2mg';
import createDiskFromD13 from './d13';
import createDiskFromDOS from './do';
@ -9,12 +19,24 @@ import createDiskFromWoz from './woz';
import createDiskFromNibble from './nib';
/** Creates a `NibbleDisk` from the given format and options. */
export function createDisk(fmt: NibbleFormat, options: DiskOptions): NibbleDisk | null;
export function createDisk(
fmt: NibbleFormat,
options: DiskOptions
): NibbleDisk | null;
/** Creates a `WozDisk` from the given format and options. */
export function createDisk(fmt: BitstreamFormat, options: DiskOptions): WozDisk | null;
export function createDisk(
fmt: BitstreamFormat,
options: DiskOptions
): WozDisk | null;
/** Creates a `FloppyDisk` (either a `NibbleDisk` or a `WozDisk`) from the given format and options. */
export function createDisk(fmt: FloppyFormat, options: DiskOptions): FloppyDisk | null;
export function createDisk(fmt: FloppyFormat, options: DiskOptions): FloppyDisk | null {
export function createDisk(
fmt: FloppyFormat,
options: DiskOptions
): FloppyDisk | null;
export function createDisk(
fmt: FloppyFormat,
options: DiskOptions
): FloppyDisk | null {
let disk: FloppyDisk | null = null;
switch (fmt) {
@ -74,7 +96,7 @@ export function createDiskFromJsonDisk(disk: JSONDisk): NibbleDisk | null {
readOnly,
name,
side,
data: trackData
data: trackData,
} as DiskOptions;
return createDisk(fmt, options);
@ -82,4 +104,3 @@ export function createDiskFromJsonDisk(disk: JSONDisk): NibbleDisk | null {
return null;
}
}

View File

@ -14,7 +14,7 @@ export default function createDiskFromDOS13(options: DiskOptions) {
metadata: { name, side },
volume,
readOnly,
tracks: []
tracks: [],
};
if (!data && !rawData) {

View File

@ -41,13 +41,13 @@ export const VTOC_OFFSETS = {
export const CATALOG_OFFSETS = {
NEXT_CATALOG_TRACK: 0x01,
NEXT_CATALOG_SECTOR: 0x02,
ENTRY1: 0x0B,
ENTRY2: 0x2E,
ENTRY1: 0x0b,
ENTRY2: 0x2e,
ENTRY3: 0x51,
ENTRY4: 0x74,
ENTRY5: 0x97,
ENTRY6: 0xBA,
ENTRY7: 0xDD,
ENTRY6: 0xba,
ENTRY7: 0xdd,
} as const;
/**
@ -113,7 +113,7 @@ export interface FileData {
}
function isNibbleDisk(disk: NibbleDisk | MassStorageData): disk is NibbleDisk {
return !!((disk as NibbleDisk).encoding);
return !!(disk as NibbleDisk).encoding;
}
/**
@ -157,7 +157,9 @@ export class DOS33 {
} else {
const offset = track * 0x1000 + sector * 0x100;
// Slice new array so modifications to apply to original track
data = new Uint8Array(this.disk.data.slice(offset, offset + 0x100));
data = new Uint8Array(
this.disk.data.slice(offset, offset + 0x100)
);
}
}
return data;
@ -212,11 +214,11 @@ export class DOS33 {
const data = this.rwts(track, sector);
track = data[0x01];
sector = data[0x02];
let offset = 0x0C; // offset in data
let offset = 0x0c; // offset in data
while ((data[offset] || data[offset + 1]) && jdx < 121) {
fileTrackSectorList.push({
track: data[offset],
sector: data[offset + 1]
sector: data[offset + 1],
});
offset += 2;
jdx++;
@ -247,16 +249,18 @@ export class DOS33 {
case 'I':
case 'A':
offset = 2;
length = data[0] | data[1] << 8;
length = data[0] | (data[1] << 8);
break;
case 'T':
length = 0;
while (data[length]) { length++; }
while (data[length]) {
length++;
}
break;
case 'B':
offset = 4;
address = data[0] | data[1] << 8;
length = data[2] | data[3] << 8;
address = data[0] | (data[1] << 8);
length = data[2] | (data[3] << 8);
break;
}
@ -308,11 +312,12 @@ export class DOS33 {
* @returns count of free sectors
*/
freeSectorCount() {
return this.vtoc.trackSectorMap.reduce((count, flags) => (
count + flags.reduce((count, flag) => (
count + (flag ? 1 : 0)
), 0)
), 0);
return this.vtoc.trackSectorMap.reduce(
(count, flags) =>
count +
flags.reduce((count, flag) => count + (flag ? 1 : 0), 0),
0
);
}
/**
@ -321,11 +326,12 @@ export class DOS33 {
* @returns used sector count
*/
usedSectorCount() {
return this.vtoc.trackSectorMap.reduce((count, flags) => (
count + flags.reduce((count, flag) => (
count + (flag ? 0 : 1)
), 0)
), 0);
return this.vtoc.trackSectorMap.reduce(
(count, flags) =>
count +
flags.reduce((count, flag) => count + (flag ? 0 : 1), 0),
0
);
}
/**
@ -341,17 +347,14 @@ export class DOS33 {
switch (file.type) {
case 'A':
case 'I':
prefix = [
data.length % 0x100,
data.length >> 8
];
prefix = [data.length % 0x100, data.length >> 8];
break;
case 'B':
prefix = [
fileData.address % 0x100,
fileData.address >> 8,
data.length % 0x100,
data.length >> 8
data.length >> 8,
];
break;
}
@ -361,8 +364,11 @@ export class DOS33 {
const { sectorByteCount, trackSectorListSize } = this.vtoc;
const dataRequiredSectors = Math.ceil(data.length / sectorByteCount);
const fileSectorListRequiredSectors = Math.ceil(dataRequiredSectors / trackSectorListSize);
const requiredSectors = dataRequiredSectors + fileSectorListRequiredSectors;
const fileSectorListRequiredSectors = Math.ceil(
dataRequiredSectors / trackSectorListSize
);
const requiredSectors =
dataRequiredSectors + fileSectorListRequiredSectors;
let idx;
let sectors: TrackSector[] = [];
@ -401,13 +407,21 @@ export class DOS33 {
}
sectorData[0x05] = idx & 0xff;
sectorData[0x06] = idx >> 8;
for (jdx = 0; jdx < trackSectorListSize && jdx < sectors.length; jdx++) {
const offset = 0xC + jdx * 2;
for (
jdx = 0;
jdx < trackSectorListSize && jdx < sectors.length;
jdx++
) {
const offset = 0xc + jdx * 2;
sectorData[offset] = sectors[jdx].track;
sectorData[offset + 1] = sectors[jdx].sector;
}
lastTrackSectorList = sectorData;
this.rwts(sector.track, sector.sector, new Uint8Array(sectorData));
this.rwts(
sector.track,
sector.sector,
new Uint8Array(sectorData)
);
}
sector = sectors.shift() as TrackSector;
@ -441,7 +455,8 @@ export class DOS33 {
for (let idx = 0; idx < fileData.data.length; idx++) {
const char = fileData.data[idx] & 0x7f;
if (char < 0x20) {
if (char === 0xd) { // CR
if (char === 0xd) {
// CR
result += '\n';
} else {
result += `$${toHex(char)}`;
@ -452,25 +467,30 @@ export class DOS33 {
}
break;
case 'B':
default: {
result = '';
let hex = '';
let ascii = '';
for (let idx = 0; idx < fileData.data.length; idx++) {
const val = fileData.data[idx];
if (idx % 16 === 0) {
if (idx !== 0) {
result += `${hex} ${ascii}\n`;
default:
{
result = '';
let hex = '';
let ascii = '';
for (let idx = 0; idx < fileData.data.length; idx++) {
const val = fileData.data[idx];
if (idx % 16 === 0) {
if (idx !== 0) {
result += `${hex} ${ascii}\n`;
}
hex = '';
ascii = '';
result += `${toHex(fileData.address + idx, 4)}:`;
}
hex = '';
ascii = '';
result += `${toHex(fileData.address + idx, 4)}:`;
hex += ` ${toHex(val)}`;
ascii +=
(val & 0x7f) >= 0x20
? String.fromCharCode(val & 0x7f)
: '.';
}
hex += ` ${toHex(val)}`;
ascii += (val & 0x7f) >= 0x20 ? String.fromCharCode(val & 0x7f) : '.';
result += '\n';
}
result += '\n';
} break;
break;
}
return result;
}
@ -486,7 +506,7 @@ export class DOS33 {
this.vtoc = {
catalog: {
track: data[VTOC_OFFSETS.CATALOG_TRACK],
sector: data[VTOC_OFFSETS.CATALOG_SECTOR]
sector: data[VTOC_OFFSETS.CATALOG_SECTOR],
},
version: data[VTOC_OFFSETS.VERSION],
volume: data[VTOC_OFFSETS.VOLUME],
@ -495,9 +515,10 @@ export class DOS33 {
allocationDirection: data[VTOC_OFFSETS.ALLOCATION_DIRECTION],
trackCount: data[VTOC_OFFSETS.TRACK_COUNT],
sectorCount: data[VTOC_OFFSETS.SECTOR_COUNT],
sectorByteCount: data[VTOC_OFFSETS.SECTOR_BYTE_COUNT_LOW] |
sectorByteCount:
data[VTOC_OFFSETS.SECTOR_BYTE_COUNT_LOW] |
(data[VTOC_OFFSETS.SECTOR_BYTE_COUNT_HIGH] << 8),
trackSectorMap: []
trackSectorMap: [],
};
for (let idx = 0; idx < this.vtoc.trackCount; idx++) {
@ -532,8 +553,9 @@ export class DOS33 {
data[VTOC_OFFSETS.CATALOG_TRACK] = vtoc.catalog.track;
data[VTOC_OFFSETS.CATALOG_SECTOR] = vtoc.catalog.sector;
data[VTOC_OFFSETS.VERSION] = vtoc.version || 3;
data[VTOC_OFFSETS.VOLUME] = vtoc.volume || 0xFE;
data[VTOC_OFFSETS.TRACK_SECTOR_LIST_SIZE] = vtoc.trackSectorListSize || 0x7a;
data[VTOC_OFFSETS.VOLUME] = vtoc.volume || 0xfe;
data[VTOC_OFFSETS.TRACK_SECTOR_LIST_SIZE] =
vtoc.trackSectorListSize || 0x7a;
data[VTOC_OFFSETS.LAST_ALLOCATION_TRACK] = vtoc.lastAllocationTrack;
data[VTOC_OFFSETS.ALLOCATION_DIRECTION] = vtoc.allocationDirection;
data[VTOC_OFFSETS.TRACK_COUNT] = vtoc.trackCount;
@ -578,7 +600,11 @@ export class DOS33 {
catTrack = data[CATALOG_OFFSETS.NEXT_CATALOG_TRACK];
catSector = data[CATALOG_OFFSETS.NEXT_CATALOG_SECTOR];
for (let idx = CATALOG_OFFSETS.ENTRY1; idx < 0x100; idx += CATALOG_ENTRY_LENGTH) {
for (
let idx = CATALOG_OFFSETS.ENTRY1;
idx < 0x100;
idx += CATALOG_ENTRY_LENGTH
) {
const file: FileEntry = {
locked: false,
deleted: false,
@ -596,12 +622,13 @@ export class DOS33 {
file.trackSectorList = {
track: entry[CATALOG_ENTRY_OFFSETS.SECTOR_LIST_TRACK],
sector: entry[CATALOG_ENTRY_OFFSETS.SECTOR_LIST_SECTOR]
sector: entry[CATALOG_ENTRY_OFFSETS.SECTOR_LIST_SECTOR],
};
if (file.trackSectorList.track === 0xff) {
file.deleted = true;
file.trackSectorList.track = entry[CATALOG_ENTRY_OFFSETS.DELETED_FILE_TRACK];
file.trackSectorList.track =
entry[CATALOG_ENTRY_OFFSETS.DELETED_FILE_TRACK];
}
// Locked
@ -642,15 +669,20 @@ export class DOS33 {
str += ' ';
// Size
file.size = entry[CATALOG_ENTRY_OFFSETS.FILE_LENGTH_LOW] |
entry[CATALOG_ENTRY_OFFSETS.FILE_LENGTH_HIGH] << 8;
file.size =
entry[CATALOG_ENTRY_OFFSETS.FILE_LENGTH_LOW] |
(entry[CATALOG_ENTRY_OFFSETS.FILE_LENGTH_HIGH] << 8);
str += Math.floor(file.size / 100);
str += Math.floor(file.size / 10) % 10;
str += file.size % 10;
str += ' ';
// Filename
for (let jdx = CATALOG_ENTRY_OFFSETS.FILE_NAME; jdx < 0x21; jdx++) {
for (
let jdx = CATALOG_ENTRY_OFFSETS.FILE_NAME;
jdx < 0x21;
jdx++
) {
file.name += String.fromCharCode(entry[jdx] & 0x7f);
}
str += file.name;
@ -672,17 +704,25 @@ export class DOS33 {
while (catSector || catTrack) {
const data = this.rwts(catTrack, catSector);
for (let idx = CATALOG_OFFSETS.ENTRY1; idx < 0x100; idx += CATALOG_ENTRY_OFFSETS.SECTOR_LIST_TRACK) {
for (
let idx = CATALOG_OFFSETS.ENTRY1;
idx < 0x100;
idx += CATALOG_ENTRY_OFFSETS.SECTOR_LIST_TRACK
) {
const file = this.files.shift();
if (!file?.trackSectorList) {
continue;
}
data[idx + CATALOG_ENTRY_OFFSETS.SECTOR_LIST_TRACK] = file.trackSectorList.track;
data[idx + CATALOG_ENTRY_OFFSETS.SECTOR_LIST_SECTOR] = file.trackSectorList.sector;
data[idx + CATALOG_ENTRY_OFFSETS.SECTOR_LIST_TRACK] =
file.trackSectorList.track;
data[idx + CATALOG_ENTRY_OFFSETS.SECTOR_LIST_SECTOR] =
file.trackSectorList.sector;
data[idx + CATALOG_ENTRY_OFFSETS.FILE_TYPE] = file.locked ? 0x80 : 0x00;
data[idx + CATALOG_ENTRY_OFFSETS.FILE_TYPE] = file.locked
? 0x80
: 0x00;
// File type
switch (file.type) {
@ -706,12 +746,15 @@ export class DOS33 {
}
// Size
data[idx + CATALOG_ENTRY_OFFSETS.FILE_LENGTH_LOW] = file.size & 0xff;
data[idx + CATALOG_ENTRY_OFFSETS.FILE_LENGTH_HIGH] = file.size >> 8;
data[idx + CATALOG_ENTRY_OFFSETS.FILE_LENGTH_LOW] =
file.size & 0xff;
data[idx + CATALOG_ENTRY_OFFSETS.FILE_LENGTH_HIGH] =
file.size >> 8;
// Filename
for (let jdx = 0; jdx < 0x1E; jdx++) {
data[idx + CATALOG_ENTRY_OFFSETS.FILE_NAME + jdx] = file.name.charCodeAt(jdx) | 0x80;
for (let jdx = 0; jdx < 0x1e; jdx++) {
data[idx + CATALOG_ENTRY_OFFSETS.FILE_NAME + jdx] =
file.name.charCodeAt(jdx) | 0x80;
}
}
this.rwts(catTrack, catSector, data);

View File

@ -1,38 +1,48 @@
import { bit, byte, memory } from '../types';
import { base64_decode, base64_encode } from '../base64';
import { bytify, debug, toHex } from '../util';
import { NibbleDisk, ENCODING_NIBBLE, JSONDisk, isNibbleDiskFormat, SupportedSectors } from './types';
import {
NibbleDisk,
ENCODING_NIBBLE,
JSONDisk,
isNibbleDiskFormat,
SupportedSectors,
} from './types';
/**
* DOS 3.3 Physical sector order (index is physical sector, value is DOS sector).
*/
// prettier-ignore
export const DO = [
0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4,
0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF
0x0, 0x7, 0xe, 0x6, 0xd, 0x5, 0xc, 0x4,
0xb, 0x3, 0xa, 0x2, 0x9, 0x1, 0x8, 0xf,
] as const;
/**
* DOS 3.3 Logical sector order (index is DOS sector, value is physical sector).
*/
// prettier-ignore
export const _DO = [
0x0, 0xD, 0xB, 0x9, 0x7, 0x5, 0x3, 0x1,
0xE, 0xC, 0xA, 0x8, 0x6, 0x4, 0x2, 0xF
0x0, 0xd, 0xb, 0x9, 0x7, 0x5, 0x3, 0x1,
0xe, 0xc, 0xa, 0x8, 0x6, 0x4, 0x2, 0xf,
] as const;
/**
* ProDOS Physical sector order (index is physical sector, value is ProDOS sector).
*/
// prettier-ignore
export const PO = [
0x0, 0x8, 0x1, 0x9, 0x2, 0xa, 0x3, 0xb,
0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf
0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf,
] as const;
/**
* ProDOS Logical sector order (index is ProDOS sector, value is physical sector).
*/
// prettier-ignore
export const _PO = [
0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe,
0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf
0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf,
] as const;
/**
@ -40,13 +50,14 @@ export const _PO = [
* physical sector).
*/
export const D13O = [
0x0, 0xa, 0x7, 0x4, 0x1, 0xb, 0x8, 0x5, 0x2, 0xc, 0x9, 0x6, 0x3
0x0, 0xa, 0x7, 0x4, 0x1, 0xb, 0x8, 0x5, 0x2, 0xc, 0x9, 0x6, 0x3,
] as const;
export const _D13O = [
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc,
] as const;
// prettier-ignore
const TRANS53 = [
0xab, 0xad, 0xae, 0xaf, 0xb5, 0xb6, 0xb7, 0xba,
0xbb, 0xbd, 0xbe, 0xbf, 0xd6, 0xd7, 0xda, 0xdb,
@ -54,6 +65,7 @@ const TRANS53 = [
0xf5, 0xf6, 0xf7, 0xfa, 0xfb, 0xfd, 0xfe, 0xff
] as const;
// prettier-ignore
export const DETRANS53 = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // A0
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, // A8
@ -69,6 +81,7 @@ export const DETRANS53 = [
0x00, 0x00, 0x1B, 0x1C, 0x00, 0x1D, 0x1E, 0x1F, // F8
] as const;
// prettier-ignore
const TRANS62 = [
0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3,
@ -80,6 +93,7 @@ const TRANS62 = [
0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff
] as const;
// prettier-ignore
export const DETRANS62 = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@ -137,7 +151,12 @@ export function defourXfour(xx: byte, yy: byte): byte {
* @param data sector data
* @returns a nibblized representation of the sector data
*/
export function explodeSector16(volume: byte, track: byte, sector: byte, data: memory): byte[] {
export function explodeSector16(
volume: byte,
track: byte,
sector: byte,
data: memory
): byte[] {
let buf = [];
let gap;
@ -145,9 +164,11 @@ export function explodeSector16(volume: byte, track: byte, sector: byte, data: m
* Gap 1/3 (40/0x28 bytes)
*/
if (sector === 0) // Gap 1
if (sector === 0)
// Gap 1
gap = 0x80;
else { // Gap 3
else {
// Gap 3
gap = track === 0 ? 0x28 : 0x26;
}
@ -202,8 +223,7 @@ export function explodeSector16(volume: byte, track: byte, sector: byte, data: m
nibbles[ptr6 + idx6] = val6;
nibbles[ptr2 + idx2] = val2;
if (--idx2 < 0)
idx2 = 0x55;
if (--idx2 < 0) idx2 = 0x55;
}
let last = 0;
@ -235,7 +255,12 @@ export function explodeSector16(volume: byte, track: byte, sector: byte, data: m
* @param data sector data
* @returns a nibblized representation of the sector data
*/
export function explodeSector13(volume: byte, track: byte, sector: byte, data: memory): byte[] {
export function explodeSector13(
volume: byte,
track: byte,
sector: byte,
data: memory
): byte[] {
let buf = [];
let gap;
@ -243,9 +268,11 @@ export function explodeSector13(volume: byte, track: byte, sector: byte, data: m
* Gap 1/3 (40/0x28 bytes)
*/
if (sector === 0) // Gap 1
if (sector === 0)
// Gap 1
gap = 0x80;
else { // Gap 3
else {
// Gap 3
gap = track === 0 ? 0x28 : 0x26;
}
@ -303,9 +330,10 @@ export function explodeSector13(volume: byte, track: byte, sector: byte, data: m
nibbles[idx + 0x66] = c5;
nibbles[idx + 0x99] = d5;
nibbles[idx + 0xcc] = e5;
nibbles[idx + 0x100] = a3 << 2 | (d3 & 0x4) >> 1 | (e3 & 0x4) >> 2;
nibbles[idx + 0x133] = b3 << 2 | (d3 & 0x2) | (e3 & 0x2) >> 1;
nibbles[idx + 0x166] = c3 << 2 | (d3 & 0x1) << 1 | (e3 & 0x1);
nibbles[idx + 0x100] =
(a3 << 2) | ((d3 & 0x4) >> 1) | ((e3 & 0x4) >> 2);
nibbles[idx + 0x133] = (b3 << 2) | (d3 & 0x2) | ((e3 & 0x2) >> 1);
nibbles[idx + 0x166] = (c3 << 2) | ((d3 & 0x1) << 1) | (e3 & 0x1);
}
nibbles[0xff] = data[jdx] >> 3;
nibbles[0x199] = data[jdx] & 0x07;
@ -352,11 +380,13 @@ enum LookingFor {
}
export class FindSectorError extends Error {
constructor(track: byte, sector: byte, e: unknown | Error | string) {
super(`Error finding track ${track} (${toHex(track)}), sector ${sector} (${toHex(sector)}): `
+ (e instanceof Error
? `${e.message}`
: `${String(e)}`));
constructor(track: byte, sector: byte, e: unknown) {
super(
`Error finding track ${track} (${toHex(
track
)}), sector ${sector} (${toHex(sector)}): ` +
(e instanceof Error ? `${e.message}` : `${String(e)}`)
);
}
}
@ -372,7 +402,11 @@ export class FindSectorError extends Error {
* @param sector sector number to read
* @returns the track, sector, nibble offset, and detected sectors
*/
export function findSector(disk: NibbleDisk, track: byte, sector: byte): TrackNibble {
export function findSector(
disk: NibbleDisk,
track: byte,
sector: byte
): TrackNibble {
const cur = disk.tracks[track];
let sectors: SupportedSectors = 16;
let state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
@ -394,21 +428,26 @@ export function findSector(disk: NibbleDisk, track: byte, sector: byte): TrackNi
retry++;
}
}
let t = 0, s = 0, v = 0, checkSum;
let t = 0,
s = 0,
v = 0,
checkSum;
while (retry < 4) {
let val: byte;
switch (state) {
case LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE:
val = _readNext();
state = (val === 0xd5)
? LookingFor.START_OF_FIELD_MARKER_SECOND_NIBBLE
: LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
state =
val === 0xd5
? LookingFor.START_OF_FIELD_MARKER_SECOND_NIBBLE
: LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
break;
case LookingFor.START_OF_FIELD_MARKER_SECOND_NIBBLE:
val = _readNext();
state = (val === 0xaa)
? LookingFor.FIELD_TYPE_MARKER
: LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
state =
val === 0xaa
? LookingFor.FIELD_TYPE_MARKER
: LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
break;
case LookingFor.FIELD_TYPE_MARKER:
val = _readNext();
@ -417,12 +456,15 @@ export function findSector(disk: NibbleDisk, track: byte, sector: byte): TrackNi
state = LookingFor.ADDRESS_FIELD;
sectors = 16;
break;
case 0xB5:
case 0xb5:
state = LookingFor.ADDRESS_FIELD;
sectors = 13;
break;
case 0xAD:
state = sectors === 16 ? LookingFor.DATA_FIELD_6AND2 : LookingFor.DATA_FIELD_5AND3;
case 0xad:
state =
sectors === 16
? LookingFor.DATA_FIELD_6AND2
: LookingFor.DATA_FIELD_5AND3;
break;
default:
state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
@ -434,7 +476,13 @@ export function findSector(disk: NibbleDisk, track: byte, sector: byte): TrackNi
s = defourXfour(_readNext(), _readNext()); // Sector
checkSum = defourXfour(_readNext(), _readNext());
if (checkSum !== (v ^ t ^ s)) {
debug('Invalid header checksum:', toHex(v), toHex(t), toHex(s), toHex(checkSum));
debug(
'Invalid header checksum:',
toHex(v),
toHex(t),
toHex(s),
toHex(checkSum)
);
}
_skipBytes(3); // Skip footer
state = 0;
@ -454,12 +502,16 @@ export function findSector(disk: NibbleDisk, track: byte, sector: byte): TrackNi
if (!checkSum) {
return { track, sector, nibble, sectors };
} else {
debug('Invalid data checksum:', toHex(last), toHex(track), toHex(sector), toHex(checkSum));
debug(
'Invalid data checksum:',
toHex(last),
toHex(track),
toHex(sector),
toHex(checkSum)
);
}
_skipBytes(3); // Skip footer
}
else
_skipBytes(0x159); // Skip data, checksum and footer
} else _skipBytes(0x159); // Skip data, checksum and footer
state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
break;
case LookingFor.DATA_FIELD_5AND3:
@ -469,20 +521,25 @@ export function findSector(disk: NibbleDisk, track: byte, sector: byte): TrackNi
// Do checksum on data
let last = 0;
for (let jdx = 0; jdx < 0x19A; jdx++) {
last = DETRANS53[_readNext() - 0xA0] ^ last;
for (let jdx = 0; jdx < 0x19a; jdx++) {
last = DETRANS53[_readNext() - 0xa0] ^ last;
}
const checkSum = DETRANS53[_readNext() - 0xA0] ^ last;
const checkSum = DETRANS53[_readNext() - 0xa0] ^ last;
// Validate checksum before returning
if (!checkSum) {
return { track, sector, nibble, sectors };
} else {
debug('Invalid data checksum:', toHex(last), toHex(track), toHex(sector), toHex(checkSum));
debug(
'Invalid data checksum:',
toHex(last),
toHex(track),
toHex(sector),
toHex(checkSum)
);
}
_skipBytes(3); // Skip footer
}
else {
_skipBytes(0x19A); // Skip data, checksum and footer
} else {
_skipBytes(0x19a); // Skip data, checksum and footer
}
state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
break;
@ -501,23 +558,25 @@ export class InvalidChecksum extends Error {
}
export class ReadSectorError extends Error {
constructor(track: byte, sector: byte, e: unknown | Error) {
super(`Error reading track ${track} (${toHex(track)}), sector ${sector} (${toHex(sector)}): `
+ (e instanceof Error
? `${e.message}`
: `${String(e)}`));
constructor(track: byte, sector: byte, e: unknown) {
super(
`Error reading track ${track} (${toHex(
track
)}), sector ${sector} (${toHex(sector)}): ` +
(e instanceof Error ? `${e.message}` : `${String(e)}`)
);
}
}
/**
* Reads a sector of data from a nibblized disk. The sector given should be the
* "physical" sector number, meaning the one that appears in the address field.
* "physical" sector number, meaning the one that appears in the address field.
* Like `findSector`, the first sector with the right sector number and data
* whose checksum matches is returned. This means that for a dual-boot disk
* (DOS 3.2 and DOS 3.3), whichever sector is found first wins.
*
* This does not work for WOZ disks.
*
*
* If the given track and sector combination is not found, a `ReadSectorError`
* will be thrown.
*
@ -526,7 +585,11 @@ export class ReadSectorError extends Error {
* @param sector sector number to read
* @returns An array of sector data bytes.
*/
export function readSector(disk: NibbleDisk, track: byte, sector: byte): Uint8Array {
export function readSector(
disk: NibbleDisk,
track: byte,
sector: byte
): Uint8Array {
const trackNibble = findSector(disk, track, sector);
const { nibble, sectors } = trackNibble;
const cur = disk.tracks[track];
@ -541,7 +604,9 @@ export function readSector(disk: NibbleDisk, track: byte, sector: byte): Uint8Ar
};
try {
return sectors === 13 ? readSector13(_readNext) : readSector16(_readNext);
return sectors === 13
? readSector13(_readNext)
: readSector16(_readNext);
} catch (e: unknown) {
throw new ReadSectorError(track, sector, e);
}
@ -591,13 +656,13 @@ function readSector13(_readNext: () => byte) {
let last: byte = 0;
// special low 3-bits of 0xFF
val = DETRANS53[_readNext() - 0xA0] ^ last;
val = DETRANS53[_readNext() - 0xa0] ^ last;
last = val;
data[0xff] = val & 0b111;
// expect 0x99 nibbles of packed lower 3-bits in reverse order
for (let i = 0x98; i >= 0x00; i--) {
val = DETRANS53[_readNext() - 0xA0] ^ last;
val = DETRANS53[_readNext() - 0xa0] ^ last;
last = val;
const off = Math.floor(i / 0x33) + 5 * (0x32 - (i % 0x33));
const dOff = 3 + 5 * (0x32 - (i % 0x33));
@ -609,19 +674,19 @@ function readSector13(_readNext: () => byte) {
}
// expect 0xFE nibbles of upper 5-bits
for (let i = 0; i < 0xFF; i++) {
val = DETRANS53[_readNext() - 0xA0] ^ last;
for (let i = 0; i < 0xff; i++) {
val = DETRANS53[_readNext() - 0xa0] ^ last;
last = val;
const off = Math.floor(i / 0x33) + 5 * (0x32 - (i % 0x33));
data[off] ^= val << 3;
}
// and the last special nibble for 0xFF
val = DETRANS53[_readNext() - 0xA0] ^ last;
val = DETRANS53[_readNext() - 0xa0] ^ last;
last = val;
data[0xFF] ^= val << 3;
data[0xff] ^= val << 3;
const checkSum = DETRANS53[_readNext() - 0xA0] ^ last;
const checkSum = DETRANS53[_readNext() - 0xa0] ^ last;
if (checkSum) {
throw new InvalidChecksum(last, checkSum ^ last);
}
@ -638,7 +703,12 @@ function readSector13(_readNext: () => byte) {
* @param sector sector number to read
* @returns An array of sector data bytes.
*/
export function writeSector(disk: NibbleDisk, track: byte, sector: byte, _data: Uint8Array): boolean {
export function writeSector(
disk: NibbleDisk,
track: byte,
sector: byte,
_data: Uint8Array
): boolean {
const trackNibble = findSector(disk, track, sector);
if (!trackNibble) {
return false;
@ -669,17 +739,23 @@ export function jsonEncode(disk: NibbleDisk, pretty: boolean): string {
} else {
for (let s = 0; s < 0x10; s++) {
const _sector = disk.format === 'po' ? _PO[s] : _DO[s];
(data[t] as string[])[s] = base64_encode(readSector(disk, t, _sector));
(data[t] as string[])[s] = base64_encode(
readSector(disk, t, _sector)
);
}
}
}
return JSON.stringify({
'type': format,
'encoding': 'base64',
'volume': disk.volume,
'data': data,
'readOnly': disk.readOnly,
}, undefined, pretty ? ' ' : undefined);
return JSON.stringify(
{
type: format,
encoding: 'base64',
volume: disk.volume,
data: data,
readOnly: disk.readOnly,
},
undefined,
pretty ? ' ' : undefined
);
}
/**
@ -728,7 +804,8 @@ export function jsonDecode(data: string): NibbleDisk {
export function analyseDisk(disk: NibbleDisk) {
for (let track = 0; track < disk.tracks.length; track++) {
let outStr = `${toHex(track)}: `;
let val, state = 0;
let val,
state = 0;
let idx = 0;
const cur = disk.tracks[track];
@ -741,20 +818,23 @@ export function analyseDisk(disk: NibbleDisk) {
idx += count;
};
let t = 0, s = 0, v = 0, checkSum;
let t = 0,
s = 0,
v = 0,
checkSum;
while (idx < cur.length) {
switch (state) {
case 0:
val = _readNext();
state = (val === 0xd5) ? 1 : 0;
state = val === 0xd5 ? 1 : 0;
break;
case 1:
val = _readNext();
state = (val === 0xaa) ? 2 : 0;
state = val === 0xaa ? 2 : 0;
break;
case 2:
val = _readNext();
state = (val === 0x96) ? 3 : (val === 0xad ? 4 : 0);
state = val === 0x96 ? 3 : val === 0xad ? 4 : 0;
break;
case 3: // Address
v = defourXfour(_readNext(), _readNext()); // Volume
@ -762,7 +842,13 @@ export function analyseDisk(disk: NibbleDisk) {
s = defourXfour(_readNext(), _readNext());
checkSum = defourXfour(_readNext(), _readNext());
if (checkSum !== (v ^ t ^ s)) {
debug('Invalid header checksum:', toHex(v), toHex(t), toHex(s), toHex(checkSum));
debug(
'Invalid header checksum:',
toHex(v),
toHex(t),
toHex(s),
toHex(checkSum)
);
} else {
outStr += toHex(s, 1);
}
@ -813,6 +899,6 @@ export function grabNibble(bits: bit[], offset: number) {
return {
nibble: nibble,
offset: offset
offset: offset,
};
}

View File

@ -14,7 +14,7 @@ export default function createDiskFromNibble(options: DiskOptions): NibbleDisk {
metadata: { name, side },
volume: volume || 254,
readOnly: readOnly || false,
tracks: []
tracks: [],
};
for (let t = 0; t < 35; t++) {

View File

@ -7,7 +7,7 @@ export interface ProDOSFileData {
}
export abstract class ProDOSFile {
constructor(public volume: ProDOSVolume) { }
constructor(public volume: ProDOSVolume) {}
abstract read(): Uint8Array;
abstract write(data: Uint8Array): void;

View File

@ -29,10 +29,11 @@ export class BitMap {
if (bitOffset > 7) {
bitOffset = 0;
byteOffset += 1;
if (byteOffset > (BLOCK_ENTRIES >> 3)) {
if (byteOffset > BLOCK_ENTRIES >> 3) {
byteOffset = 0;
blockOffset += 1;
bitMapBlock = this.blocks[this.vdh.bitMapPointer + blockOffset];
bitMapBlock =
this.blocks[this.vdh.bitMapPointer + blockOffset];
}
}
}
@ -42,7 +43,8 @@ export class BitMap {
allocBlock() {
for (let idx = 0; idx < this.vdh.totalBlocks; idx++) {
const blockOffset = Math.floor(idx / BLOCK_ENTRIES);
const bitMapBlock = this.blocks[this.vdh.bitMapPointer + blockOffset];
const bitMapBlock =
this.blocks[this.vdh.bitMapPointer + blockOffset];
const byteOffset = (idx - blockOffset * BLOCK_ENTRIES) >> 8;
const bits = bitMapBlock[byteOffset];
if (bits !== 0xff) {

View File

@ -9,9 +9,9 @@ export const STORAGE_TYPES = {
TREE: 0x3,
PASCAL: 0x4,
EXTENDED: 0x5,
DIRECTORY: 0xD,
SUBDIRECTORY_HEADER: 0xE,
VDH_HEADER: 0xF
DIRECTORY: 0xd,
SUBDIRECTORY_HEADER: 0xe,
VDH_HEADER: 0xf,
} as const;
export const ACCESS_TYPES = {
@ -20,7 +20,7 @@ export const ACCESS_TYPES = {
BACKUP: 0x20,
WRITE: 0x02,
READ: 0x01,
ALL: 0xE3
ALL: 0xe3,
} as const;
export const FILE_TYPES: Record<byte, string> = {
@ -34,10 +34,10 @@ export const FILE_TYPES: Record<byte, string> = {
0x07: 'FNT', // Font file
0x08: 'FOT', // Graphics screen file
0x09: 'BA3', // Business BASIC program file
0x0A: 'DA3', // Business BASIC data file
0x0B: 'WPF', // Word Processor file
0x0C: 'SOS', // SOS system file
0x0F: 'DIR', // Directory file (SOS and ProDOS)
0x0a: 'DA3', // Business BASIC data file
0x0b: 'WPF', // Word Processor file
0x0c: 'SOS', // SOS system file
0x0f: 'DIR', // Directory file (SOS and ProDOS)
0x10: 'RPD', // RPS data file
0x11: 'RPI', // RPS index file
0x12: 'AFD', // AppleFile discard file
@ -45,14 +45,14 @@ export const FILE_TYPES: Record<byte, string> = {
0x14: 'ARF', // AppleFile report format file
0x15: 'SCL', // Screen Library file
0x19: 'ADB', // AppleWorks Data Base file
0x1A: 'AWP', // AppleWorks Word Processor file
0x1B: 'ASP', // AppleWorks Spreadsheet file
0xEF: 'PAR', // Pascal area
0xF0: 'CMD', // ProDOS CI added command file
0xFA: 'INT', // Integer BASIC program file
0xFB: 'IVR', // Integer BASIC variable file
0xFC: 'BAS', // Applesoft program file
0xFD: 'VAR', // Applesoft variables file
0xFE: 'REL', // Relocatable code file (EDASM)
0xFF: 'SYS' // ProDOS system file
0x1a: 'AWP', // AppleWorks Word Processor file
0x1b: 'ASP', // AppleWorks Spreadsheet file
0xef: 'PAR', // Pascal area
0xf0: 'CMD', // ProDOS CI added command file
0xfa: 'INT', // Integer BASIC program file
0xfb: 'IVR', // Integer BASIC variable file
0xfc: 'BAS', // Applesoft program file
0xfd: 'VAR', // Applesoft variables file
0xfe: 'REL', // Relocatable code file (EDASM)
0xff: 'SYS', // ProDOS system file
} as const;

View File

@ -1,5 +1,9 @@
import { dateToUint32, readFileName, writeFileName, uint32ToDate } from './utils';
import {
dateToUint32,
readFileName,
writeFileName,
uint32ToDate,
} from './utils';
import { FileEntry, readEntries, writeEntries } from './file_entry';
import { STORAGE_TYPES, ACCESS_TYPES } from './constants';
import { byte, word } from 'js/types';
@ -13,7 +17,7 @@ export const DIRECTORY_OFFSETS = {
NAME_LENGTH: 0x04,
DIRECTORY_NAME: 0x05,
RESERVED_1: 0x14,
CREATION: 0x1C,
CREATION: 0x1c,
CASE_BITS: 0x20,
VERSION: 0x20,
MIN_VERSION: 0x21,
@ -23,7 +27,7 @@ export const DIRECTORY_OFFSETS = {
FILE_COUNT: 0x25,
PARENT: 0x27,
PARENT_ENTRY_NUMBER: 0x29,
PARENT_ENTRY_LENGTH: 0x2A
PARENT_ENTRY_LENGTH: 0x2a,
} as const;
export class Directory {
@ -44,7 +48,10 @@ export class Directory {
parentEntryNumber: byte = 0;
entries: FileEntry[] = [];
constructor(private volume: ProDOSVolume, private fileEntry: FileEntry) {
constructor(
private volume: ProDOSVolume,
private fileEntry: FileEntry
) {
this.blocks = this.volume.blocks();
this.vdh = this.volume.vdh();
this.read();
@ -53,41 +60,78 @@ export class Directory {
read(fileEntry?: FileEntry) {
this.fileEntry = fileEntry ?? this.fileEntry;
const block = new DataView(this.blocks[this.fileEntry.keyPointer].buffer);
const block = new DataView(
this.blocks[this.fileEntry.keyPointer].buffer
);
this.prev = block.getUint16(DIRECTORY_OFFSETS.PREV, true);
this.next = block.getUint16(DIRECTORY_OFFSETS.NEXT, true);
this.storageType = block.getUint8(DIRECTORY_OFFSETS.STORAGE_TYPE) >> 4;
const nameLength = block.getUint8(DIRECTORY_OFFSETS.NAME_LENGTH) & 0xF;
const nameLength = block.getUint8(DIRECTORY_OFFSETS.NAME_LENGTH) & 0xf;
const caseBits = block.getUint8(DIRECTORY_OFFSETS.CASE_BITS);
this.name = readFileName(block, DIRECTORY_OFFSETS.DIRECTORY_NAME, nameLength, caseBits);
this.creation = uint32ToDate(block.getUint32(DIRECTORY_OFFSETS.CREATION, true));
this.name = readFileName(
block,
DIRECTORY_OFFSETS.DIRECTORY_NAME,
nameLength,
caseBits
);
this.creation = uint32ToDate(
block.getUint32(DIRECTORY_OFFSETS.CREATION, true)
);
this.access = block.getUint8(DIRECTORY_OFFSETS.ACCESS);
this.entryLength = block.getUint8(DIRECTORY_OFFSETS.ENTRY_LENGTH);
this.entriesPerBlock = block.getUint8(DIRECTORY_OFFSETS.ENTRIES_PER_BLOCK);
this.entriesPerBlock = block.getUint8(
DIRECTORY_OFFSETS.ENTRIES_PER_BLOCK
);
this.fileCount = block.getUint16(DIRECTORY_OFFSETS.FILE_COUNT, true);
this.parent = block.getUint16(DIRECTORY_OFFSETS.PARENT, true);
this.parentEntryNumber = block.getUint8(DIRECTORY_OFFSETS.PARENT_ENTRY_NUMBER);
this.parentEntryLength = block.getUint8(DIRECTORY_OFFSETS.PARENT_ENTRY_LENGTH);
this.parentEntryNumber = block.getUint8(
DIRECTORY_OFFSETS.PARENT_ENTRY_NUMBER
);
this.parentEntryLength = block.getUint8(
DIRECTORY_OFFSETS.PARENT_ENTRY_LENGTH
);
this.entries = readEntries(this.volume, block, this);
}
write() {
const block = new DataView(this.blocks[this.fileEntry.keyPointer].buffer);
const block = new DataView(
this.blocks[this.fileEntry.keyPointer].buffer
);
const nameLength = this.name.length & 0x0f;
block.setUint8(DIRECTORY_OFFSETS.STORAGE_TYPE, this.storageType << 4 & nameLength);
const caseBits = writeFileName(block, DIRECTORY_OFFSETS.DIRECTORY_NAME, this.name);
block.setUint32(DIRECTORY_OFFSETS.CREATION, dateToUint32(this.creation), true);
block.setUint8(
DIRECTORY_OFFSETS.STORAGE_TYPE,
(this.storageType << 4) & nameLength
);
const caseBits = writeFileName(
block,
DIRECTORY_OFFSETS.DIRECTORY_NAME,
this.name
);
block.setUint32(
DIRECTORY_OFFSETS.CREATION,
dateToUint32(this.creation),
true
);
block.setUint16(DIRECTORY_OFFSETS.CASE_BITS, caseBits);
block.setUint8(DIRECTORY_OFFSETS.ACCESS, this.access);
block.setUint8(DIRECTORY_OFFSETS.ENTRY_LENGTH, this.entryLength);
block.setUint8(DIRECTORY_OFFSETS.ENTRIES_PER_BLOCK, this.entriesPerBlock);
block.setUint8(
DIRECTORY_OFFSETS.ENTRIES_PER_BLOCK,
this.entriesPerBlock
);
block.setUint16(DIRECTORY_OFFSETS.FILE_COUNT, this.fileCount, true);
block.setUint16(DIRECTORY_OFFSETS.PARENT, this.parent, true);
block.setUint8(DIRECTORY_OFFSETS.PARENT_ENTRY_NUMBER, this.parentEntryNumber);
block.setUint8(DIRECTORY_OFFSETS.PARENT_ENTRY_LENGTH, this.parentEntryLength);
block.setUint8(
DIRECTORY_OFFSETS.PARENT_ENTRY_NUMBER,
this.parentEntryNumber
);
block.setUint8(
DIRECTORY_OFFSETS.PARENT_ENTRY_LENGTH,
this.parentEntryLength
);
writeEntries(this.volume, block, this.vdh);
}

View File

@ -1,4 +1,9 @@
import { dateToUint32, readFileName, writeFileName, uint32ToDate } from './utils';
import {
dateToUint32,
readFileName,
writeFileName,
uint32ToDate,
} from './utils';
import { STORAGE_TYPES, ACCESS_TYPES } from './constants';
import type { byte, word } from 'js/types';
import { toHex } from 'js/util';
@ -20,13 +25,13 @@ const ENTRY_OFFSETS = {
BLOCKS_USED: 0x13,
EOF: 0x15,
CREATION: 0x18,
CASE_BITS: 0x1C,
VERSION: 0x1C,
MIN_VERSION: 0x1D,
ACCESS: 0x1E,
AUX_TYPE: 0x1F,
CASE_BITS: 0x1c,
VERSION: 0x1c,
MIN_VERSION: 0x1d,
ACCESS: 0x1e,
AUX_TYPE: 0x1f,
LAST_MOD: 0x21,
HEADER_POINTER: 0x25
HEADER_POINTER: 0x25,
} as const;
export class FileEntry {
@ -45,28 +50,51 @@ export class FileEntry {
keyPointer: word = 0;
headerPointer: word = 0;
constructor(public volume: ProDOSVolume) { }
constructor(public volume: ProDOSVolume) {}
read(block: DataView, offset: word) {
this.block = block;
this.offset = offset;
this.storageType = block.getUint8(offset + ENTRY_OFFSETS.STORAGE_TYPE) >> 4;
const nameLength = block.getUint8(offset + ENTRY_OFFSETS.NAME_LENGTH) & 0xF;
const caseBits = block.getUint16(offset + ENTRY_OFFSETS.CASE_BITS, true);
this.name = readFileName(block, offset + ENTRY_OFFSETS.FILE_NAME, nameLength, caseBits);
this.storageType =
block.getUint8(offset + ENTRY_OFFSETS.STORAGE_TYPE) >> 4;
const nameLength =
block.getUint8(offset + ENTRY_OFFSETS.NAME_LENGTH) & 0xf;
const caseBits = block.getUint16(
offset + ENTRY_OFFSETS.CASE_BITS,
true
);
this.name = readFileName(
block,
offset + ENTRY_OFFSETS.FILE_NAME,
nameLength,
caseBits
);
this.fileType = block.getUint8(offset + ENTRY_OFFSETS.FILE_TYPE);
this.keyPointer = block.getUint16(offset + ENTRY_OFFSETS.KEY_POINTER, true);
this.blocksUsed = block.getUint16(offset + ENTRY_OFFSETS.BLOCKS_USED, true);
this.keyPointer = block.getUint16(
offset + ENTRY_OFFSETS.KEY_POINTER,
true
);
this.blocksUsed = block.getUint16(
offset + ENTRY_OFFSETS.BLOCKS_USED,
true
);
this.eof =
block.getUint8(offset + ENTRY_OFFSETS.EOF) |
block.getUint8(offset + ENTRY_OFFSETS.EOF + 1) << 8 |
block.getUint8(offset + ENTRY_OFFSETS.EOF + 2) << 16;
this.creation = uint32ToDate(block.getUint32(offset + ENTRY_OFFSETS.CREATION, true));
(block.getUint8(offset + ENTRY_OFFSETS.EOF + 1) << 8) |
(block.getUint8(offset + ENTRY_OFFSETS.EOF + 2) << 16);
this.creation = uint32ToDate(
block.getUint32(offset + ENTRY_OFFSETS.CREATION, true)
);
this.access = block.getUint8(offset + ENTRY_OFFSETS.ACCESS);
this.auxType = block.getUint16(offset + ENTRY_OFFSETS.AUX_TYPE, true);
this.lastMod = uint32ToDate(block.getUint32(offset + ENTRY_OFFSETS.LAST_MOD, true));
this.headerPointer = block.getUint16(offset + ENTRY_OFFSETS.HEADER_POINTER, true);
this.lastMod = uint32ToDate(
block.getUint32(offset + ENTRY_OFFSETS.LAST_MOD, true)
);
this.headerPointer = block.getUint16(
offset + ENTRY_OFFSETS.HEADER_POINTER,
true
);
}
write(block?: DataView, offset?: word) {
@ -74,20 +102,60 @@ export class FileEntry {
this.offset = offset ?? this.offset;
const nameLength = this.name.length & 0x0f;
this.block.setUint8(this.offset + ENTRY_OFFSETS.STORAGE_TYPE, this.storageType << 4 & nameLength);
const caseBits = writeFileName(this.block, this.offset + ENTRY_OFFSETS.FILE_NAME, this.name);
this.block.setUint8(
this.offset + ENTRY_OFFSETS.STORAGE_TYPE,
(this.storageType << 4) & nameLength
);
const caseBits = writeFileName(
this.block,
this.offset + ENTRY_OFFSETS.FILE_NAME,
this.name
);
this.block.setUint16(this.offset + ENTRY_OFFSETS.CASE_BITS, caseBits);
this.block.setUint8(this.offset + ENTRY_OFFSETS.FILE_TYPE, this.fileType);
this.block.setUint16(this.offset + ENTRY_OFFSETS.KEY_POINTER, this.keyPointer, true);
this.block.setUint16(this.offset + ENTRY_OFFSETS.BLOCKS_USED, this.blocksUsed, true);
this.block.setUint8(
this.offset + ENTRY_OFFSETS.FILE_TYPE,
this.fileType
);
this.block.setUint16(
this.offset + ENTRY_OFFSETS.KEY_POINTER,
this.keyPointer,
true
);
this.block.setUint16(
this.offset + ENTRY_OFFSETS.BLOCKS_USED,
this.blocksUsed,
true
);
this.block.setUint8(this.offset + ENTRY_OFFSETS.EOF, this.eof & 0xff);
this.block.setUint8(this.offset + ENTRY_OFFSETS.EOF + 1, (this.eof && 0xff00) >> 8);
this.block.setUint8(this.offset + ENTRY_OFFSETS.EOF + 2, this.eof >> 16);
this.block.setUint32(this.offset + ENTRY_OFFSETS.CREATION, dateToUint32(this.creation), true);
this.block.setUint8(
this.offset + ENTRY_OFFSETS.EOF + 1,
(this.eof && 0xff00) >> 8
);
this.block.setUint8(
this.offset + ENTRY_OFFSETS.EOF + 2,
this.eof >> 16
);
this.block.setUint32(
this.offset + ENTRY_OFFSETS.CREATION,
dateToUint32(this.creation),
true
);
this.block.setUint8(this.offset + ENTRY_OFFSETS.ACCESS, this.access);
this.block.setUint16(this.offset + ENTRY_OFFSETS.AUX_TYPE, this.auxType, true);
this.block.setUint32(this.offset + ENTRY_OFFSETS.LAST_MOD, dateToUint32(this.lastMod), true);
this.block.setUint16(this.offset + ENTRY_OFFSETS.HEADER_POINTER, this.headerPointer, true);
this.block.setUint16(
this.offset + ENTRY_OFFSETS.AUX_TYPE,
this.auxType,
true
);
this.block.setUint32(
this.offset + ENTRY_OFFSETS.LAST_MOD,
dateToUint32(this.lastMod),
true
);
this.block.setUint16(
this.offset + ENTRY_OFFSETS.HEADER_POINTER,
this.headerPointer,
true
);
}
getFileData() {
@ -116,10 +184,12 @@ export class FileEntry {
let address = 0;
if (data) {
if (this.fileType === 0xFC) { // BAS
if (this.fileType === 0xfc) {
// BAS
result = new ApplesoftDump(data, 0).decompile();
} else {
if (this.fileType === 0x06) { // BIN
if (this.fileType === 0x06) {
// BIN
address = this.auxType;
}
result = '';
@ -136,7 +206,10 @@ export class FileEntry {
result += `${toHex(address + idx, 4)}:`;
}
hex += ` ${toHex(val)}`;
ascii += (val & 0x7f) >= 0x20 ? String.fromCharCode(val & 0x7f) : '.';
ascii +=
(val & 0x7f) >= 0x20
? String.fromCharCode(val & 0x7f)
: '.';
}
result += '\n';
}
@ -145,14 +218,18 @@ export class FileEntry {
}
}
export function readEntries(volume: ProDOSVolume, block: DataView, header: VDH | Directory) {
export function readEntries(
volume: ProDOSVolume,
block: DataView,
header: VDH | Directory
) {
const blocks = volume.blocks();
const entries = [];
let offset = header.entryLength + 0x4;
let count = 2;
let next = header.next;
for (let idx = 0; idx < header.fileCount;) {
for (let idx = 0; idx < header.fileCount; ) {
const fileEntry = new FileEntry(volume);
fileEntry.read(block, offset);
entries.push(fileEntry);
@ -172,7 +249,11 @@ export function readEntries(volume: ProDOSVolume, block: DataView, header: VDH |
return entries;
}
export function writeEntries(volume: ProDOSVolume, block: DataView, header: VDH | Directory) {
export function writeEntries(
volume: ProDOSVolume,
block: DataView,
header: VDH | Directory
) {
const blocks = volume.blocks();
const bitMap = volume.bitMap();
let offset = header.entryLength + 0x4;

View File

@ -8,7 +8,10 @@ export class SaplingFile extends ProDOSFile {
blocks: Uint8Array[];
bitMap: BitMap;
constructor(volume: ProDOSVolume, private fileEntry: FileEntry) {
constructor(
volume: ProDOSVolume,
private fileEntry: FileEntry
) {
super(volume);
this.blocks = this.volume.blocks();
this.bitMap = this.volume.bitMap();
@ -45,7 +48,10 @@ export class SaplingFile extends ProDOSFile {
(seedlingPointers.getUint8(0x100 + idx) << 8);
if (seedlingPointer) {
const seedlingBlock = this.blocks[seedlingPointer];
const bytes = seedlingBlock.slice(0, Math.min(BLOCK_SIZE, remainingLength));
const bytes = seedlingBlock.slice(
0,
Math.min(BLOCK_SIZE, remainingLength)
);
data.set(bytes, offset);
}
@ -72,7 +78,9 @@ export class SaplingFile extends ProDOSFile {
seedlingPointers.setUint8(idx, seedlingPointer & 0xff);
seedlingPointers.setUint8(0x100 + idx, seedlingPointer >> 8);
const seedlingBlock = this.blocks[seedlingPointer];
seedlingBlock.set(data.slice(offset, Math.min(BLOCK_SIZE, remainingLength)));
seedlingBlock.set(
data.slice(offset, Math.min(BLOCK_SIZE, remainingLength))
);
idx++;
offset += BLOCK_SIZE;
remainingLength -= BLOCK_SIZE;
@ -87,4 +95,3 @@ export class SaplingFile extends ProDOSFile {
}
}
}

View File

@ -8,7 +8,10 @@ export class SeedlingFile extends ProDOSFile {
blocks: Uint8Array[];
bitMap: BitMap;
constructor(volume: ProDOSVolume, private fileEntry: FileEntry) {
constructor(
volume: ProDOSVolume,
private fileEntry: FileEntry
) {
super(volume);
this.blocks = volume.blocks();
this.bitMap = volume.bitMap();
@ -45,4 +48,3 @@ export class SeedlingFile extends ProDOSFile {
}
}
}

View File

@ -8,7 +8,10 @@ export class TreeFile extends ProDOSFile {
private bitMap: BitMap;
private blocks: Uint8Array[];
constructor(volume: ProDOSVolume, private fileEntry: FileEntry) {
constructor(
volume: ProDOSVolume,
private fileEntry: FileEntry
) {
super(volume);
this.blocks = volume.blocks();
this.bitMap = volume.bitMap();
@ -24,7 +27,9 @@ export class TreeFile extends ProDOSFile {
(saplingPointers.getUint8(0x100 + idx) << 8);
if (saplingPointer) {
pointers.push(saplingPointer);
const seedlingPointers = new DataView(this.blocks[saplingPointer]);
const seedlingPointers = new DataView(
this.blocks[saplingPointer]
);
for (let jdx = 0; jdx < 256; jdx++) {
const seedlingPointer =
seedlingPointers.getUint8(idx) |
@ -62,7 +67,9 @@ export class TreeFile extends ProDOSFile {
(seedlingPointers.getUint8(0x100 + idx) << 8);
if (seedlingPointer) {
const seedlingBlock = this.blocks[seedlingPointer];
const bytes = seedlingBlock.slice(Math.min(BLOCK_SIZE, remainingLength));
const bytes = seedlingBlock.slice(
Math.min(BLOCK_SIZE, remainingLength)
);
data.set(bytes, offset);
}
@ -105,7 +112,9 @@ export class TreeFile extends ProDOSFile {
seedlingPointers.setUint8(idx, seedlingPointer & 0xff);
seedlingPointers.setUint8(0x100 + idx, seedlingPointer >> 8);
const seedlingBlock = this.blocks[seedlingPointer];
seedlingBlock.set(data.slice(offset, Math.min(BLOCK_SIZE, remainingLength)));
seedlingBlock.set(
data.slice(offset, Math.min(BLOCK_SIZE, remainingLength))
);
jdx++;
offset += BLOCK_SIZE;
remainingLength -= BLOCK_SIZE;
@ -122,4 +131,3 @@ export class TreeFile extends ProDOSFile {
}
}
}

View File

@ -12,13 +12,19 @@ export function uint32ToDate(val: word) {
const hourMinute = val >> 16;
const year = yearMonthDay >> 9;
const month = (yearMonthDay & 0x01E0) >> 5;
const day = yearMonthDay & 0x001F;
const month = (yearMonthDay & 0x01e0) >> 5;
const day = yearMonthDay & 0x001f;
const hour = hourMinute >> 8;
const min = hourMinute & 0xff;
return new Date(year < 70 ? 2000 + year : 1900 + year, month - 1, day, hour, min);
return new Date(
year < 70 ? 2000 + year : 1900 + year,
month - 1,
day,
hour,
min
);
}
return new Date(0);
}
@ -36,15 +42,20 @@ export function dateToUint32(date: Date) {
const hour = date.getHours();
const min = date.getMinutes();
const yearMonthDay = year << 9 | month << 5 | day;
const hourMinute = hour << 8 | min;
val = hourMinute << 16 | yearMonthDay;
const yearMonthDay = (year << 9) | (month << 5) | day;
const hourMinute = (hour << 8) | min;
val = (hourMinute << 16) | yearMonthDay;
}
return val;
}
export function readFileName(block: DataView, offset: word, nameLength: byte, caseBits: word) {
export function readFileName(
block: DataView,
offset: word,
nameLength: byte,
caseBits: word
) {
let name = '';
if (!(caseBits & 0x8000)) {
caseBits = 0;
@ -66,7 +77,7 @@ export function writeFileName(block: DataView, offset: word, name: string) {
for (let idx = 0; idx < name.length; idx++) {
caseBits <<= 1;
let charCode = name.charCodeAt(idx);
if (charCode > 0x60 && charCode < 0x7B) {
if (charCode > 0x60 && charCode < 0x7b) {
caseBits |= 0x1;
charCode -= 0x20;
}
@ -75,7 +86,11 @@ export function writeFileName(block: DataView, offset: word, name: string) {
return caseBits;
}
export function dumpDirectory(volume: ProDOSVolume, dirEntry: FileEntry, depth: string) {
export function dumpDirectory(
volume: ProDOSVolume,
dirEntry: FileEntry,
depth: string
) {
const dir = new Directory(volume, dirEntry);
let str = '';

View File

@ -1,4 +1,9 @@
import { dateToUint32, readFileName, writeFileName, uint32ToDate } from './utils';
import {
dateToUint32,
readFileName,
writeFileName,
uint32ToDate,
} from './utils';
import { FileEntry, readEntries, writeEntries } from './file_entry';
import { STORAGE_TYPES, ACCESS_TYPES } from './constants';
import { byte, word } from 'js/types';
@ -12,8 +17,8 @@ const VDH_OFFSETS = {
NAME_LENGTH: 0x04,
VOLUME_NAME: 0x05,
RESERVED_1: 0x14,
CASE_BITS: 0x1A,
CREATION: 0x1C,
CASE_BITS: 0x1a,
CREATION: 0x1c,
VERSION: 0x20,
MIN_VERSION: 0x21,
ACCESS: 0x22,
@ -44,16 +49,22 @@ export class VDH {
this.blocks = this.volume.blocks();
}
read() {
const block = new DataView(this.blocks[VDH_BLOCK].buffer);
this.next = block.getUint16(VDH_OFFSETS.NEXT, true);
this.storageType = block.getUint8(VDH_OFFSETS.STORAGE_TYPE) >> 4;
const nameLength = block.getUint8(VDH_OFFSETS.NAME_LENGTH) & 0xF;
const nameLength = block.getUint8(VDH_OFFSETS.NAME_LENGTH) & 0xf;
const caseBits = block.getUint8(VDH_OFFSETS.CASE_BITS);
this.name = readFileName(block, VDH_OFFSETS.VOLUME_NAME, nameLength, caseBits);
this.creation = uint32ToDate(block.getUint32(VDH_OFFSETS.CREATION, true));
this.name = readFileName(
block,
VDH_OFFSETS.VOLUME_NAME,
nameLength,
caseBits
);
this.creation = uint32ToDate(
block.getUint32(VDH_OFFSETS.CREATION, true)
);
this.access = block.getUint8(VDH_OFFSETS.ACCESS);
this.entryLength = block.getUint8(VDH_OFFSETS.ENTRY_LENGTH);
this.entriesPerBlock = block.getUint8(VDH_OFFSETS.ENTRIES_PER_BLOCK);
@ -68,9 +79,20 @@ export class VDH {
const block = new DataView(this.blocks[VDH_BLOCK].buffer);
const nameLength = this.name.length & 0x0f;
block.setUint8(VDH_OFFSETS.STORAGE_TYPE, this.storageType << 4 & nameLength);
const caseBits = writeFileName(block, VDH_OFFSETS.VOLUME_NAME, this.name);
block.setUint32(VDH_OFFSETS.CREATION, dateToUint32(this.creation), true);
block.setUint8(
VDH_OFFSETS.STORAGE_TYPE,
(this.storageType << 4) & nameLength
);
const caseBits = writeFileName(
block,
VDH_OFFSETS.VOLUME_NAME,
this.name
);
block.setUint32(
VDH_OFFSETS.CREATION,
dateToUint32(this.creation),
true
);
block.setUint16(VDH_OFFSETS.CASE_BITS, caseBits);
block.setUint8(VDH_OFFSETS.ACCESS, this.access);
block.setUint8(VDH_OFFSETS.ENTRY_LENGTH, this.entryLength);

View File

@ -70,7 +70,10 @@ export const ENCODING_BITSTREAM = 'bitstream';
export const ENCODING_BLOCK = 'block';
export interface FloppyDisk extends Disk {
encoding: typeof ENCODING_NIBBLE | typeof ENCODING_BITSTREAM | typeof NO_DISK;
encoding:
| typeof ENCODING_NIBBLE
| typeof ENCODING_BITSTREAM
| typeof NO_DISK;
}
export interface NoFloppyDisk extends FloppyDisk {
@ -101,21 +104,12 @@ export interface BlockDisk extends Disk {
/**
* File types supported by floppy devices in nibble mode.
*/
export const NIBBLE_FORMATS = [
'2mg',
'd13',
'do',
'dsk',
'po',
'nib',
] as const;
export const NIBBLE_FORMATS = ['2mg', 'd13', 'do', 'dsk', 'po', 'nib'] as const;
/**
* File types supported by floppy devices in bitstream mode.
*/
export const BITSTREAM_FORMATS = [
'woz',
] as const;
export const BITSTREAM_FORMATS = ['woz'] as const;
/**
* All file types supported by floppy devices.
@ -128,19 +122,12 @@ export const FLOPPY_FORMATS = [
/**
* File types supported by block devices.
*/
export const BLOCK_FORMATS = [
'2mg',
'hdv',
'po',
] as const;
export const BLOCK_FORMATS = ['2mg', 'hdv', 'po'] as const;
/**
* All supported disk formats.
*/
export const DISK_FORMATS = [
...FLOPPY_FORMATS,
...BLOCK_FORMATS,
] as const;
export const DISK_FORMATS = [...FLOPPY_FORMATS, ...BLOCK_FORMATS] as const;
export type FloppyFormat = MemberOf<typeof FLOPPY_FORMATS>;
export type NibbleFormat = MemberOf<typeof NIBBLE_FORMATS>;
@ -259,9 +246,9 @@ export interface ProcessJsonMessage {
}
export type FormatWorkerMessage =
ProcessBinaryMessage |
ProcessJsonDiskMessage |
ProcessJsonMessage;
| ProcessBinaryMessage
| ProcessJsonDiskMessage
| ProcessJsonMessage;
/**
* Format work result message type
@ -277,8 +264,7 @@ export interface DiskProcessedResponse {
};
}
export type FormatWorkerResponse =
DiskProcessedResponse;
export type FormatWorkerResponse = DiskProcessedResponse;
export interface MassStorageData {
metadata: DiskMetadata;

View File

@ -6,8 +6,8 @@ import { DiskOptions, ENCODING_BITSTREAM, WozDisk } from './types';
const WOZ_HEADER_START = 0;
const WOZ_HEADER_SIZE = 12;
const WOZ1_SIGNATURE = 0x315A4F57;
const WOZ2_SIGNATURE = 0x325A4F57;
const WOZ1_SIGNATURE = 0x315a4f57;
const WOZ2_SIGNATURE = 0x325a4f57;
const WOZ_INTEGRITY_CHECK = 0x0a0d0aff;
/**
@ -89,24 +89,37 @@ export class TrksChunk1 extends TrksChunk {
this.rawTracks = [];
this.tracks = [];
for (let trackNo = 0, idx = 0; idx < data.byteLength; idx += WOZ_TRACK_SIZE, trackNo++) {
for (
let trackNo = 0, idx = 0;
idx < data.byteLength;
idx += WOZ_TRACK_SIZE, trackNo++
) {
let track = [];
const rawTrack: bit[] = [];
const slice = data.buffer.slice(data.byteOffset + idx, data.byteOffset + idx + WOZ_TRACK_SIZE);
const slice = data.buffer.slice(
data.byteOffset + idx,
data.byteOffset + idx + WOZ_TRACK_SIZE
);
const trackData = new Uint8Array(slice);
const trackInfo = new DataView(slice);
const trackBitCount = trackInfo.getUint16(WOZ_TRACK_INFO_BITS, true);
const trackBitCount = trackInfo.getUint16(
WOZ_TRACK_INFO_BITS,
true
);
for (let jdx = 0; jdx < trackBitCount; jdx++) {
const byteIndex = jdx >> 3;
const bitIndex = 7 - (jdx & 0x07);
rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x01 ? 1 : 0;
rawTrack[jdx] =
(trackData[byteIndex] >> bitIndex) & 0x01 ? 1 : 0;
}
track = [];
let offset = 0;
while (offset < rawTrack.length) {
const result = grabNibble(rawTrack, offset);
if (!result.nibble) { break; }
if (!result.nibble) {
break;
}
track.push(result.nibble);
offset = result.offset + 1;
}
@ -126,7 +139,7 @@ export interface Trk {
export class TrksChunk2 extends TrksChunk {
trks: Trk[];
constructor (data: DataView) {
constructor(data: DataView) {
super();
let trackNo;
@ -135,11 +148,13 @@ export class TrksChunk2 extends TrksChunk {
const startBlock = data.getUint16(trackNo * 8, true);
const blockCount = data.getUint16(trackNo * 8 + 2, true);
const bitCount = data.getUint32(trackNo * 8 + 4, true);
if (bitCount === 0) { break; }
if (bitCount === 0) {
break;
}
this.trks.push({
startBlock: startBlock,
blockCount: blockCount,
bitCount: bitCount
bitCount: bitCount,
});
}
this.tracks = [];
@ -161,14 +176,17 @@ export class TrksChunk2 extends TrksChunk {
for (let jdx = 0; jdx < trk.bitCount; jdx++) {
const byteIndex = jdx >> 3;
const bitIndex = 7 - (jdx & 0x07);
rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x01 ? 1 : 0;
rawTrack[jdx] =
(trackData[byteIndex] >> bitIndex) & 0x01 ? 1 : 0;
}
track = [];
let offset = 0;
while (offset < rawTrack.length) {
const result = grabNibble(rawTrack, offset);
if (!result.nibble) { break; }
if (!result.nibble) {
break;
}
track.push(result.nibble);
offset = result.offset + 1;
}
@ -179,13 +197,16 @@ export class TrksChunk2 extends TrksChunk {
}
}
export class MetaChunk {
export class MetaChunk {
values: Record<string, string>;
constructor (data: DataView) {
constructor(data: DataView) {
const infoStr = stringFromBytes(data, 0, data.byteLength);
const parts = infoStr.split('\n');
this.values = parts.reduce(function(acc: Record<string, string>, part) {
this.values = parts.reduce(function (
acc: Record<string, string>,
part
) {
const subParts = part.split('\t');
acc[subParts[0]] = subParts[1];
return acc;
@ -250,7 +271,7 @@ export default function createDiskFromWoz(options: DiskOptions): WozDisk {
return {
type: type,
size: size,
data: data
data: data,
};
}
@ -259,24 +280,24 @@ export default function createDiskFromWoz(options: DiskOptions): WozDisk {
let chunk = readChunk();
while (chunk) {
switch (chunk.type) {
case 0x4F464E49: // INFO
case 0x4f464e49: // INFO
chunks.info = new InfoChunk(chunk.data);
break;
case 0x50414D54: // TMAP
case 0x50414d54: // TMAP
chunks.tmap = new TMapChunk(chunk.data);
break;
case 0x534B5254: // TRKS
case 0x534b5254: // TRKS
if (wozVersion === 1) {
chunks.trks = new TrksChunk1(chunk.data);
} else {
chunks.trks = new TrksChunk2(chunk.data);
}
break;
case 0x4154454D: // META
case 0x4154454d: // META
chunks.meta = new MetaChunk(chunk.data);
break;
case 0x54495257: // WRIT
// Ignore
// Ignore
break;
default:
debug('Unsupported chunk', toHex(chunk.type, 8));
@ -299,9 +320,9 @@ export default function createDiskFromWoz(options: DiskOptions): WozDisk {
readOnly: true, //chunks.info.writeProtected === 1;
metadata: {
name: meta?.values['title'] || options.name,
side: meta?.values['side_name'] || meta?.values['side']
side: meta?.values['side_name'] || meta?.values['side'],
},
info
info,
};
return disk;

221
js/gl.ts
View File

@ -11,7 +11,7 @@ import {
VideoModes,
VideoModesState,
bank,
pageNo
pageNo,
} from './videomodes';
// Color constants
@ -22,7 +22,7 @@ const notDirty: Region = {
top: 193,
bottom: -1,
left: 561,
right: -1
right: -1,
};
/****************************************************************************
@ -40,7 +40,7 @@ export class LoresPageGL implements LoresPage {
private _refreshing = false;
private _blink = false;
dirty: Region = {...notDirty};
dirty: Region = { ...notDirty };
imageData: ImageData;
constructor(
@ -58,14 +58,18 @@ export class LoresPageGL implements LoresPage {
}
private _drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = data[off + 4] = c0;
data[off + 1] = data[off + 5] = c1;
data[off + 2] = data[off + 6] = c2;
}
private _drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = c0;
data[off + 1] = c1;
data[off + 2] = c2;
@ -75,10 +79,10 @@ export class LoresPageGL implements LoresPage {
let inverse = false;
if (this.e) {
if (!this.vm._80colMode && !this.vm.altCharMode) {
inverse = ((val & 0xc0) === 0x40) && this._blink;
inverse = (val & 0xc0) === 0x40 && this._blink;
}
} else {
inverse = !((val & 0x80) || (val & 0x40) && this._blink);
inverse = !(val & 0x80 || (val & 0x40 && this._blink));
}
return inverse;
}
@ -104,19 +108,22 @@ export class LoresPageGL implements LoresPage {
// These are used by both bank 0 and 1
private _start() {
return (0x04 * this.page);
return 0x04 * this.page;
}
private _end() { return (0x04 * this.page) + 0x03; }
private _end() {
return 0x04 * this.page + 0x03;
}
private _read(page: byte, off: byte, bank: bank) {
const addr = (page << 8) | off, base = addr & 0x3FF;
const addr = (page << 8) | off,
base = addr & 0x3ff;
return this._buffer[bank][base];
}
private _write(page: byte, off: byte, val: byte, bank: bank) {
const addr = (page << 8) | off;
const base = addr & 0x3FF;
const base = addr & 0x3ff;
let fore, back;
if (this._buffer[bank][base] === val && !this._refreshing) {
@ -128,23 +135,35 @@ export class LoresPageGL implements LoresPage {
const adj = off - col;
// 000001cd eabab000 -> 000abcde
const ab = (adj & 0x18);
const ab = adj & 0x18;
const cd = (page & 0x03) << 1;
const ee = adj >> 7;
const row = ab | cd | ee;
const data = this.imageData.data;
if ((row < 24) && (col < 40)) {
if (row < 24 && col < 40) {
let y = row << 3;
if (y < this.dirty.top) { this.dirty.top = y; }
if (y < this.dirty.top) {
this.dirty.top = y;
}
y += 8;
if (y > this.dirty.bottom) { this.dirty.bottom = y; }
if (y > this.dirty.bottom) {
this.dirty.bottom = y;
}
let x = col * 14;
if (x < this.dirty.left) { this.dirty.left = x; }
if (x < this.dirty.left) {
this.dirty.left = x;
}
x += 14;
if (x > this.dirty.right) { this.dirty.right = x; }
if (x > this.dirty.right) {
this.dirty.right = x;
}
if (this.vm.textMode || this.vm.hiresMode || (this.vm.mixedMode && row > 19)) {
if (
this.vm.textMode ||
this.vm.hiresMode ||
(this.vm.mixedMode && row > 19)
) {
if (this.vm._80colMode) {
const inverse = this._checkInverse(val);
@ -152,15 +171,16 @@ export class LoresPageGL implements LoresPage {
back = inverse ? whiteCol : blackCol;
if (!this.vm.altCharMode) {
val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
val = val >= 0x40 && val < 0x80 ? val - 0x40 : val;
}
let offset = (col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
let offset =
(col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
for (let jdx = 0; jdx < 8; jdx++) {
let b = this.charset[val * 8 + jdx];
for (let idx = 0; idx < 7; idx++) {
const color = (b & 0x01) ? back : fore;
const color = b & 0x01 ? back : fore;
this._drawHalfPixel(data, offset, color);
b >>= 1;
offset += 4;
@ -176,7 +196,7 @@ export class LoresPageGL implements LoresPage {
back = inverse ? whiteCol : blackCol;
if (!this.vm.altCharMode) {
val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
val = val >= 0x40 && val < 0x80 ? val - 0x40 : val;
}
let offset = (col * 14 + row * 560 * 8) * 4;
@ -185,7 +205,7 @@ export class LoresPageGL implements LoresPage {
for (let jdx = 0; jdx < 8; jdx++) {
let b = this.charset[val * 8 + jdx];
for (let idx = 0; idx < 7; idx++) {
const color = (b & 0x01) ? back : fore;
const color = b & 0x01 ? back : fore;
this._drawPixel(data, offset, color);
b >>= 1;
offset += 8;
@ -197,7 +217,7 @@ export class LoresPageGL implements LoresPage {
let b = this.charset[val * 8 + jdx] << 1;
for (let idx = 0; idx < 7; idx++) {
const color = (b & 0x80) ? fore : back;
const color = b & 0x80 ? fore : back;
this._drawPixel(data, offset, color);
b <<= 1;
offset += 8;
@ -208,16 +228,17 @@ export class LoresPageGL implements LoresPage {
}
} else {
if (this.vm._80colMode && !this.vm.an3State) {
let offset = (col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
let offset =
(col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
for (let jdx = 0; jdx < 8; jdx++) {
let b = (jdx < 4) ? (val & 0x0f) : (val >> 4);
b |= (b << 4);
b |= (b << 8);
let b = jdx < 4 ? val & 0x0f : val >> 4;
b |= b << 4;
b |= b << 8;
if (col & 0x1) {
b >>= 2;
}
for (let idx = 0; idx < 7; idx++) {
const color = (b & 0x01) ? whiteCol : blackCol;
const color = b & 0x01 ? whiteCol : blackCol;
this._drawHalfPixel(data, offset, color);
b >>= 1;
offset += 4;
@ -227,14 +248,14 @@ export class LoresPageGL implements LoresPage {
} else if (bank === 0) {
let offset = (col * 14 + row * 560 * 8) * 4;
for (let jdx = 0; jdx < 8; jdx++) {
let b = (jdx < 4) ? (val & 0x0f) : (val >> 4);
b |= (b << 4);
b |= (b << 8);
let b = jdx < 4 ? val & 0x0f : val >> 4;
b |= b << 4;
b |= b << 8;
if (col & 0x1) {
b >>= 2;
}
for (let idx = 0; idx < 14; idx++) {
const color = (b & 0x0001) ? whiteCol : blackCol;
const color = b & 0x0001 ? whiteCol : blackCol;
this._drawHalfPixel(data, offset, color);
b >>= 1;
offset += 4;
@ -264,7 +285,7 @@ export class LoresPageGL implements LoresPage {
this._blink = !this._blink;
for (let idx = 0; idx < 0x400; idx++, addr++) {
const b = this._buffer[0][idx];
if ((b & 0xC0) === 0x40) {
if ((b & 0xc0) === 0x40) {
this._write(addr >> 8, addr & 0xff, this._buffer[0][idx], 0);
}
}
@ -293,7 +314,7 @@ export class LoresPageGL implements LoresPage {
buffer: [
new Uint8Array(this._buffer[0]),
new Uint8Array(this._buffer[1]),
]
],
};
}
@ -312,25 +333,29 @@ export class LoresPageGL implements LoresPage {
}
private mapCharCode(charCode: byte) {
charCode &= 0x7F;
charCode &= 0x7f;
if (charCode < 0x20) {
charCode += 0x40;
}
if (!this.e && (charCode >= 0x60)) {
if (!this.e && charCode >= 0x60) {
charCode -= 0x40;
}
return charCode;
}
getText() {
let buffer = '', line, charCode;
let buffer = '',
line,
charCode;
let row, col, base;
for (row = 0; row < 24; row++) {
base = this.rowToBase(row);
line = '';
if (this.e && this.vm._80colMode) {
for (col = 0; col < 80; col++) {
charCode = this.mapCharCode(this._buffer[1 - col % 2][base + Math.floor(col / 2)]);
charCode = this.mapCharCode(
this._buffer[1 - (col % 2)][base + Math.floor(col / 2)]
);
line += String.fromCharCode(charCode);
}
} else {
@ -353,7 +378,9 @@ export class LoresPageGL implements LoresPage {
***************************************************************************/
const _drawPixel = (data: Uint8ClampedArray, off: number, color: Color) => {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = data[off + 4] = c0;
data[off + 1] = data[off + 5] = c1;
@ -361,7 +388,9 @@ const _drawPixel = (data: Uint8ClampedArray, off: number, color: Color) => {
};
const _drawHalfPixel = (data: Uint8ClampedArray, off: number, color: Color) => {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = c0;
data[off + 1] = c1;
@ -370,14 +399,14 @@ const _drawHalfPixel = (data: Uint8ClampedArray, off: number, color: Color) => {
export class HiresPageGL implements HiresPage {
public imageData: ImageData;
dirty: Region = {...notDirty};
dirty: Region = { ...notDirty };
private _buffer: memory[] = [];
private _refreshing = false;
constructor(
private vm: VideoModes,
private page: pageNo,
private page: pageNo
) {
this.imageData = this.vm.context.createImageData(560, 192);
this.imageData.data.fill(0xff);
@ -405,18 +434,23 @@ export class HiresPageGL implements HiresPage {
};
}
private _start() { return (0x20 * this.page); }
private _start() {
return 0x20 * this.page;
}
private _end() { return (0x020 * this.page) + 0x1f; }
private _end() {
return 0x020 * this.page + 0x1f;
}
private _read(page: byte, off: byte, bank: bank) {
const addr = (page << 8) | off, base = addr & 0x1FFF;
const addr = (page << 8) | off,
base = addr & 0x1fff;
return this._buffer[bank][base];
}
private _write(page: byte, off: byte, val: byte, bank: bank) {
const addr = (page << 8) | off;
const base = addr & 0x1FFF;
const base = addr & 0x1fff;
if (this._buffer[bank][base] === val && !this._refreshing) {
return;
@ -427,7 +461,7 @@ export class HiresPageGL implements HiresPage {
const adj = off - col;
// 000001cd eabab000 -> 000abcde
const ab = (adj & 0x18);
const ab = adj & 0x18;
const cd = (page & 0x03) << 1;
const e = adj >> 7;
@ -435,17 +469,25 @@ export class HiresPageGL implements HiresPage {
rowb = base >> 10;
const data = this.imageData.data;
if ((rowa < 24) && (col < 40) && this.vm.hiresMode) {
let y = rowa << 3 | rowb;
if (y < this.dirty.top) { this.dirty.top = y; }
if (rowa < 24 && col < 40 && this.vm.hiresMode) {
let y = (rowa << 3) | rowb;
if (y < this.dirty.top) {
this.dirty.top = y;
}
y += 1;
if (y > this.dirty.bottom) { this.dirty.bottom = y; }
if (y > this.dirty.bottom) {
this.dirty.bottom = y;
}
let x = col * 14 - 2;
if (x < this.dirty.left) { this.dirty.left = x; }
if (x < this.dirty.left) {
this.dirty.left = x;
}
x += 14;
if (x > this.dirty.right) { this.dirty.right = x; }
if (x > this.dirty.right) {
this.dirty.right = x;
}
const dy = rowa << 3 | rowb;
const dy = (rowa << 3) | rowb;
if (this.vm.doubleHiresMode) {
const dx = col * 14 + (bank ? 0 : 7);
let offset = dx * 4 + dy * 280 * 4 * 2;
@ -476,9 +518,10 @@ export class HiresPageGL implements HiresPage {
}
let bits = val;
for (let idx = 0; idx < 7; idx++, offset += 8) {
const drawPixel = cropLastPixel && idx === 6
? _drawHalfPixel
: _drawPixel;
const drawPixel =
cropLastPixel && idx === 6
? _drawHalfPixel
: _drawPixel;
if (bits & 0x01) {
drawPixel(data, offset, whiteCol);
} else {
@ -489,7 +532,12 @@ export class HiresPageGL implements HiresPage {
if (!this._refreshing) {
this._refreshing = true;
const after = addr + 1;
this._write(after >> 8, after & 0xff, this._buffer[0][after & 0x1fff], 0);
this._write(
after >> 8,
after & 0xff,
this._buffer[0][after & 0x1fff],
0
);
this._refreshing = false;
}
}
@ -531,7 +579,7 @@ export class HiresPageGL implements HiresPage {
buffer: [
new Uint8Array(this._buffer[0]),
new Uint8Array(this._buffer[1]),
]
],
};
}
@ -595,7 +643,10 @@ export class VideoModesGL implements VideoModes {
private defaultMonitor(): screenEmu.DisplayConfiguration {
const config = new screenEmu.DisplayConfiguration();
config.displayResolution = new screenEmu.Size(this.screen.width, this.screen.height);
config.displayResolution = new screenEmu.Size(
this.screen.width,
this.screen.height
);
config.displayScanlineLevel = 0.5;
config.videoWhiteOnly = true;
config.videoSaturation = 0.8;
@ -608,7 +659,10 @@ export class VideoModesGL implements VideoModes {
private monitorII(): screenEmu.DisplayConfiguration {
// Values taken from openemulator/libemulation/res/library/Monitors/Apple Monitor II.xml
const config = new screenEmu.DisplayConfiguration();
config.displayResolution = new screenEmu.Size(this.screen.width, this.screen.height);
config.displayResolution = new screenEmu.Size(
this.screen.width,
this.screen.height
);
config.videoDecoder = 'CANVAS_MONOCHROME';
config.videoBrightness = 0.15;
config.videoContrast = 0.8;
@ -625,13 +679,16 @@ export class VideoModesGL implements VideoModes {
}
private _refresh() {
this.doubleHiresMode = !this.an3State && this.hiresMode && this._80colMode;
this.doubleHiresMode =
!this.an3State && this.hiresMode && this._80colMode;
this._refreshFlag = true;
if (this._displayConfig) {
this._displayConfig.videoWhiteOnly = this.textMode || this.monoMode;
this._displayConfig.displayScanlineLevel = this._scanlines ? 0.5 : 0;
this._displayConfig.displayScanlineLevel = this._scanlines
? 0.5
: 0;
this._sv.displayConfiguration = this._displayConfig;
}
}
@ -680,7 +737,9 @@ export class VideoModesGL implements VideoModes {
}
_80col(on: boolean) {
if (!this.e) { return; }
if (!this.e) {
return;
}
const old = this._80colMode;
this._80colMode = on;
@ -691,7 +750,9 @@ export class VideoModesGL implements VideoModes {
}
altChar(on: boolean) {
if (!this.e) { return; }
if (!this.e) {
return;
}
const old = this.altCharMode;
this.altCharMode = on;
@ -710,7 +771,9 @@ export class VideoModesGL implements VideoModes {
}
an3(on: boolean) {
if (!this.e) { return; }
if (!this.e) {
return;
}
const old = this.an3State;
this.an3State = on;
@ -788,7 +851,9 @@ export class VideoModesGL implements VideoModes {
buildScreen(mainData: ImageData, mixData?: ImageData | null) {
const details = screenEmu.C.NTSC_DETAILS;
const { width, height } = details.imageSize;
const { x, y } = this._80colMode ? details.topLeft80Col : details.topLeft;
const { x, y } = this._80colMode
? details.topLeft80Col
: details.topLeft;
if (mixData) {
this.context.putImageData(mainData, x, y, 0, 0, 560, 160);
@ -813,10 +878,12 @@ export class VideoModesGL implements VideoModes {
}
if (altData) {
blitted = this.updateImage(
altData,
{ top: 0, left: 0, right: 560, bottom: 192 }
);
blitted = this.updateImage(altData, {
top: 0,
left: 0,
right: 560,
bottom: 192,
});
} else if (this.hiresMode && !this.textMode) {
blitted = this.updateImage(
hgr.imageData,
@ -825,12 +892,10 @@ export class VideoModesGL implements VideoModes {
this.mixedMode ? gr.dirty : null
);
} else {
blitted = this.updateImage(
gr.imageData, gr.dirty
);
blitted = this.updateImage(gr.imageData, gr.dirty);
}
hgr.dirty = {...notDirty};
gr.dirty = {...notDirty};
hgr.dirty = { ...notDirty };
gr.dirty = { ...notDirty };
return blitted;
}
@ -846,7 +911,7 @@ export class VideoModesGL implements VideoModes {
_80colMode: this._80colMode,
altCharMode: this.altCharMode,
an3State: this.an3State,
flag: 0
flag: 0,
};
}

View File

@ -17,12 +17,12 @@ const TOKENS: Record<byte, string> = {
0x07: 'RUN',
0x08: 'RUN',
0x09: 'DEL',
0x0A: ',',
0x0B: 'NEW',
0x0C: 'CLR',
0x0D: 'AUTO',
0x0E: ',',
0x0F: 'MAN',
0x0a: ',',
0x0b: 'NEW',
0x0c: 'CLR',
0x0d: 'AUTO',
0x0e: ',',
0x0f: 'MAN',
0x10: 'HIMEM:',
0x11: 'LOMEM:',
0x12: '+',
@ -33,12 +33,12 @@ const TOKENS: Record<byte, string> = {
0x17: '#',
0x18: '>=',
0x19: '>',
0x1A: '<=',
0x1B: '<>',
0x1C: '<',
0x1D: 'AND',
0x1E: 'OR',
0x1F: 'MOD',
0x1a: '<=',
0x1b: '<>',
0x1c: '<',
0x1d: 'AND',
0x1e: 'OR',
0x1f: 'MOD',
0x20: '^',
0x21: '+',
0x22: '(',
@ -49,12 +49,12 @@ const TOKENS: Record<byte, string> = {
0x27: ',',
0x28: '"',
0x29: '"',
0x2A: '(',
0x2B: '!',
0x2C: '!',
0x2D: '(',
0x2E: 'PEEK',
0x2F: 'RND',
0x2a: '(',
0x2b: '!',
0x2c: '!',
0x2d: '(',
0x2e: 'PEEK',
0x2f: 'RND',
0x30: 'SGN',
0x31: 'ABS',
0x32: 'PDL',
@ -65,12 +65,12 @@ const TOKENS: Record<byte, string> = {
0x37: 'NOT',
0x38: '(',
0x39: '=',
0x3A: '#',
0x3B: 'LEN(',
0x3C: 'ASC(',
0x3D: 'SCRN(',
0x3E: ',',
0x3F: '(',
0x3a: '#',
0x3b: 'LEN(',
0x3c: 'ASC(',
0x3d: 'SCRN(',
0x3e: ',',
0x3f: '(',
0x40: '$',
0x41: '$',
0x42: '(',
@ -81,12 +81,12 @@ const TOKENS: Record<byte, string> = {
0x47: ';',
0x48: ',',
0x49: ',',
0x4A: ',',
0x4B: 'TEXT',
0x4C: 'GR',
0x4D: 'CALL',
0x4E: 'DIM',
0x4F: 'DIM',
0x4a: ',',
0x4b: 'TEXT',
0x4c: 'GR',
0x4d: 'CALL',
0x4e: 'DIM',
0x4f: 'DIM',
0x50: 'TAB',
0x51: 'END',
0x52: 'INPUT',
@ -97,12 +97,12 @@ const TOKENS: Record<byte, string> = {
0x57: 'TO',
0x58: 'STEP',
0x59: 'NEXT',
0x5A: ',',
0x5B: 'RETURN',
0x5C: 'GOSUB',
0x5D: 'REM',
0x5E: 'LET',
0x5F: 'GOTO',
0x5a: ',',
0x5b: 'RETURN',
0x5c: 'GOSUB',
0x5d: 'REM',
0x5e: 'LET',
0x5f: 'GOTO',
0x60: 'IF',
0x61: 'PRINT',
0x62: 'PRINT',
@ -113,12 +113,12 @@ const TOKENS: Record<byte, string> = {
0x67: 'PLOT',
0x68: ',',
0x69: 'HLIN',
0x6A: ',',
0x6B: 'AT',
0x6C: 'VLIN',
0x6D: ',',
0x6E: 'AT',
0x6F: 'VTAB',
0x6a: ',',
0x6b: 'AT',
0x6c: 'VLIN',
0x6d: ',',
0x6e: 'AT',
0x6f: 'VTAB',
0x70: '=',
0x71: '=',
0x72: ')',
@ -129,16 +129,16 @@ const TOKENS: Record<byte, string> = {
0x77: 'POP',
0x78: 'NODSP',
0x79: 'NODSP',
0x7A: 'NOTRACE',
0x7B: 'DSP',
0x7C: 'DSP',
0x7D: 'TRACE',
0x7E: 'PR#',
0x7F: 'IN#'
0x7a: 'NOTRACE',
0x7b: 'DSP',
0x7c: 'DSP',
0x7d: 'TRACE',
0x7e: 'PR#',
0x7f: 'IN#',
};
export default class IntBasicDump {
constructor(private data: Uint8Array) { }
constructor(private data: Uint8Array) {}
private readByte(addr: word) {
return this.data[addr];
@ -168,17 +168,33 @@ export default class IntBasicDump {
let val = 0;
do {
val = this.readByte(addr++);
if (!inRem && !inQuote && !isAlphaNum && val >= 0xB0 && val <= 0xB9) {
if (
!inRem &&
!inQuote &&
!isAlphaNum &&
val >= 0xb0 &&
val <= 0xb9
) {
str += this.readWord(addr);
addr += 2;
} else if (val < 0x80 && val > 0x01) {
const t = TOKENS[val];
if (t.length > 1) { str += ' '; }
if (t.length > 1) {
str += ' ';
}
str += t;
if (t.length > 1) { str += ' '; }
if (val === 0x28) { inQuote = true; }
if (val === 0x29) { inQuote = false; }
if (val === 0x5d) { inRem = true; }
if (t.length > 1) {
str += ' ';
}
if (val === 0x28) {
inQuote = true;
}
if (val === 0x29) {
inQuote = false;
}
if (val === 0x5d) {
inRem = true;
}
isAlphaNum = false;
} else if (val > 0x80) {
const char = LETTERS[val - 0x80];

View File

@ -59,33 +59,35 @@ const options = {
characterRom,
e: false,
enhanced: false,
tick: updateUI
tick: updateUI,
};
export const apple2 = new Apple2(options);
apple2.ready.then(() => {
const cpu = apple2.getCPU();
const io = apple2.getIO();
apple2.ready
.then(() => {
const cpu = apple2.getCPU();
const io = apple2.getIO();
const printer = new Printer('#printer-modal .paper');
const printer = new Printer('#printer-modal .paper');
const lc = new LanguageCard(apple2.getROM());
const parallel = new Parallel(printer);
const videoTerm = new VideoTerm();
const slinky = new RAMFactor(1024 * 1024);
const disk2 = new DiskII(io, driveLights, sectors);
const clock = new Thunderclock();
const smartport = new SmartPort(cpu, null, { block: true });
const lc = new LanguageCard(apple2.getROM());
const parallel = new Parallel(printer);
const videoTerm = new VideoTerm();
const slinky = new RAMFactor(1024 * 1024);
const disk2 = new DiskII(io, driveLights, sectors);
const clock = new Thunderclock();
const smartport = new SmartPort(cpu, null, { block: true });
io.setSlot(0, lc);
io.setSlot(1, parallel);
io.setSlot(2, slinky);
io.setSlot(4, clock);
io.setSlot(3, videoTerm);
io.setSlot(6, disk2);
io.setSlot(7, smartport);
io.setSlot(0, lc);
io.setSlot(1, parallel);
io.setSlot(2, slinky);
io.setSlot(4, clock);
io.setSlot(3, videoTerm);
io.setSlot(6, disk2);
io.setSlot(7, smartport);
cpu.addPageHandler(lc);
cpu.addPageHandler(lc);
initUI(apple2, disk2, smartport, printer, false);
}).catch(console.error);
initUI(apple2, disk2, smartport, printer, false);
})
.catch(console.error);

Some files were not shown because too many files have changed in this diff Show More