Compare commits

...

4 Commits

Author SHA1 Message Date
Will Scullin aaca31f96b
VS Code and prettier are fighting... 2023-11-24 16:30:59 -08:00
Will Scullin 67abcd4efd
Audit fixes 2023-11-24 10:13:25 -08:00
Will Scullin 45a5e63cf9
Use debugger from submodule 2023-11-24 10:12:19 -08:00
Will Scullin 1e79d9d59d
Prettier (#203)
* Enabled prettier

* Update lint, fix issues

* Restore some array formatting
2023-11-24 06:45:55 -08:00
173 changed files with 35406 additions and 34254 deletions

View File

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

View File

@ -1,23 +1,17 @@
{ {
// Global // Global
"root": true, "root": true,
"plugins": [
"prettier"
],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:prettier/recommended",
"plugin:jest/recommended" "plugin:jest/recommended"
], ],
"rules": { "rules": {
"indent": [ "prettier/prettier": "error",
"error",
4,
{
"SwitchCase": 1
}
],
"quotes": [
"error",
"single"
],
"linebreak-style": [ "linebreak-style": [
"error", "error",
"unix" "unix"
@ -80,12 +74,6 @@
"rules": { "rules": {
// recommended is just "warn" // recommended is just "warn"
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
// enforce semicolons at ends of statements
"semi": "off",
"@typescript-eslint/semi": [
"error",
"always"
],
// enforce semicolons to separate members // enforce semicolons to separate members
"@typescript-eslint/member-delimiter-style": [ "@typescript-eslint/member-delimiter-style": [
"error", "error",

View File

@ -3,25 +3,25 @@ name: Node CI
on: [push] on: [push]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [16.x, 18.x] node-version: [16.x, 18.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
submodules: "true" submodules: 'true'
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: npm install, build, and test - name: npm install, build, and test
run: | run: |
npm ci npm ci
npm run build --if-present npm run build --if-present
npm test npm test
env: env:
CI: true CI: true

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": [ "extends": [
"stylelint-config-standard-scss", "stylelint-config-standard-scss",
"stylelint-config-css-modules" "stylelint-config-css-modules"
], ],
"rules": { "rules": {
"indentation": 4, "selector-class-pattern": "^[a-z][a-zA-Z0-9_-]+$"
"selector-class-pattern": "^[a-z][a-zA-Z0-9_-]+$" }
}
} }

View File

@ -46,29 +46,29 @@ then
### 2019-03-02 ### 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. 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. 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. 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. Thanks to [Ian Flanigan](https://github.com/iflan) for additions to improve ChromeBook behavior.
### 2017-10-08 ### 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. 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. 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) (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 \]\[!. 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 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. You can now select between original, autostart and plus Apple \]\[s, and unenhanced and enhanced //es.
## Updates (2013-07-04) ## Updates (2013-07-04)
* RAMFactor Emulation (//jse) - RAMFactor Emulation (//jse)
I now simulate having a 1 Megabyte RAMFactor card in slot 2. 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. 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. Joystick support has yet to officially land, but the latest nightlies support the gamepad API.
## Updates (2013-03-20) ## 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. 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. 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) ## 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. 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) ## 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. 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. 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. 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. 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 ## 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. 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. If you don't know how to use an Apple \]\[, this won't be much fun for you.
## Acknowledgements ## Acknowledgements
* I'm using the following libraries: - I'm using the following libraries:
* [jQuery](https://jquery.com) and [jQuery UI](https://jqueryui.com) - [jQuery](https://jquery.com) and [jQuery UI](https://jqueryui.com)
* Base64 Utilities via [KvZ](http://kevin.vanzonneveld.net/) - Base64 Utilities via [KvZ](http://kevin.vanzonneveld.net/)
* LED graphics from [Modern Life](http://modernl.com/). - 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 - [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 - [_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 - _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/) - [_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 - [_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/) - [6502.org](http://6502.org/)
* The [comp.sys.apple2.programmer](http://www.faqs.org/faqs/apple2/programmerfaq/part1/) FAQ - 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. - [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. - [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. - [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. - [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. - [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. - [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. - [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. - [Zellyn Hunter](https://github.com/zellyn/a2audit) and a2audit, for allowing me to get really nitpicky in my memory emulation.
* Contributors - Contributors
* [Snapperfish](http://github.com/Snapperfish) Various fixes - [Snapperfish](http://github.com/Snapperfish) Various fixes

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,21 +5,15 @@ import {
VideoModes, VideoModes,
VideoModesState, VideoModesState,
} from './videomodes'; } from './videomodes';
import { import { HiresPage2D, LoresPage2D, VideoModes2D } from './canvas';
HiresPage2D, import { HiresPageGL, LoresPageGL, VideoModesGL } from './gl';
LoresPage2D,
VideoModes2D,
} from './canvas';
import {
HiresPageGL,
LoresPageGL,
VideoModesGL,
} from './gl';
import ROM from './roms/rom'; import ROM from './roms/rom';
import { Apple2IOState } from './apple2io'; import { Apple2IOState } from './apple2io';
import { import {
CPU6502, CPU6502,
CpuState, CpuState,
Debugger,
DebuggerContainer,
FLAVOR_6502, FLAVOR_6502,
FLAVOR_ROCKWELL_65C02, FLAVOR_ROCKWELL_65C02,
} from '@whscullin/cpu6502'; } from '@whscullin/cpu6502';
@ -27,7 +21,6 @@ import MMU, { MMUState } from './mmu';
import RAM, { RAMState } from './ram'; import RAM, { RAMState } from './ram';
import SYMBOLS from './symbols'; import SYMBOLS from './symbols';
import Debugger, { DebuggerContainer } from './debugger';
import { ReadonlyUint8Array, Restorable, rom } from './types'; import { ReadonlyUint8Array, Restorable, rom } from './types';
import { processGamepad } from './ui/gamepad'; import { processGamepad } from './ui/gamepad';
@ -82,7 +75,7 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
private stats: Stats = { private stats: Stats = {
cycles: 0, cycles: 0,
frames: 0, frames: 0,
renderedFrames: 0 renderedFrames: 0,
}; };
public ready: Promise<void>; public ready: Promise<void>;
@ -92,23 +85,30 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
} }
async init(options: Apple2Options) { async init(options: Apple2Options) {
const romImportPromise = import(`./roms/system/${options.rom}`) as Promise<{ default: new () => ROM }>; const romImportPromise = import(
const characterRomImportPromise = import(`./roms/character/${options.characterRom}`) as Promise<{ default: ReadonlyUint8Array }>; `./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 LoresPage = options.gl ? LoresPageGL : LoresPage2D;
const HiresPage = options.gl ? HiresPageGL : HiresPage2D; const HiresPage = options.gl ? HiresPageGL : HiresPage2D;
const VideoModes = options.gl ? VideoModesGL : VideoModes2D; const VideoModes = options.gl ? VideoModesGL : VideoModes2D;
this.cpu = new CPU6502({ 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); this.vm = new VideoModes(options.canvas, options.e);
const [{ default: Apple2ROM }, { default: characterRom }] = await Promise.all([ const [{ default: Apple2ROM }, { default: characterRom }] =
romImportPromise, await Promise.all([
characterRomImportPromise, romImportPromise,
this.vm.ready, characterRomImportPromise,
]); this.vm.ready,
]);
this.rom = new Apple2ROM(); this.rom = new Apple2ROM();
this.characterRom = characterRom; this.characterRom = characterRom;
@ -121,13 +121,22 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
this.tick = options.tick; this.tick = options.tick;
if (options.e) { 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); this.cpu.addPageHandler(this.mmu);
} else { } else {
this.ram = [ this.ram = [
new RAM(0x00, 0x03), new RAM(0x00, 0x03),
new RAM(0x0C, 0x1F), new RAM(0x0c, 0x1f),
new RAM(0x60, 0xBF) new RAM(0x60, 0xbf),
]; ];
this.cpu.addPageHandler(this.ram[0]); this.cpu.addPageHandler(this.ram[0]);
@ -158,7 +167,8 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
const interval = 30; const interval = 30;
let now, last = Date.now(); let now,
last = Date.now();
const runFn = () => { const runFn = () => {
const kHz = this.io.getKHz(); const kHz = this.io.getKHz();
now = Date.now(); now = Date.now();
@ -228,7 +238,7 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
vm: this.vm.getState(), vm: this.vm.getState(),
io: this.io.getState(), io: this.io.getState(),
mmu: this.mmu?.getState(), mmu: this.mmu?.getState(),
ram: this.ram?.map(bank => bank.getState()), ram: this.ram?.map((bank) => bank.getState()),
}; };
return state; return state;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,17 +11,17 @@ export const TXTTAB = 0x67;
/** Start of variables (word) */ /** Start of variables (word) */
export const VARTAB = 0x69; export const VARTAB = 0x69;
/** Start of arrays (word) */ /** Start of arrays (word) */
export const ARYTAB = 0x6B; export const ARYTAB = 0x6b;
/** End of strings (word). (Strings are allocated down from HIMEM.) */ /** End of strings (word). (Strings are allocated down from HIMEM.) */
export const STREND = 0x6D; export const STREND = 0x6d;
/** Current line */ /** Current line */
export const CURLINE = 0x75; export const CURLINE = 0x75;
/** Floating Point accumulator (float) */ /** Floating Point accumulator (float) */
export const FAC = 0x9D; export const FAC = 0x9d;
/** Floating Point arguments (float) */ /** 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 * End of program (word). This is actually 1 or 2 bytes past the three
* zero bytes that end the program. * 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. */ /** Encode an array of bytes in base64. */
export function base64_encode(data: null | undefined): undefined; export function base64_encode(data: null | undefined): undefined;
export function base64_encode(data: memory): string; 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' // Twacked by Will Scullin to handle arrays of 'bytes'
// http://kevin.vanzonneveld.net // http://kevin.vanzonneveld.net
@ -25,28 +27,39 @@ export function base64_encode(data: memory | null | undefined): string | undefin
// return atob(data); // return atob(data);
//} //}
let o1,
let o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc=''; o2,
o3,
h1,
h2,
h3,
h4,
bits,
i = 0,
ac = 0,
enc = '';
const tmp_arr = []; const tmp_arr = [];
if (!data) { if (!data) {
return undefined; return undefined;
} }
do { // pack three octets into four hexets do {
// pack three octets into four hexets
o1 = data[i++]; o1 = data[i++];
o2 = data[i++]; o2 = data[i++];
o3 = data[i++]; o3 = data[i++];
bits = o1<<16 | o2<<8 | o3; bits = (o1 << 16) | (o2 << 8) | o3;
h1 = bits>>18 & 0x3f; h1 = (bits >> 18) & 0x3f;
h2 = bits>>12 & 0x3f; h2 = (bits >> 12) & 0x3f;
h3 = bits>>6 & 0x3f; h3 = (bits >> 6) & 0x3f;
h4 = bits & 0x3f; h4 = bits & 0x3f;
// use hexets to index into b64, and append result to encoded string // 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); } while (i < data.length);
enc = tmp_arr.join(''); 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. */ /** Returns an array of bytes from the given base64-encoded string. */
export function base64_decode(data: string): memory; export function base64_decode(data: string): memory;
/** Returns an array of bytes from the given base64-encoded string. */ /** 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' // Twacked by Will Scullin to handle arrays of 'bytes'
// http://kevin.vanzonneveld.net // http://kevin.vanzonneveld.net
@ -91,23 +106,33 @@ export function base64_decode(data: string | null | undefined): memory | undefin
// return btoa(data); // 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 = []; const tmp_arr = [];
if (!data) { if (!data) {
return undefined; 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++)); h1 = B64.indexOf(data.charAt(i++));
h2 = B64.indexOf(data.charAt(i++)); h2 = B64.indexOf(data.charAt(i++));
h3 = B64.indexOf(data.charAt(i++)); h3 = B64.indexOf(data.charAt(i++));
h4 = 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; o1 = (bits >> 16) & 0xff;
o2 = bits>>8 & 0xff; o2 = (bits >> 8) & 0xff;
o3 = bits & 0xff; o3 = bits & 0xff;
tmp_arr[ac++] = o1; 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 { export function base64_json_parse(json: string): unknown {
const reviver = (_key: string, value: unknown) => { const reviver = (_key: string, value: unknown) => {
if (typeof value ==='string' && value.startsWith(DATA_URL_PREFIX)) { if (typeof value === 'string' && value.startsWith(DATA_URL_PREFIX)) {
return base64_decode(value.slice(DATA_URL_PREFIX.length)); return base64_decode(value.slice(DATA_URL_PREFIX.length));
} }
return value; return value;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,9 @@ export interface LanguageCardState {
prewrite: boolean; prewrite: boolean;
} }
export default class LanguageCard implements Card, Restorable<LanguageCardState> { export default class LanguageCard
implements Card, Restorable<LanguageCardState>
{
private bank1: RAM; private bank1: RAM;
private bank2: RAM; private bank2: RAM;
private ram: RAM; private ram: RAM;
@ -88,7 +90,8 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
let bankStr; let bankStr;
let rwStr; let rwStr;
if (writeSwitch) { // 0xC081, 0xC083 if (writeSwitch) {
// 0xC081, 0xC083
if (readMode) { if (readMode) {
if (this._prewrite) { if (this._prewrite) {
this._writebsr = true; this._writebsr = true;
@ -96,30 +99,37 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
} }
this._prewrite = readMode; this._prewrite = readMode;
if (offSwitch) { // $C083, $C08B if (offSwitch) {
// $C083, $C08B
this._readbsr = true; this._readbsr = true;
rwStr = 'Read/Write'; rwStr = 'Read/Write';
} else { // $C081, $C089 } else {
// $C081, $C089
this._readbsr = false; this._readbsr = false;
rwStr = 'Write'; rwStr = 'Write';
} }
} else { // $C080, $C082, $C088, $C08A } else {
// $C080, $C082, $C088, $C08A
this._writebsr = false; this._writebsr = false;
this._prewrite = false; this._prewrite = false;
if (offSwitch) { // $C082, $C08A if (offSwitch) {
// $C082, $C08A
this._readbsr = false; this._readbsr = false;
rwStr = 'Off'; rwStr = 'Off';
} else { // $C080, $C088 } else {
// $C080, $C088
this._readbsr = true; this._readbsr = true;
rwStr = 'Read'; rwStr = 'Read';
} }
} }
if (bank1Switch) { // C08[8-C] if (bank1Switch) {
// C08[8-C]
this._bsr2 = false; this._bsr2 = false;
bankStr = 'Bank 1'; bankStr = 'Bank 1';
} else { // C08[0-3] } else {
// C08[0-3]
this._bsr2 = true; this._bsr2 = true;
bankStr = 'Bank 2'; bankStr = 'Bank 2';
} }
@ -180,7 +190,7 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
prewrite: this._prewrite, prewrite: this._prewrite,
ram: this.ram.getState(), ram: this.ram.getState(),
bank1: this.bank1.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'; import { rom } from '../roms/cards/mouse';
const CLAMP_MIN_LOW = 0x478; const CLAMP_MIN_LOW = 0x478;
const CLAMP_MAX_LOW = 0x4F8; const CLAMP_MAX_LOW = 0x4f8;
const CLAMP_MIN_HIGH = 0x578; const CLAMP_MIN_HIGH = 0x578;
const CLAMP_MAX_HIGH = 0x5F8; const CLAMP_MAX_HIGH = 0x5f8;
const X_LOW = 0x478; const X_LOW = 0x478;
const Y_LOW = 0x4F8; const Y_LOW = 0x4f8;
const X_HIGH = 0x578; const X_HIGH = 0x578;
const Y_HIGH = 0x5F8; const Y_HIGH = 0x5f8;
const STATUS = 0x778; const STATUS = 0x778;
const MODE = 0x7F8; const MODE = 0x7f8;
const STATUS_DOWN = 0x80; const STATUS_DOWN = 0x80;
const STATUS_LAST = 0x40; const STATUS_LAST = 0x40;
@ -38,7 +38,7 @@ const ENTRIES = {
POS_MOUSE: 0x16, POS_MOUSE: 0x16,
CLAMP_MOUSE: 0x17, CLAMP_MOUSE: 0x17,
HOME_MOUSE: 0x18, HOME_MOUSE: 0x18,
INIT_MOUSE: 0x19 INIT_MOUSE: 0x19,
}; };
interface MouseState { interface MouseState {
@ -65,9 +65,9 @@ export default class Mouse implements Card, Restorable<MouseState> {
/** Lowest mouse Y */ /** Lowest mouse Y */
private clampYMin: word = 0; private clampYMin: word = 0;
/** Highest mouse X */ /** Highest mouse X */
private clampXMax: word = 0x3FF; private clampXMax: word = 0x3ff;
/** Highest mouse Y */ /** Highest mouse Y */
private clampYMax: word = 0x3FF; private clampYMax: word = 0x3ff;
/** Mouse X position */ /** Mouse X position */
private x: word = 0; private x: word = 0;
/** Mouse Y position */ /** Mouse Y position */
@ -117,7 +117,7 @@ export default class Mouse implements Card, Restorable<MouseState> {
}; };
const clearCarry = (state: CpuState) => { const clearCarry = (state: CpuState) => {
state.s &= 0xFE; state.s &= 0xfe;
return state; return state;
}; };
@ -145,7 +145,8 @@ export default class Mouse implements Card, Restorable<MouseState> {
break; break;
case rom[ENTRIES.READ_MOUSE]: 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 = const status =
(this.down ? STATUS_DOWN : 0) | (this.down ? STATUS_DOWN : 0) |
(this.lastDown ? STATUS_LAST : 0) | (this.lastDown ? STATUS_LAST : 0) |
@ -183,13 +184,29 @@ export default class Mouse implements Card, Restorable<MouseState> {
{ {
const clampY = state.a; const clampY = state.a;
if (clampY) { if (clampY) {
this.clampYMin = holeRead(CLAMP_MIN_LOW) | (holeRead(CLAMP_MIN_HIGH) << 8); this.clampYMin =
this.clampYMax = holeRead(CLAMP_MAX_LOW) | (holeRead(CLAMP_MAX_HIGH) << 8); holeRead(CLAMP_MIN_LOW) |
debug('clampMouse Y', this.clampYMin, this.clampYMax); (holeRead(CLAMP_MIN_HIGH) << 8);
this.clampYMax =
holeRead(CLAMP_MAX_LOW) |
(holeRead(CLAMP_MAX_HIGH) << 8);
debug(
'clampMouse Y',
this.clampYMin,
this.clampYMax
);
} else { } else {
this.clampXMin = holeRead(CLAMP_MIN_LOW) | (holeRead(CLAMP_MIN_HIGH) << 8); this.clampXMin =
this.clampXMax = holeRead(CLAMP_MAX_LOW) | (holeRead(CLAMP_MAX_HIGH) << 8); holeRead(CLAMP_MIN_LOW) |
debug('clampMouse X', this.clampXMin, this.clampXMax); (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); state = clearCarry(state);
} }
@ -229,10 +246,10 @@ export default class Mouse implements Card, Restorable<MouseState> {
if (this.mode & MODE_INT_VBL) { if (this.mode & MODE_INT_VBL) {
this.serve |= INT_SCREEN; this.serve |= INT_SCREEN;
} }
if ((this.mode & MODE_INT_PRESS) && this.shouldIntPress) { if (this.mode & MODE_INT_PRESS && this.shouldIntPress) {
this.serve |= INT_PRESS; this.serve |= INT_PRESS;
} }
if ((this.mode & MODE_INT_MOVE) && this.shouldIntMove) { if (this.mode & MODE_INT_MOVE && this.shouldIntMove) {
this.serve |= INT_MOVE; this.serve |= INT_MOVE;
} }
if (this.serve) { if (this.serve) {
@ -255,8 +272,8 @@ export default class Mouse implements Card, Restorable<MouseState> {
setMouseXY(x: number, y: number, w: number, h: number) { setMouseXY(x: number, y: number, w: number, h: number) {
const rangeX = this.clampXMax - this.clampXMin; const rangeX = this.clampXMax - this.clampXMin;
const rangeY = this.clampYMax - this.clampYMin; const rangeY = this.clampYMax - this.clampYMin;
this.x = (x * rangeX / w + this.clampXMin) & 0xffff; this.x = ((x * rangeX) / w + this.clampXMin) & 0xffff;
this.y = (y * rangeY / h + this.clampYMin) & 0xffff; this.y = ((y * rangeY) / h + this.clampYMin) & 0xffff;
this.shouldIntMove = true; this.shouldIntMove = true;
} }
@ -318,7 +335,7 @@ export default class Mouse implements Card, Restorable<MouseState> {
serve: this.serve, serve: this.serve,
shouldIntMove: this.shouldIntMove, shouldIntMove: this.shouldIntMove,
shouldIntPress: this.shouldIntPress, 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 { bit, byte } from 'js/types';
import { debug } from '../util'; import { debug } from '../util';
const PATTERN = [ const PATTERN = [0xc5, 0x3a, 0xa3, 0x5c, 0xc5, 0x3a, 0xa3, 0x5c];
0xC5, 0x3A, 0xA3, 0x5C, 0xC5, 0x3A, 0xA3, 0x5C
];
const A0 = 0x01; const A0 = 0x01;
const A2 = 0x04; const A2 = 0x04;
@ -18,7 +16,6 @@ export default class NoSlotClock {
debug('NoSlotClock'); debug('NoSlotClock');
} }
private patternMatch() { private patternMatch() {
for (let idx = 0; idx < 8; idx++) { for (let idx = 0; idx < 8; idx++) {
let byte = 0; let byte = 0;
@ -53,7 +50,7 @@ export default class NoSlotClock {
const hour = now.getHours(); const hour = now.getHours();
const minutes = now.getMinutes(); const minutes = now.getMinutes();
const seconds = now.getSeconds(); const seconds = now.getSeconds();
const hundredths = (now.getMilliseconds() / 10); const hundredths = now.getMilliseconds() / 10;
this.bits = []; this.bits = [];
@ -113,4 +110,3 @@ export default class NoSlotClock {
// Setting the state makes no sense. // Setting the state makes no sense.
} }
} }

View File

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

View File

@ -12,7 +12,7 @@ const LOC = {
_RAMMID: 0x85, _RAMMID: 0x85,
_RAMHI: 0x86, _RAMHI: 0x86,
_RAMDATA: 0x87, _RAMDATA: 0x87,
BANK: 0x8F BANK: 0x8f,
} as const; } as const;
export class RAMFactorState { export class RAMFactorState {
@ -41,21 +41,21 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
} }
private sethi(val: byte) { private sethi(val: byte) {
this.ramhi = (val & 0xff); this.ramhi = val & 0xff;
} }
private setmid(val: byte) { 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.sethi(this.ramhi + 1);
} }
this.rammid = (val & 0xff); this.rammid = val & 0xff;
} }
private setlo(val: byte) { 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.setmid(this.rammid + 1);
} }
this.ramlo = (val & 0xff); this.ramlo = val & 0xff;
} }
private access(off: byte, val: byte) { private access(off: byte, val: byte) {
@ -105,7 +105,7 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
default: default:
break; break;
} }
this.loc = (this.ramhi << 16) | (this.rammid << 8) | (this.ramlo); this.loc = (this.ramhi << 16) | (this.rammid << 8) | this.ramlo;
/* /*
if (val === undefined) { if (val === undefined) {
@ -123,7 +123,7 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
} }
read(page: byte, off: byte) { read(page: byte, off: byte) {
return rom[this.firmware << 12 | (page - 0xC0) << 8 | off]; return rom[(this.firmware << 12) | ((page - 0xc0) << 8) | off];
} }
write() { write() {
@ -138,7 +138,7 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
return { return {
loc: this.loc, loc: this.loc,
firmware: this.firmware, 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.ramhi = (this.loc >> 16) & 0xff;
this.rammid = (this.loc >> 8) & 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 { debug, toHex } from '../util';
import { rom as smartPortRom } from '../roms/cards/smartport'; import { rom as smartPortRom } from '../roms/cards/smartport';
import { Card, Restorable, byte, word, rom } from '../types'; 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 { 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 createBlockDisk from '../formats/block';
import { DriveNumber } from '../formats/types'; import { DriveNumber } from '../formats/types';
@ -27,7 +38,11 @@ class Address {
lo: byte; lo: byte;
hi: byte; hi: byte;
constructor(private cpu: CPU6502, a: byte | word, b?: byte) { constructor(
private cpu: CPU6502,
a: byte,
b?: byte
) {
if (b === undefined) { if (b === undefined) {
this.lo = a & 0xff; this.lo = a & 0xff;
this.hi = a >> 8; this.hi = a >> 8;
@ -46,7 +61,10 @@ class Address {
} }
inc(val: byte) { 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() { readByte() {
@ -57,7 +75,7 @@ class Address {
const readLo = this.readByte(); const readLo = this.readByte();
const readHi = this.inc(1).readByte(); const readHi = this.inc(1).readByte();
return readHi << 8 | readLo; return (readHi << 8) | readLo;
} }
readAddress() { readAddress() {
@ -97,8 +115,8 @@ const BLOCK_LO = 0x46;
// const IO_ERROR = 0x27; // const IO_ERROR = 0x27;
const NO_DEVICE_CONNECTED = 0x28; const NO_DEVICE_CONNECTED = 0x28;
const WRITE_PROTECTED = 0x2B; const WRITE_PROTECTED = 0x2b;
const DEVICE_OFFLINE = 0x2F; const DEVICE_OFFLINE = 0x2f;
// const VOLUME_DIRECTORY_NOT_FOUND = 0x45; // const VOLUME_DIRECTORY_NOT_FOUND = 0x45;
// const NOT_A_PRODOS_DISK = 0x52; // const NOT_A_PRODOS_DISK = 0x52;
// const VOLUME_CONTROL_BLOCK_FULL = 0x55; // const VOLUME_CONTROL_BLOCK_FULL = 0x55;
@ -123,8 +141,9 @@ const DEVICE_TYPE_SCSI_HD = 0x07;
// $0D: Printer // $0D: Printer
// $0E: Clock // $0E: Clock
// $0F: Modem // $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 rom: rom;
private disks: BlockDisk[] = []; private disks: BlockDisk[] = [];
private busy: boolean[] = []; private busy: boolean[] = [];
@ -139,7 +158,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
) { ) {
if (options?.block) { if (options?.block) {
const dumbPortRom = new Uint8Array(smartPortRom); const dumbPortRom = new Uint8Array(smartPortRom);
dumbPortRom[0x07] = 0x3C; dumbPortRom[0x07] = 0x3c;
this.rom = dumbPortRom; this.rom = dumbPortRom;
debug('DumbPort card'); debug('DumbPort card');
} else { } else {
@ -221,7 +240,12 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
* readBlock * 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 drive=${driveNo}`);
this.debug(`read buffer=${buffer.toString()}`); this.debug(`read buffer=${buffer.toString()}`);
this.debug(`read block=$${toHex(block)}`); this.debug(`read block=$${toHex(block)}`);
@ -249,7 +273,12 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
* writeBlock * 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 drive=${driveNo}`);
this.debug(`write buffer=${buffer.toString()}`); this.debug(`write buffer=${buffer.toString()}`);
this.debug(`write block=$${toHex(block)}`); 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 blockOff = this.rom[0xff];
const smartOff = blockOff + 3; 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'); this.debug('block device entry');
cmd = this.cpu.read(0x00, COMMAND); cmd = this.cpu.read(0x00, COMMAND);
unit = this.cpu.read(0x00, UNIT); unit = this.cpu.read(0x00, UNIT);
const bufferAddr = new Address(this.cpu, ADDRESS_LO); const bufferAddr = new Address(this.cpu, ADDRESS_LO);
const blockAddr = new Address(this.cpu, BLOCK_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; const driveSlot = (unit & 0x70) >> 4;
buffer = bufferAddr.readAddress(); buffer = bufferAddr.readAddress();
@ -435,27 +465,42 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
default: // Unit 1 default: // Unit 1
switch (status) { switch (status) {
case 0: 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.writeByte(0xf0); // W/R Block device in drive
buffer.inc(1).writeByte(blocks & 0xff); // 1600 blocks buffer.inc(1).writeByte(blocks & 0xff); // 1600 blocks
buffer.inc(2).writeByte((blocks & 0xff00) >> 8); buffer
buffer.inc(3).writeByte((blocks & 0xff0000) >> 16); .inc(2)
.writeByte((blocks & 0xff00) >> 8);
buffer
.inc(3)
.writeByte((blocks & 0xff0000) >> 16);
state.x = 4; state.x = 4;
state.y = 0; state.y = 0;
state.a = 0; state.a = 0;
state.s &= ~flags.C; state.s &= ~flags.C;
break; break;
case 3: 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.writeByte(0xf0); // W/R Block device in drive
buffer.inc(1).writeByte(blocks & 0xff); // Blocks low byte buffer.inc(1).writeByte(blocks & 0xff); // Blocks low byte
buffer.inc(2).writeByte((blocks & 0xff00) >> 8); // Blocks middle byte buffer
buffer.inc(3).writeByte((blocks & 0xff0000) >> 16); // Blocks high byte .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 buffer.inc(4).writeByte(ID.length); // Vendor ID length
for (let idx = 0; idx < ID.length; idx++) { // Vendor ID for (let idx = 0; idx < ID.length; idx++) {
buffer.inc(5 + idx).writeByte(ID.charCodeAt(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(22).writeByte(0x0); // Device Subtype
buffer.inc(23).writeWord(0x0101); // Version buffer.inc(23).writeWord(0x0101); // Version
state.x = 24; state.x = 24;
@ -515,41 +560,38 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
getState() { getState() {
return { return {
disks: this.disks.map( disks: this.disks.map((disk) => {
(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) => {
const result: BlockDisk = { const result: BlockDisk = {
blocks: disk.blocks.map( blocks: disk.blocks.map((block) => new Uint8Array(block)),
(block) => new Uint8Array(block)
),
encoding: ENCODING_BLOCK, encoding: ENCODING_BLOCK,
format: disk.format, format: disk.format,
readOnly: disk.readOnly, readOnly: disk.readOnly,
metadata: { ...disk.metadata }, metadata: { ...disk.metadata },
}; };
return result; 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 volume = 254;
let readOnly = false; let readOnly = false;
if (fmt === '2mg') { if (fmt === '2mg') {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,11 @@ interface ControlStripProps {
* @param e Whether or not this is a //e * @param e Whether or not this is a //e
* @returns ControlStrip component * @returns ControlStrip component
*/ */
export const ControlStrip = ({ apple2, e, toggleDebugger }: ControlStripProps) => { export const ControlStrip = ({
apple2,
e,
toggleDebugger,
}: ControlStripProps) => {
const [showOptions, setShowOptions] = useState(false); const [showOptions, setShowOptions] = useState(false);
const [io, setIO] = useState<Apple2IO>(); const [io, setIO] = useState<Apple2IO>();
const options = useContext(OptionsContext); const options = useContext(OptionsContext);
@ -55,23 +59,22 @@ export const ControlStrip = ({ apple2, e, toggleDebugger }: ControlStripProps) =
} }
}, [apple2, e, options]); }, [apple2, e, options]);
const doReset = useCallback(() => const doReset = useCallback(() => apple2?.reset(), [apple2]);
apple2?.reset(), [apple2]);
const doReadme = useCallback(() => const doReadme = useCallback(() => window.open(README, '_blank'), []);
window.open(README, '_blank'), []);
const doShowOptions = useCallback(() => const doShowOptions = useCallback(() => setShowOptions(true), []);
setShowOptions(true), []);
const doCloseOptions = useCallback(() => const doCloseOptions = useCallback(() => setShowOptions(false), []);
setShowOptions(false), []);
const doToggleFullPage = useCallback(() => const doToggleFullPage = useCallback(
options.setOption( () =>
SCREEN_FULL_PAGE, options.setOption(
!options.getOption(SCREEN_FULL_PAGE) SCREEN_FULL_PAGE,
), [options]); !options.getOption(SCREEN_FULL_PAGE)
),
[options]
);
useHotKey('F2', doToggleFullPage); useHotKey('F2', doToggleFullPage);
useHotKey('F4', doShowOptions); useHotKey('F4', doShowOptions);
@ -82,12 +85,20 @@ export const ControlStrip = ({ apple2, e, toggleDebugger }: ControlStripProps) =
<OptionsModal isOpen={showOptions} onClose={doCloseOptions} /> <OptionsModal isOpen={showOptions} onClose={doCloseOptions} />
<Inset> <Inset>
<CPUMeter apple2={apple2} /> <CPUMeter apple2={apple2} />
<ControlButton onClick={toggleDebugger} title="Toggle Debugger" icon="bug" /> <ControlButton
onClick={toggleDebugger}
title="Toggle Debugger"
icon="bug"
/>
<AudioControl apple2={apple2} /> <AudioControl apple2={apple2} />
<Printer io={io} slot={1} /> <Printer io={io} slot={1} />
<div style={{ flexGrow: 1 }} /> <div style={{ flexGrow: 1 }} />
<ControlButton onClick={doReadme} title="About" icon="info" /> <ControlButton onClick={doReadme} title="About" icon="info" />
<ControlButton onClick={doShowOptions} title="Options (F4)" icon="cog" /> <ControlButton
onClick={doShowOptions}
title="Options (F4)"
icon="cog"
/>
</Inset> </Inset>
{e && ( {e && (
<div className={styles.reset} onClick={doReset}> <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 { h, JSX, RefObject } from 'preact';
import { useEffect, useRef } from 'preact/hooks'; import { useEffect, useRef } from 'preact/hooks';
import { loadLocalFile } from './util/files'; import { loadLocalFile } from './util/files';
import { spawn } from './util/promises'; 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; storage: MassStorage<T> | undefined;
driveNo?: DriveNumber; driveNo?: DriveNumber;
formats: typeof FLOPPY_FORMATS formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS;
| typeof BLOCK_FORMATS
| typeof DISK_FORMATS;
dropRef?: RefObject<HTMLElement>; dropRef?: RefObject<HTMLElement>;
onError: (error: unknown) => void; onError: (error: unknown) => void;
} }
@ -32,7 +37,11 @@ export const DiskDragTarget = ({
event.preventDefault(); event.preventDefault();
const dt = event.dataTransfer; const dt = event.dataTransfer;
if (dt) { 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'; dt.dropEffect = 'copy';
} else { } else {
dt.dropEffect = 'none'; dt.dropEffect = 'none';
@ -54,13 +63,18 @@ export const DiskDragTarget = ({
const onDrop = (event: DragEvent) => { const onDrop = (event: DragEvent) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); 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; const dt = event.dataTransfer;
if (dt?.files.length === 1 && storage) { if (dt?.files.length === 1 && storage) {
spawn(async () => { spawn(async () => {
try { try {
await loadLocalFile(storage, formats, targetDrive, dt.files[0]); await loadLocalFile(
storage,
formats,
targetDrive,
dt.files[0]
);
} catch (e) { } catch (e) {
onError(e); onError(e);
} }
@ -68,8 +82,18 @@ export const DiskDragTarget = ({
} else if (dt?.files.length === 2 && storage) { } else if (dt?.files.length === 2 && storage) {
spawn(async () => { spawn(async () => {
try { try {
await loadLocalFile(storage, formats, 1, dt.files[0]); await loadLocalFile(
await loadLocalFile(storage, formats, 2, dt.files[1]); storage,
formats,
1,
dt.files[0]
);
await loadLocalFile(
storage,
formats,
2,
dt.files[1]
);
} catch (e) { } catch (e) {
onError(e); onError(e);
} }

View File

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

View File

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

View File

@ -8,9 +8,18 @@ import { CPU6502 } from '@whscullin/cpu6502';
import { BlockDisk } from './BlockDisk'; import { BlockDisk } from './BlockDisk';
import { ErrorModal } from './ErrorModal'; import { ErrorModal } from './ErrorModal';
import { ProgressModal } from './ProgressModal'; 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 { 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 { spawn, Ready } from './util/promises';
import styles from './css/Drives.module.scss'; 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>({ const [smartData1, setSmartData1] = useState<DiskIIData>({
on: false, on: false,
number: 1, number: 1,
name: 'HD 1' name: 'HD 1',
}); });
const [smartData2, setSmartData2] = useState<DiskIIData>({ const [smartData2, setSmartData2] = useState<DiskIIData>({
on: false, on: false,
number: 2, number: 2,
name: 'HD 2' name: 'HD 2',
}); });
const hash = useHash(); const hash = useHash();
@ -97,25 +106,30 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
const isJson = hashPart.match(/\.json$/i); const isJson = hashPart.match(/\.json$/i);
if (isHttp && !isJson) { if (isHttp && !isJson) {
loading++; loading++;
controllers.push(spawn(async (signal) => { controllers.push(
try { spawn(async (signal) => {
await loadHttpUnknownFile( try {
smartStorageBroker, await loadHttpUnknownFile(
driveNo, smartStorageBroker,
hashPart, driveNo,
signal, hashPart,
onProgress); signal,
} catch (e) { onProgress
setError(e); );
} } catch (e) {
if (--loading === 0) { setError(e);
ready.onReady(); }
} if (--loading === 0) {
setCurrent(0); ready.onReady();
setTotal(0); }
})); setCurrent(0);
setTotal(0);
})
);
} else { } 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)); loadJSON(disk2, driveNo, url).catch((e) => setError(e));
} }
} }
@ -123,7 +137,8 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
if (!loading) { if (!loading) {
ready.onReady(); ready.onReady();
} }
return () => controllers.forEach((controller) => controller.abort()); return () =>
controllers.forEach((controller) => controller.abort());
} }
}, [hash, onProgress, ready, storageDevices]); }, [hash, onProgress, ready, storageDevices]);
@ -132,10 +147,10 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
const setSmartData = [setSmartData1, setSmartData2]; const setSmartData = [setSmartData1, setSmartData2];
const callbacks: Callbacks = { const callbacks: Callbacks = {
driveLight: (drive, on) => { driveLight: (drive, on) => {
setData[drive - 1]?.(data => ({ ...data, on })); setData[drive - 1]?.((data) => ({ ...data, on }));
}, },
label: (drive, name, side) => { label: (drive, name, side) => {
setData[drive - 1]?.(data => ({ setData[drive - 1]?.((data) => ({
...data, ...data,
name: name ?? `Disk ${drive}`, name: name ?? `Disk ${drive}`,
side, side,
@ -143,27 +158,31 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
}, },
dirty: () => { dirty: () => {
// do nothing // do nothing
} },
}; };
const smartPortCallbacks: Callbacks = { const smartPortCallbacks: Callbacks = {
driveLight: (drive, on) => { driveLight: (drive, on) => {
setSmartData[drive - 1]?.(data => ({ ...data, on })); setSmartData[drive - 1]?.((data) => ({ ...data, on }));
}, },
label: (drive, name, side) => { label: (drive, name, side) => {
setSmartData[drive - 1]?.(data => ({ setSmartData[drive - 1]?.((data) => ({
...data, ...data,
name: name ?? `HD ${drive}`, name: name ?? `HD ${drive}`,
side, side,
})); }));
}, },
dirty: () => {/* Unused */ } dirty: () => {
/* Unused */
},
}; };
if (cpu && io) { if (cpu && io) {
const disk2 = new Disk2(io, callbacks, sectors); const disk2 = new Disk2(io, callbacks, sectors);
io.setSlot(6, disk2); io.setSlot(6, disk2);
const smartPort = new SmartPort(cpu, smartPortCallbacks, { block: !enhanced }); const smartPort = new SmartPort(cpu, smartPortCallbacks, {
block: !enhanced,
});
io.setSlot(7, smartPort); io.setSlot(7, smartPort);
const smartStorageBroker = new SmartStorageBroker(disk2, 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'; import styles from './css/ErrorModal.module.scss';
export interface ErrorProps { export interface ErrorProps {
error: unknown | undefined; error: unknown;
setError: (error: string | undefined) => void; setError: (error: string | undefined) => void;
} }
@ -32,9 +32,7 @@ export const ErrorModal = ({ error, setError }: ErrorProps) => {
onClose={onClose} onClose={onClose}
> >
<ModalContent> <ModalContent>
<div className={styles.errorModal}> <div className={styles.errorModal}>{errorStr}</div>
{errorStr}
</div>
</ModalContent> </ModalContent>
<ModalFooter> <ModalFooter>
<button onClick={onClose}>OK</button> <button onClick={onClose}>OK</button>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@
.diskLight { .diskLight {
margin: 5px; margin: 5px;
background-image: url("../../../css/green-off-16.png"); background-image: url('../../../css/green-off-16.png');
background-size: 16px 16px; background-size: 16px 16px;
flex-shrink: 0; flex-shrink: 0;
width: 16px; width: 16px;
@ -14,7 +14,7 @@
} }
.diskLight.on { .diskLight.on {
background-image: url("../../../css/green-on-16.png"); background-image: url('../../../css/green-on-16.png');
} }
.diskLabel { .diskLabel {
@ -29,12 +29,12 @@
user-select: none; user-select: none;
} }
@media only screen and (min-resolution: 1.25dppx) { @media only screen and (resolution >= 1.25dppx) {
.diskLight { .diskLight {
background-image: url("../../../css/green-off-32.png"); background-image: url('../../../css/green-off-32.png');
} }
.diskLight.on { .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-size: 14px;
font-family: sans-serif; font-family: sans-serif;
a[role="button"] { a[role='button'] {
text-decoration: none; text-decoration: none;
} }
button, button,
a[role="button"], a[role='button'],
input[type="file"]::file-selector-button { input[type='file']::file-selector-button {
background: #44372c; background: #44372c;
color: #fff; color: #fff;
padding: 2px 8px; padding: 2px 8px;
@ -24,26 +24,26 @@
} }
button:hover, button:hover,
a[role="button"]:hover, a[role='button']:hover,
input[type="file"]::file-selector-button { input[type='file']::file-selector-button {
background-color: #55473d; background-color: #55473d;
border: 1px outset #66594e; border: 1px outset #66594e;
} }
button:active, button:active,
a[role="button"]:active, a[role='button']:active,
input[type="file"]::file-selector-button { input[type='file']::file-selector-button {
background-color: #22150a; background-color: #22150a;
border: 1px outset #44372c; border: 1px outset #44372c;
} }
button:focus, button:focus,
a[role="button"]:focus, a[role='button']:focus,
input[type="file"]::file-selector-button { input[type='file']::file-selector-button {
outline: none; outline: none;
} }
input[type="checkbox"] { input[type='checkbox'] {
appearance: none; appearance: none;
background-color: #65594d; background-color: #65594d;
border: 1px inset #65594d; border: 1px inset #65594d;
@ -54,14 +54,14 @@
position: relative; position: relative;
} }
input[type="checkbox"]:checked { input[type='checkbox']:checked {
background-color: #65594d; background-color: #65594d;
border: 1px inset #65594d; border: 1px inset #65594d;
color: #0d0; color: #0d0;
} }
input[type="checkbox"]:checked::after { input[type='checkbox']:checked::after {
content: "\2716"; content: '\2716';
font-size: 12px; font-size: 12px;
position: absolute; position: absolute;
top: 0; top: 0;

View File

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

View File

@ -6,7 +6,7 @@
.diskLight { .diskLight {
margin: 5px; margin: 5px;
background-image: url("../../../css/red-off-16.png"); background-image: url('../../../css/red-off-16.png');
background-size: 16px 16px; background-size: 16px 16px;
flex-shrink: 0; flex-shrink: 0;
width: 16px; width: 16px;
@ -14,7 +14,7 @@
} }
.diskLight.on { .diskLight.on {
background-image: url("../../../css/red-on-16.png"); background-image: url('../../../css/red-on-16.png');
} }
.diskLabel { .diskLabel {
@ -29,12 +29,12 @@
user-select: none; user-select: none;
} }
@media only screen and (min-resolution: 1.25dppx) { @media only screen and (resolution >= 1.25dppx) {
.diskLight { .diskLight {
background-image: url("../../../css/red-off-32.png"); background-image: url('../../../css/red-off-32.png');
} }
.diskLight.on { .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; margin: 0;
padding: 3px 0 0 10; padding: 3px 0 0 10;
color: black; color: black;
font-family: "Adobe Garamond Pro", Garamond, Times, serif; font-family: 'Adobe Garamond Pro', Garamond, Times, serif;
font-size: 13px; font-size: 13px;
font-weight: normal; font-weight: normal;
user-select: none; user-select: none;

View File

@ -177,7 +177,7 @@
} }
.key-OPEN_APPLE div { .key-OPEN_APPLE div {
background-image: url("../../../img/open-apple24.png"); background-image: url('../../../img/open-apple24.png');
width: 24px; width: 24px;
height: 24px; height: 24px;
bottom: 1px; bottom: 1px;
@ -185,7 +185,7 @@
} }
.key-CLOSED_APPLE div { .key-CLOSED_APPLE div {
background-image: url("../../../img/closed-apple24.png"); background-image: url('../../../img/closed-apple24.png');
width: 24px; width: 24px;
height: 24px; height: 24px;
bottom: 1px; bottom: 1px;
@ -201,9 +201,9 @@
} }
.key-OPEN_APPLE.active div { .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 { .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 { .modalOverlay {
position: fixed; position: fixed;
left: 0; inset: 0;
right: 0;
top: 0;
bottom: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -63,7 +60,7 @@
text-align: right; text-align: right;
user-select: none; user-select: none;
a[role="button"], a[role='button'],
button { button {
margin: 0 0 0 5px; margin: 0 0 0 5px;
min-width: 75px; min-width: 75px;

View File

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

View File

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

View File

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

View File

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

View File

@ -56,7 +56,14 @@ const HiresPreview = ({ binary }: { binary: Uint8Array }) => {
vm.blit(); 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(); 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(() => { useEffect(() => {
if (fileData) { if (fileData) {
const { binary, text } = fileData; const { binary, text } = fileData;
const binaryBlob = new Blob( const binaryBlob = new Blob([binary], {
[binary], type: 'application/octet-stream',
{ type: 'application/octet-stream' } });
);
const binaryHref = window.URL.createObjectURL(binaryBlob); const binaryHref = window.URL.createObjectURL(binaryBlob);
setBinaryHref(binaryHref); setBinaryHref(binaryHref);
const textBlob = new Blob( const textBlob = new Blob([text], {
[text], type: 'application/octet-stream',
{ type: 'application/octet-stream' } });
);
const textHref = window.URL.createObjectURL(textBlob); const textHref = window.URL.createObjectURL(textBlob);
setTextHref(textHref); setTextHref(textHref);
} }
@ -142,7 +154,7 @@ export const FileViewer = ({ fileData, onClose }: FileViewerProps) => {
<div className={styles.fileViewer}> <div className={styles.fileViewer}>
<HiresPreview binary={binary} /> <HiresPreview binary={binary} />
<DoubleHiresPreview binary={binary} /> <DoubleHiresPreview binary={binary} />
<pre className={styles.textViewer} tabIndex={-1} > <pre className={styles.textViewer} tabIndex={-1}>
{text} {text}
</pre> </pre>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { Debugger } from '@whscullin/cpu6502';
import Disk2 from 'js/cards/disk2'; import Disk2 from 'js/cards/disk2';
import SmartPort from 'js/cards/smartport'; import SmartPort from 'js/cards/smartport';
import Debugger from 'js/debugger';
import { import {
BlockFormat, BlockFormat,
BLOCK_FORMATS, BLOCK_FORMATS,
@ -46,10 +46,10 @@ export const getNameAndExtension = (url: string) => {
}; };
export const loadLocalFile = ( export const loadLocalFile = (
storage: MassStorage<FloppyFormat|BlockFormat>, storage: MassStorage<FloppyFormat | BlockFormat>,
formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS, formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS,
driveNo: DriveNumber, driveNo: DriveNumber,
file: File, file: File
) => { ) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const fileReader = new FileReader(); const fileReader = new FileReader();
@ -80,7 +80,11 @@ export const loadLocalFile = (
* @param file Browser File object to load * @param file Browser File object to load
* @returns true if successful * @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); 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 * @param file Browser File object to load
* @returns true if successful * @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); return loadLocalFile(disk2, FLOPPY_FORMATS, driveNo, file);
}; };
@ -110,13 +118,13 @@ export const loadLocalNibbleFile = (disk2: Disk2, driveNo: DriveNumber, file: Fi
export const loadJSON = async ( export const loadJSON = async (
disk2: Disk2, disk2: Disk2,
driveNo: DriveNumber, driveNo: DriveNumber,
url: string, url: string
) => { ) => {
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error(`Error loading: ${response.statusText}`); 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)) { if (!includes(FLOPPY_FORMATS, data.type)) {
throw new Error(`Type "${data.type}" not recognized.`); throw new Error(`Type "${data.type}" not recognized.`);
} }
@ -127,7 +135,7 @@ export const loadJSON = async (
export const loadHttpFile = async ( export const loadHttpFile = async (
url: string, url: string,
signal?: AbortSignal, signal?: AbortSignal,
onProgress?: ProgressCallback, onProgress?: ProgressCallback
): Promise<ArrayBuffer> => { ): Promise<ArrayBuffer> => {
const response = await fetch(url, signal ? { signal } : {}); const response = await fetch(url, signal ? { signal } : {});
if (!response.ok) { if (!response.ok) {
@ -137,7 +145,10 @@ export const loadHttpFile = async (
throw new Error('Error loading: no body'); throw new Error('Error loading: no body');
} }
const reader = response.body.getReader(); 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; let received = 0;
const chunks: Uint8Array[] = []; const chunks: Uint8Array[] = [];
@ -223,7 +234,7 @@ export const loadHttpUnknownFile = async (
driveNo: DriveNumber, driveNo: DriveNumber,
url: string, url: string,
signal?: AbortSignal, signal?: AbortSignal,
onProgress?: ProgressCallback, onProgress?: ProgressCallback
) => { ) => {
const data = await loadHttpFile(url, signal, onProgress); const data = await loadHttpFile(url, signal, onProgress);
const { name, ext } = getNameAndExtension(url); const { name, ext } = getNameAndExtension(url);
@ -231,9 +242,17 @@ export const loadHttpUnknownFile = async (
}; };
export class SmartStorageBroker implements MassStorage<unknown> { 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 (includes(DISK_FORMATS, ext)) {
if (data.byteLength >= 800 * 1024) { if (data.byteLength >= 800 * 1024) {
if (includes(BLOCK_FORMATS, ext)) { if (includes(BLOCK_FORMATS, ext)) {
@ -265,7 +284,11 @@ export class SmartStorageBroker implements MassStorage<unknown> {
* @param debug Debugger object * @param debug Debugger object
* @returns resolves to true if successful * @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) => { return new Promise((resolve, _reject) => {
const fileReader = new FileReader(); const fileReader = new FileReader();
fileReader.onload = function () { fileReader.onload = function () {

View File

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

View File

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

View File

@ -1,370 +0,0 @@
import { debug, toHex } from './util';
import { byte, word } from './types';
import { CPU6502, DebugInfo, flags, sizes } from '@whscullin/cpu6502';
export interface DebuggerContainer {
run: () => void;
stop: () => void;
isRunning: () => boolean;
}
type symbols = { [key: number]: string };
type breakpointFn = (info: DebugInfo) => boolean;
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' : '-',
].join('');
export default class Debugger {
private verbose = false;
private maxTrace = 256;
private trace: DebugInfo[] = [];
private breakpoints: Map<word, breakpointFn> = new Map();
private symbols: symbols = {};
constructor(private cpu: CPU6502, private container: DebuggerContainer) {}
stepCycles(cycles: number) {
this.cpu.stepCyclesDebug(this.verbose ? 1 : cycles, () => {
const info = this.cpu.getDebugInfo();
if (this.breakpoints.get(info.pc)?.(info)) {
debug('breakpoint', this.printDebugInfo(info));
this.container.stop();
return true;
}
if (this.verbose) {
debug(this.printDebugInfo(info));
} else {
this.updateTrace(info);
}
});
}
break = () => {
this.container.stop();
};
step = () => {
this.cpu.step(() => {
const info = this.cpu.getDebugInfo();
debug(this.printDebugInfo(info));
this.updateTrace(info);
});
};
continue = () => {
this.container.run();
};
/**
* Restart at a given memory address.
*
* @param address Address to start execution
*/
runAt = (address: word) => {
this.cpu.reset();
this.cpu.setPC(address);
};
isRunning = () =>
this.container.isRunning();
setVerbose = (verbose: boolean) => {
this.verbose = verbose;
};
setMaxTrace = (maxTrace: number) => {
this.maxTrace = maxTrace;
};
getTrace = (count?: number) => {
return this.trace.slice(count ? -count : undefined).map(this.printDebugInfo).join('\n');
};
printTrace = (count?: number) => {
debug(this.getTrace(count));
};
getStack = (size?: number) => {
const { sp } = this.cpu.getDebugInfo();
const stack = [];
let max = 255;
let min = 0;
if (size) {
if ((sp - 3) >= (255 - size)) {
min = Math.max(255 - size + 1, 0);
} else {
max = Math.min(sp + size - 4, 255);
min = Math.max(sp - 3, 0);
}
}
for (let addr = max; addr >= min; addr--) {
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))) {
stack.push(`${isSP} ${addrStr} ${valStr}`);
}
}
return stack.join('\n');
};
setBreakpoint = (addr: word, exp?: breakpointFn) => {
this.breakpoints.set(addr, exp || alwaysBreak);
};
clearBreakpoint = (addr: word) => {
this.breakpoints.delete(addr);
};
listBreakpoints = () => {
for(const [addr, fn] of this.breakpoints.entries()) {
debug(toHex(addr, 4), fn);
}
};
addSymbols = (symbols: symbols) => {
this.symbols = { ...this.symbols, ...symbols };
};
printDebugInfo = (info: DebugInfo) => {
const { pc, cmd } = info;
const symbol = this.padWithSymbol(pc);
return [
toHex(pc, 4),
'- ', symbol,
this.dumpRegisters(info),
' ',
this.dumpRawOp(cmd),
' ',
this.dumpOp(pc, cmd)
].join('');
};
dumpPC = (pc: word) => {
const b = this.cpu.read(pc);
const op = this.cpu.getOpInfo(b);
const size = sizes[op.mode];
let result = toHex(pc, 4) + '- ';
result += this.padWithSymbol(pc);
const cmd = new Array<number>(size);
for (let idx = 0, jdx = pc; idx < size; idx++, jdx++) {
cmd[idx] = this.cpu.read(jdx);
}
result += this.dumpRawOp(cmd) + ' ' + this.dumpOp(pc, cmd);
return result;
};
dumpRegisters = (debugInfo?: DebugInfo) => {
if (debugInfo === undefined) {
debugInfo = this.cpu.getDebugInfo();
}
const { ar, xr, yr, sr, sp } = debugInfo;
return [
'A=' + toHex(ar),
' X=' + toHex(xr),
' Y=' + toHex(yr),
' P=' + toHex(sr),
' S=' + toHex(sp),
' ',
dumpStatusRegister(sr),
].join('');
};
dumpPage = (start: byte, end?: byte) => {
let result = '';
if (end === undefined) {
end = start;
}
for (let page = start; page <= end; page++) {
for (let idx = 0; idx < 16; idx++) {
result += toHex(page) + toHex(idx << 4) + ': ';
for (let jdx = 0; jdx < 16; jdx++) {
const b = this.cpu.read(page, idx * 16 + jdx);
result += toHex(b) + ' ';
}
result += ' ';
for (let jdx = 0; jdx < 16; jdx++) {
const b = this.cpu.read(page, idx * 16 + jdx) & 0x7f;
if (b >= 0x20 && b < 0x7f) {
result += String.fromCharCode(b);
} else {
result += '.';
}
}
result += '\n';
}
}
return result;
};
/**
* Reads a range of memory. Will wrap at memory limit.
*
* @param address Starting address to read memory
* @param length Length of memory to read.
* @returns Byte array containing memory
*/
getMemory(address: word, length: word) {
const bytes = new Uint8Array(length);
for (let idx = 0; idx < length; idx++) {
address &= 0xffff;
bytes[idx] = this.cpu.read(address++);
}
return bytes;
}
/**
* Writes a range of memory. Will wrap at memory limit.
*
* @param address Starting address to write memory
* @param bytes Data to write
*/
setMemory(address: word, bytes: Uint8Array) {
for (const byte of bytes) {
address &= 0xffff;
this.cpu.write(address++, byte);
}
}
list = (pc: word) => {
const results = [];
for (let idx = 0; idx < 20; idx++) {
const b = this.cpu.read(pc);
const op = this.cpu.getOpInfo(b);
results.push(this.dumpPC(pc));
pc += sizes[op.mode];
}
return results;
};
private updateTrace(info: DebugInfo) {
this.trace.push(info);
if (this.trace.length > this.maxTrace) {
this.trace.shift();
}
}
private padWithSymbol(pc: word): string {
const padding = ' ';
const symbol = this.symbols[pc];
let result: string = padding;
if (symbol) {
result = `${symbol}${padding.substring(symbol.length)}`;
}
return result;
}
private dumpRawOp(parts: byte[]) {
const result = new Array(4);
for (let idx = 0; idx < 4; idx++) {
if (idx < parts.length) {
result[idx] = toHex(parts[idx]);
} else {
result[idx] = ' ';
}
}
return result.join(' ');
}
private dumpOp(pc: word, parts: byte[]) {
const op = this.cpu.getOpInfo(parts[0]);
const lsb = parts[1];
const msb = parts[2];
const addr = (msb << 8) | lsb;
let val;
let off;
const toHexOrSymbol = (v: word, n?: number) => (
this.symbols[v] || ('$' + toHex(v, n))
);
let result = op.name + ' ';
switch (op.mode) {
case 'implied':
break;
case 'immediate':
result += `#${toHexOrSymbol(lsb)}`;
break;
case 'absolute':
result += `${toHexOrSymbol(addr, 4)}`;
break;
case 'zeroPage':
result += `${toHexOrSymbol(lsb)}`;
break;
case 'relative':
{
off = lsb;
if (off > 127) {
off -= 256;
}
pc += off + 2;
result += `${toHexOrSymbol(pc, 4)} (${off})`;
}
break;
case 'absoluteX':
result += `${toHexOrSymbol(addr, 4)},X`;
break;
case 'absoluteY':
result += `${toHexOrSymbol(addr, 4)},Y`;
break;
case 'zeroPageX':
result += `${toHexOrSymbol(lsb)},X`;
break;
case 'zeroPageY':
result += `${toHexOrSymbol(lsb)},Y`;
break;
case 'absoluteIndirect':
result += `(${toHexOrSymbol(addr, 4)})`;
break;
case 'zeroPageXIndirect':
result += `(${toHexOrSymbol(lsb)},X)`;
break;
case 'zeroPageIndirectY':
result += `(${toHexOrSymbol(lsb)},),Y`;
break;
case 'accumulator':
result += 'A';
break;
case 'zeroPageIndirect':
result += `(${toHexOrSymbol(lsb)})`;
break;
case 'absoluteXIndirect':
result += `(${toHexOrSymbol(addr, 4)},X)`;
break;
case 'zeroPage_relative':
val = lsb;
off = msb;
if (off > 127) {
off -= 256;
}
pc += off + 2;
result += `${toHexOrSymbol(val)},${toHexOrSymbol(pc, 4)} (${off})`;
break;
default:
break;
}
return result;
}
}

View File

@ -20,9 +20,9 @@ const OFFSETS = {
/** Header length (2 bytes) */ /** Header length (2 bytes) */
HEADER_LENGTH: 0x08, HEADER_LENGTH: 0x08,
/** Version number (2 bytes). (Version of what? Format? Image?). */ /** Version number (2 bytes). (Version of what? Format? Image?). */
VERSION: 0x0A, VERSION: 0x0a,
/** Image format ID (4 bytes) */ /** Image format ID (4 bytes) */
FORMAT: 0x0C, FORMAT: 0x0c,
/** Flags and DOS 3.3 volume number */ /** Flags and DOS 3.3 volume number */
FLAGS: 0x10, FLAGS: 0x10,
/** /**
@ -40,7 +40,7 @@ const OFFSETS = {
* Length of disk data in bytes (4 bytes). (143,360 bytes for 5.25" * Length of disk data in bytes (4 bytes). (143,360 bytes for 5.25"
* floppies; 512 × blocks for ProDOS volumes.) * 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). * 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 * 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 length in bytes (4 bytes). Must be zero if there is no
* creator data. * creator data.
*/ */
CREATOR_DATA_LENGTH: 0x2C, CREATOR_DATA_LENGTH: 0x2c,
/** Padding (16 bytes). Must be zero. */ /** Padding (16 bytes). Must be zero. */
PADDING: 0x30, PADDING: 0x30,
} as const; } as const;
const FLAGS = { const FLAGS = {
READ_ONLY: 0x80000000, READ_ONLY: 0x80000000,
VOLUME_VALID: 0x00000100, VOLUME_VALID: 0x00000100,
VOLUME_MASK: 0x000000FF VOLUME_MASK: 0x000000ff,
} as const; } as const;
export enum FORMAT { export enum FORMAT {
@ -92,16 +92,22 @@ export interface HeaderData {
export function read2MGHeader(rawData: ArrayBuffer): HeaderData { export function read2MGHeader(rawData: ArrayBuffer): HeaderData {
const prefix = new DataView(rawData); const prefix = new DataView(rawData);
const decoder = new TextDecoder('ascii'); 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') { if (signature !== '2IMG') {
throw new Error(`Unrecognized 2mg signature: ${signature}`); 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); const headerLength = prefix.getInt16(OFFSETS.HEADER_LENGTH, true);
if (headerLength !== 64) { 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 flags = prefix.getInt32(OFFSETS.FLAGS, true);
const blocks = prefix.getInt32(OFFSETS.BLOCKS, true); const blocks = prefix.getInt32(OFFSETS.BLOCKS, true);
const offset = prefix.getInt32(OFFSETS.DATA_OFFSET, 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 commentOffset = prefix.getInt32(OFFSETS.COMMENT, true);
const commentLength = prefix.getInt32(OFFSETS.COMMENT_LENGTH, true); const commentLength = prefix.getInt32(OFFSETS.COMMENT_LENGTH, true);
const creatorDataOffset = prefix.getInt32(OFFSETS.CREATOR_DATA, 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 // 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. // 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. // However we do check that it's correct if the image _is_ ProDOS.
if (format === FORMAT.ProDOS && blocks * 512 !== bytes) { 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) { 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) { 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; const dataEnd = offset + bytes;
if (commentOffset && commentOffset < dataEnd) { 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; const commentEnd = commentOffset ? commentOffset + commentLength : dataEnd;
if (commentEnd > prefix.byteLength) { 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) { 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) { 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 } = {}; const extras: { comment?: string; creatorData?: ReadonlyUint8Array } = {};
if (commentOffset) { if (commentOffset) {
extras.comment = new TextDecoder('utf-8').decode( extras.comment = new TextDecoder('utf-8').decode(
new Uint8Array(rawData, commentOffset, commentLength)); new Uint8Array(rawData, commentOffset, commentLength)
);
} }
if (creatorDataOffset) { if (creatorDataOffset) {
extras.creatorData = new Uint8Array(rawData, creatorDataOffset, creatorDataLength); extras.creatorData = new Uint8Array(
rawData,
creatorDataOffset,
creatorDataLength
);
} }
const readOnly = (flags & FLAGS.READ_ONLY) !== 0; const readOnly = (flags & FLAGS.READ_ONLY) !== 0;
@ -161,7 +191,7 @@ export function read2MGHeader(rawData: ArrayBuffer): HeaderData {
offset, offset,
readOnly, readOnly,
volume, volume,
...extras ...extras,
}; };
} }
@ -177,7 +207,10 @@ export function read2MGHeader(rawData: ArrayBuffer): HeaderData {
* @returns 2mg prefix and suffix for creating a 2mg disk image * @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) { if (!headerData) {
headerData = { headerData = {
bytes: blocks * 512, bytes: blocks * 512,
@ -197,7 +230,9 @@ export const create2MGFragments = (headerData: HeaderData | null, { blocks } : {
const prefix = new Uint8Array(64); const prefix = new Uint8Array(64);
const prefixView = new DataView(prefix.buffer); 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 readOnlyFlag = headerData.readOnly ? FLAGS.READ_ONLY : 0;
const flags = volumeFlags | readOnlyFlag; const flags = volumeFlags | readOnlyFlag;
const prefixLength = prefix.length; const prefixLength = prefix.length;
@ -252,8 +287,13 @@ export const create2MGFragments = (headerData: HeaderData | null, { blocks } : {
* @returns 2MS * @returns 2MS
*/ */
export const create2MGFromBlockDisk = (headerData: HeaderData | null, { blocks }: BlockDisk): ArrayBuffer => { export const create2MGFromBlockDisk = (
const { prefix, suffix } = create2MGFragments(headerData, { blocks: blocks.length }); headerData: HeaderData | null,
{ blocks }: BlockDisk
): ArrayBuffer => {
const { prefix, suffix } = create2MGFragments(headerData, {
blocks: blocks.length,
});
const imageLength = prefix.length + blocks.length * 512 + suffix.length; const imageLength = prefix.length + blocks.length * 512 + suffix.length;
const byteArray = new Uint8Array(imageLength); const byteArray = new Uint8Array(imageLength);
@ -292,7 +332,7 @@ export default function createDiskFrom2MG(options: DiskOptions) {
disk = Nibble(options); disk = Nibble(options);
break; break;
case FORMAT.DOS: // dsk case FORMAT.DOS: // dsk
default: // Something hinky, assume 'dsk' default: // Something hinky, assume 'dsk'
disk = DOS(options); disk = DOS(options);
break; 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. * Returns a `Disk` object for a block volume with block-ordered data.
* @param options the disk image and options * @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; const { rawData, readOnly, name } = options;
if (!rawData) { if (!rawData) {
@ -13,7 +16,7 @@ export default function createBlockDisk(fmt: BlockFormat, options: DiskOptions):
const blocks = []; const blocks = [];
let offset = 0; let offset = 0;
while (offset < rawData.byteLength) { while (offset < rawData.byteLength) {
blocks.push(new Uint8Array(rawData.slice(offset, offset + 0x200))); blocks.push(new Uint8Array(rawData.slice(offset, offset + 0x200)));
offset += 0x200; offset += 0x200;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,10 @@ export class SaplingFile extends ProDOSFile {
blocks: Uint8Array[]; blocks: Uint8Array[];
bitMap: BitMap; bitMap: BitMap;
constructor(volume: ProDOSVolume, private fileEntry: FileEntry) { constructor(
volume: ProDOSVolume,
private fileEntry: FileEntry
) {
super(volume); super(volume);
this.blocks = this.volume.blocks(); this.blocks = this.volume.blocks();
this.bitMap = this.volume.bitMap(); this.bitMap = this.volume.bitMap();
@ -45,7 +48,10 @@ export class SaplingFile extends ProDOSFile {
(seedlingPointers.getUint8(0x100 + idx) << 8); (seedlingPointers.getUint8(0x100 + idx) << 8);
if (seedlingPointer) { if (seedlingPointer) {
const seedlingBlock = this.blocks[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); data.set(bytes, offset);
} }
@ -72,7 +78,9 @@ export class SaplingFile extends ProDOSFile {
seedlingPointers.setUint8(idx, seedlingPointer & 0xff); seedlingPointers.setUint8(idx, seedlingPointer & 0xff);
seedlingPointers.setUint8(0x100 + idx, seedlingPointer >> 8); seedlingPointers.setUint8(0x100 + idx, seedlingPointer >> 8);
const seedlingBlock = this.blocks[seedlingPointer]; 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++; idx++;
offset += BLOCK_SIZE; offset += BLOCK_SIZE;
remainingLength -= 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[]; blocks: Uint8Array[];
bitMap: BitMap; bitMap: BitMap;
constructor(volume: ProDOSVolume, private fileEntry: FileEntry) { constructor(
volume: ProDOSVolume,
private fileEntry: FileEntry
) {
super(volume); super(volume);
this.blocks = volume.blocks(); this.blocks = volume.blocks();
this.bitMap = volume.bitMap(); 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 bitMap: BitMap;
private blocks: Uint8Array[]; private blocks: Uint8Array[];
constructor(volume: ProDOSVolume, private fileEntry: FileEntry) { constructor(
volume: ProDOSVolume,
private fileEntry: FileEntry
) {
super(volume); super(volume);
this.blocks = volume.blocks(); this.blocks = volume.blocks();
this.bitMap = volume.bitMap(); this.bitMap = volume.bitMap();
@ -24,7 +27,9 @@ export class TreeFile extends ProDOSFile {
(saplingPointers.getUint8(0x100 + idx) << 8); (saplingPointers.getUint8(0x100 + idx) << 8);
if (saplingPointer) { if (saplingPointer) {
pointers.push(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++) { for (let jdx = 0; jdx < 256; jdx++) {
const seedlingPointer = const seedlingPointer =
seedlingPointers.getUint8(idx) | seedlingPointers.getUint8(idx) |
@ -62,7 +67,9 @@ export class TreeFile extends ProDOSFile {
(seedlingPointers.getUint8(0x100 + idx) << 8); (seedlingPointers.getUint8(0x100 + idx) << 8);
if (seedlingPointer) { if (seedlingPointer) {
const seedlingBlock = this.blocks[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); data.set(bytes, offset);
} }
@ -105,7 +112,9 @@ export class TreeFile extends ProDOSFile {
seedlingPointers.setUint8(idx, seedlingPointer & 0xff); seedlingPointers.setUint8(idx, seedlingPointer & 0xff);
seedlingPointers.setUint8(0x100 + idx, seedlingPointer >> 8); seedlingPointers.setUint8(0x100 + idx, seedlingPointer >> 8);
const seedlingBlock = this.blocks[seedlingPointer]; 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++; jdx++;
offset += BLOCK_SIZE; offset += BLOCK_SIZE;
remainingLength -= 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 hourMinute = val >> 16;
const year = yearMonthDay >> 9; const year = yearMonthDay >> 9;
const month = (yearMonthDay & 0x01E0) >> 5; const month = (yearMonthDay & 0x01e0) >> 5;
const day = yearMonthDay & 0x001F; const day = yearMonthDay & 0x001f;
const hour = hourMinute >> 8; const hour = hourMinute >> 8;
const min = hourMinute & 0xff; 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); return new Date(0);
} }
@ -36,15 +42,20 @@ export function dateToUint32(date: Date) {
const hour = date.getHours(); const hour = date.getHours();
const min = date.getMinutes(); const min = date.getMinutes();
const yearMonthDay = year << 9 | month << 5 | day; const yearMonthDay = (year << 9) | (month << 5) | day;
const hourMinute = hour << 8 | min; const hourMinute = (hour << 8) | min;
val = hourMinute << 16 | yearMonthDay; val = (hourMinute << 16) | yearMonthDay;
} }
return val; 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 = ''; let name = '';
if (!(caseBits & 0x8000)) { if (!(caseBits & 0x8000)) {
caseBits = 0; caseBits = 0;
@ -66,7 +77,7 @@ export function writeFileName(block: DataView, offset: word, name: string) {
for (let idx = 0; idx < name.length; idx++) { for (let idx = 0; idx < name.length; idx++) {
caseBits <<= 1; caseBits <<= 1;
let charCode = name.charCodeAt(idx); let charCode = name.charCodeAt(idx);
if (charCode > 0x60 && charCode < 0x7B) { if (charCode > 0x60 && charCode < 0x7b) {
caseBits |= 0x1; caseBits |= 0x1;
charCode -= 0x20; charCode -= 0x20;
} }
@ -75,7 +86,11 @@ export function writeFileName(block: DataView, offset: word, name: string) {
return caseBits; 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); const dir = new Directory(volume, dirEntry);
let str = ''; 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 { FileEntry, readEntries, writeEntries } from './file_entry';
import { STORAGE_TYPES, ACCESS_TYPES } from './constants'; import { STORAGE_TYPES, ACCESS_TYPES } from './constants';
import { byte, word } from 'js/types'; import { byte, word } from 'js/types';
@ -12,8 +17,8 @@ const VDH_OFFSETS = {
NAME_LENGTH: 0x04, NAME_LENGTH: 0x04,
VOLUME_NAME: 0x05, VOLUME_NAME: 0x05,
RESERVED_1: 0x14, RESERVED_1: 0x14,
CASE_BITS: 0x1A, CASE_BITS: 0x1a,
CREATION: 0x1C, CREATION: 0x1c,
VERSION: 0x20, VERSION: 0x20,
MIN_VERSION: 0x21, MIN_VERSION: 0x21,
ACCESS: 0x22, ACCESS: 0x22,
@ -44,16 +49,22 @@ export class VDH {
this.blocks = this.volume.blocks(); this.blocks = this.volume.blocks();
} }
read() { read() {
const block = new DataView(this.blocks[VDH_BLOCK].buffer); const block = new DataView(this.blocks[VDH_BLOCK].buffer);
this.next = block.getUint16(VDH_OFFSETS.NEXT, true); this.next = block.getUint16(VDH_OFFSETS.NEXT, true);
this.storageType = block.getUint8(VDH_OFFSETS.STORAGE_TYPE) >> 4; 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); const caseBits = block.getUint8(VDH_OFFSETS.CASE_BITS);
this.name = readFileName(block, VDH_OFFSETS.VOLUME_NAME, nameLength, caseBits); this.name = readFileName(
this.creation = uint32ToDate(block.getUint32(VDH_OFFSETS.CREATION, true)); block,
VDH_OFFSETS.VOLUME_NAME,
nameLength,
caseBits
);
this.creation = uint32ToDate(
block.getUint32(VDH_OFFSETS.CREATION, true)
);
this.access = block.getUint8(VDH_OFFSETS.ACCESS); this.access = block.getUint8(VDH_OFFSETS.ACCESS);
this.entryLength = block.getUint8(VDH_OFFSETS.ENTRY_LENGTH); this.entryLength = block.getUint8(VDH_OFFSETS.ENTRY_LENGTH);
this.entriesPerBlock = block.getUint8(VDH_OFFSETS.ENTRIES_PER_BLOCK); 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 block = new DataView(this.blocks[VDH_BLOCK].buffer);
const nameLength = this.name.length & 0x0f; const nameLength = this.name.length & 0x0f;
block.setUint8(VDH_OFFSETS.STORAGE_TYPE, this.storageType << 4 & nameLength); block.setUint8(
const caseBits = writeFileName(block, VDH_OFFSETS.VOLUME_NAME, this.name); VDH_OFFSETS.STORAGE_TYPE,
block.setUint32(VDH_OFFSETS.CREATION, dateToUint32(this.creation), true); (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.setUint16(VDH_OFFSETS.CASE_BITS, caseBits);
block.setUint8(VDH_OFFSETS.ACCESS, this.access); block.setUint8(VDH_OFFSETS.ACCESS, this.access);
block.setUint8(VDH_OFFSETS.ENTRY_LENGTH, this.entryLength); 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 const ENCODING_BLOCK = 'block';
export interface FloppyDisk extends Disk { 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 { export interface NoFloppyDisk extends FloppyDisk {
@ -101,21 +104,12 @@ export interface BlockDisk extends Disk {
/** /**
* File types supported by floppy devices in nibble mode. * File types supported by floppy devices in nibble mode.
*/ */
export const NIBBLE_FORMATS = [ export const NIBBLE_FORMATS = ['2mg', 'd13', 'do', 'dsk', 'po', 'nib'] as const;
'2mg',
'd13',
'do',
'dsk',
'po',
'nib',
] as const;
/** /**
* File types supported by floppy devices in bitstream mode. * File types supported by floppy devices in bitstream mode.
*/ */
export const BITSTREAM_FORMATS = [ export const BITSTREAM_FORMATS = ['woz'] as const;
'woz',
] as const;
/** /**
* All file types supported by floppy devices. * All file types supported by floppy devices.
@ -128,19 +122,12 @@ export const FLOPPY_FORMATS = [
/** /**
* File types supported by block devices. * File types supported by block devices.
*/ */
export const BLOCK_FORMATS = [ export const BLOCK_FORMATS = ['2mg', 'hdv', 'po'] as const;
'2mg',
'hdv',
'po',
] as const;
/** /**
* All supported disk formats. * All supported disk formats.
*/ */
export const DISK_FORMATS = [ export const DISK_FORMATS = [...FLOPPY_FORMATS, ...BLOCK_FORMATS] as const;
...FLOPPY_FORMATS,
...BLOCK_FORMATS,
] as const;
export type FloppyFormat = MemberOf<typeof FLOPPY_FORMATS>; export type FloppyFormat = MemberOf<typeof FLOPPY_FORMATS>;
export type NibbleFormat = MemberOf<typeof NIBBLE_FORMATS>; export type NibbleFormat = MemberOf<typeof NIBBLE_FORMATS>;
@ -259,9 +246,9 @@ export interface ProcessJsonMessage {
} }
export type FormatWorkerMessage = export type FormatWorkerMessage =
ProcessBinaryMessage | | ProcessBinaryMessage
ProcessJsonDiskMessage | | ProcessJsonDiskMessage
ProcessJsonMessage; | ProcessJsonMessage;
/** /**
* Format work result message type * Format work result message type
@ -277,8 +264,7 @@ export interface DiskProcessedResponse {
}; };
} }
export type FormatWorkerResponse = export type FormatWorkerResponse = DiskProcessedResponse;
DiskProcessedResponse;
export interface MassStorageData { export interface MassStorageData {
metadata: DiskMetadata; metadata: DiskMetadata;

View File

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

221
js/gl.ts
View File

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

View File

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

View File

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

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