mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
Prettier (#203)
* Enabled prettier * Update lint, fix issues * Restore some array formatting
This commit is contained in:
parent
e7891114c6
commit
1e79d9d59d
@ -1,4 +1,5 @@
|
||||
dist
|
||||
json/disks/index.js
|
||||
node_modules
|
||||
submodules
|
||||
tmp
|
||||
|
@ -1,23 +1,17 @@
|
||||
{
|
||||
// Global
|
||||
"root": true,
|
||||
"plugins": [
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:jest/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
4,
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"prettier/prettier": "error",
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
@ -80,12 +74,6 @@
|
||||
"rules": {
|
||||
// recommended is just "warn"
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
// enforce semicolons at ends of statements
|
||||
"semi": "off",
|
||||
"@typescript-eslint/semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
// enforce semicolons to separate members
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"error",
|
||||
|
40
.github/workflows/nodejs.yml
vendored
40
.github/workflows/nodejs.yml
vendored
@ -3,25 +3,25 @@ name: Node CI
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x, 18.x]
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x, 18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: "true"
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: npm install, build, and test
|
||||
run: |
|
||||
npm ci
|
||||
npm run build --if-present
|
||||
npm test
|
||||
env:
|
||||
CI: true
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'true'
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: npm install, build, and test
|
||||
run: |
|
||||
npm ci
|
||||
npm run build --if-present
|
||||
npm test
|
||||
env:
|
||||
CI: true
|
||||
|
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@ -0,0 +1,2 @@
|
||||
js/roms
|
||||
submodules
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": true
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
{
|
||||
"extends": [
|
||||
"stylelint-config-standard-scss",
|
||||
"stylelint-config-css-modules"
|
||||
],
|
||||
"rules": {
|
||||
"indentation": 4,
|
||||
"selector-class-pattern": "^[a-z][a-zA-Z0-9_-]+$"
|
||||
}
|
||||
"extends": [
|
||||
"stylelint-config-standard-scss",
|
||||
"stylelint-config-css-modules"
|
||||
],
|
||||
"rules": {
|
||||
"selector-class-pattern": "^[a-z][a-zA-Z0-9_-]+$"
|
||||
}
|
||||
}
|
||||
|
90
README.md
90
README.md
@ -46,29 +46,29 @@ then
|
||||
|
||||
### 2019-03-02
|
||||
|
||||
* Behind the scenes
|
||||
- Behind the scenes
|
||||
|
||||
A lot of not so visible changes behind the scenes. The website now runs directly off of github, and has a more modern toolchain (Goodbye make, Perl and closure compiler, hello Webpack!) to facilitate development.
|
||||
|
||||
* Performance
|
||||
- Performance
|
||||
|
||||
In particular, the screen no longer redraws as quickly as possible.
|
||||
|
||||
* Drag and Drop
|
||||
- Drag and Drop
|
||||
|
||||
Disk images can be dragged into the window to load them.
|
||||
|
||||
* Contributions
|
||||
- Contributions
|
||||
|
||||
Thanks to [Ian Flanigan](https://github.com/iflan) for additions to improve ChromeBook behavior.
|
||||
|
||||
### 2017-10-08
|
||||
|
||||
* Better iOS support
|
||||
- Better iOS support
|
||||
|
||||
Bluetooth keyboards now work better. Reset is Ctrl-Shift-Delete. iOS now allows loading disks from iCloud and services like Drop Box. Saving locally is still not supported by iOS. I now understand why sound doesn't work, and I'm working on a work-around.
|
||||
|
||||
* Source Maps
|
||||
- Source Maps
|
||||
|
||||
Although the source code has always been available, by default I serve up minified Javascript for performance reasons. But now you can poke around more easily.
|
||||
|
||||
@ -76,102 +76,104 @@ then
|
||||
|
||||
(It's been a long time since I updated, so this is a rough list)
|
||||
|
||||
* Videx Videoterm Emulation (\]\[js)
|
||||
- Videx Videoterm Emulation (\]\[js)
|
||||
|
||||
PR#3 now does something on the Apple \]\[!.
|
||||
|
||||
* AppleColor RGB Card Emulation (//jse)
|
||||
- AppleColor RGB Card Emulation (//jse)
|
||||
|
||||
Now supports a bunch of the mostly non-standard video formats found on the AppleColor RGB card, including 16 color text, 16 color hires mode, and mixed black and white and color double hires
|
||||
* Machine selection
|
||||
|
||||
- Machine selection
|
||||
|
||||
You can now select between original, autostart and plus Apple \]\[s, and unenhanced and enhanced //es.
|
||||
|
||||
## Updates (2013-07-04)
|
||||
|
||||
* RAMFactor Emulation (//jse)
|
||||
- RAMFactor Emulation (//jse)
|
||||
|
||||
I now simulate having a 1 Megabyte RAMFactor card in slot 2.
|
||||
|
||||
* Thunderclock Emulation
|
||||
- Thunderclock Emulation
|
||||
|
||||
There is cursory emulation of the Thunderclock card, enough to keep ProDOS applications from asking you to enter the date all the time. ProDOS attempts to guess the year from the month, the day and the day of the week, something that needs to be patched every 6 years. This means newer versions think it's 1996, older versions are stuck in the 80s.
|
||||
|
||||
* Firefox Nightly Joystick Support
|
||||
- Firefox Nightly Joystick Support
|
||||
|
||||
Joystick support has yet to officially land, but the latest nightlies support the gamepad API.
|
||||
|
||||
## Updates (2013-03-20)
|
||||
|
||||
* Animation Frames
|
||||
- Animation Frames
|
||||
|
||||
I've switched from using setInterval() to requestAnimationFrame() where supported. This, in conjunction with the graphics re-write, seems to smooth performance and provide a more stable CPU speed.
|
||||
* Graphics Re-Write
|
||||
|
||||
- Graphics Re-Write
|
||||
|
||||
This (third) re-write of the graphics system should improve performance with graphics intensive programs. Rather than rendering each graphics update as it happens, updates are rendered each animation frame.
|
||||
|
||||
## Updates (2013-03-12)
|
||||
|
||||
* Apple //e
|
||||
- Apple //e
|
||||
|
||||
After much flailing, and much staring at MMU emulation code in despair, I've finally published my Apple //e emulator. It's probably a little more rough than I'd hoped, but it has a lot of features that I really wanted to get into it, like basic double hires support, and it uses the enhanced Apple //e ROMs.
|
||||
|
||||
## Updates (2013-02-25)
|
||||
|
||||
* Joystick Support
|
||||
- Joystick Support
|
||||
|
||||
Chrome only so far, the nascent gamepad API has finally allowed me to add basic joystick support. I can now re-live my glory days of Skyfox.
|
||||
|
||||
* Re-written CPU emulator
|
||||
- Re-written CPU emulator
|
||||
|
||||
I finally got around to applying some of the many lessons I learned along the way writing my first CPU emulator in Javascript. The last re-working gave me about a 100% performance gain.
|
||||
|
||||
* Finally Fixed Oregon Trail
|
||||
- Finally Fixed Oregon Trail
|
||||
|
||||
This seems to have been a major disappointment for many people. I was able to make it as a banker, but I'm embarassed to reveal my score.
|
||||
|
||||
* Competition
|
||||
- Competition
|
||||
|
||||
Now in addition to [Gil Megidish's](http://www.megidish.net/apple2js/) Apple2JS, there's a couple of new kids on the block, including [David Caldwell's](http://porkrind.org/a2/) Apple II+ emulator where he's put a lot more thought into the graphics rendering than I have, and [appletoo](https://github.com/nicholasbs/appletoo), which I just stumbled across while looking for David's emulator and haven't had much time to look at.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A Browser with HTML5 Support
|
||||
- A Browser with HTML5 Support
|
||||
|
||||
The most recent versions of [Google Chrome](https://www.google.com/chrome/), [Safari](https://www.apple.com/safari/), [Firefox](https://www.firefox.com/), and [Opera](https//www.opera.com/) all seem to work reasonably well these days,although variations in HTML5 support pop up, and occasionally a major release will move things around out from under me. IE prior to 9 lacks canvas tag support and is unsupported. [IE 9+](https://windows.microsoft.com/ie9) renders nicely on a modern machine.
|
||||
|
||||
* Basic Knowledge of the Apple \]\[
|
||||
- Basic Knowledge of the Apple \]\[
|
||||
|
||||
If you don't know how to use an Apple \]\[, this won't be much fun for you.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
* I'm using the following libraries:
|
||||
- I'm using the following libraries:
|
||||
|
||||
* [jQuery](https://jquery.com) and [jQuery UI](https://jqueryui.com)
|
||||
* Base64 Utilities via [KvZ](http://kevin.vanzonneveld.net/)
|
||||
* LED graphics from [Modern Life](http://modernl.com/).
|
||||
* [CFFA2 Firmware](http://dreher.net/?s=projects/CFforAppleII&c=projects/CFforAppleII/downloads1.php) by Chris Schumann, Rich Dreher and Dave Lyons
|
||||
- [jQuery](https://jquery.com) and [jQuery UI](https://jqueryui.com)
|
||||
- Base64 Utilities via [KvZ](http://kevin.vanzonneveld.net/)
|
||||
- LED graphics from [Modern Life](http://modernl.com/).
|
||||
- [CFFA2 Firmware](http://dreher.net/?s=projects/CFforAppleII&c=projects/CFforAppleII/downloads1.php) by Chris Schumann, Rich Dreher and Dave Lyons
|
||||
|
||||
* I heavily referenced:
|
||||
- I heavily referenced:
|
||||
|
||||
* [_Beneath Apple DOS_](http://www.scribd.com/doc/200679/Beneath-Apple-DOS-By-Don-Worth-and-Pieter-Lechner) by Don Worth and Pieter Lechner
|
||||
* _Inside the Apple //e_ by Gary B. Little
|
||||
* [_DOS 3.3 Anatomy_](http://apple2.org.za/gswv/a2zine/GS.WorldView/Resources/DOS.3.3.ANATOMY/)
|
||||
* [_Apple II Disk Drive Article_](http://www.doc.ic.ac.uk/~ih/doc/stepper/others/example3/diskii_specs.html) by Neil Parker
|
||||
* [6502.org](http://6502.org/)
|
||||
* The [comp.sys.apple2.programmer](http://www.faqs.org/faqs/apple2/programmerfaq/part1/) FAQ
|
||||
* [Understanding the Apple \]\[](https://archive.org/details/understanding_the_apple_ii) and [Understanding the Apple //e](https://archive.org/details/Understanding_the_Apple_IIe) by Jim Sather.
|
||||
* [Apple II Colors](https://mrob.com/pub/xapple2/colors.html) by Robert Munafo.
|
||||
- [_Beneath Apple DOS_](http://www.scribd.com/doc/200679/Beneath-Apple-DOS-By-Don-Worth-and-Pieter-Lechner) by Don Worth and Pieter Lechner
|
||||
- _Inside the Apple //e_ by Gary B. Little
|
||||
- [_DOS 3.3 Anatomy_](http://apple2.org.za/gswv/a2zine/GS.WorldView/Resources/DOS.3.3.ANATOMY/)
|
||||
- [_Apple II Disk Drive Article_](http://www.doc.ic.ac.uk/~ih/doc/stepper/others/example3/diskii_specs.html) by Neil Parker
|
||||
- [6502.org](http://6502.org/)
|
||||
- The [comp.sys.apple2.programmer](http://www.faqs.org/faqs/apple2/programmerfaq/part1/) FAQ
|
||||
- [Understanding the Apple \]\[](https://archive.org/details/understanding_the_apple_ii) and [Understanding the Apple //e](https://archive.org/details/Understanding_the_Apple_IIe) by Jim Sather.
|
||||
- [Apple II Colors](https://mrob.com/pub/xapple2/colors.html) by Robert Munafo.
|
||||
|
||||
* And special thanks to:
|
||||
- And special thanks to:
|
||||
|
||||
* [ADTPro](http://adtpro.sourceforge.net/) for allowing me to pull some of my circa 1980 programming efforts off some ancient floppies.
|
||||
* [KEGS](http://kegs.sourceforge.net/), because at some point I got so tired of futzing with ADC/SBC code I just ported the KEGS C code for those opcodes to Javascript so I could stop worrying about it.
|
||||
* [Apple II History](http://apple2history.org/), for a lovely, informative site.
|
||||
* [Gil Megidish](http://www.megidish.net/apple2js/), for the kick in the pants to finally post my version, once I realized there was, in fact, another apple2js in the world.
|
||||
* [AppleWin](https://github.com/AppleWin/AppleWin/), whose source code is a goldmine of useful references.
|
||||
* [Zellyn Hunter](https://github.com/zellyn/a2audit) and a2audit, for allowing me to get really nitpicky in my memory emulation.
|
||||
- [ADTPro](http://adtpro.sourceforge.net/) for allowing me to pull some of my circa 1980 programming efforts off some ancient floppies.
|
||||
- [KEGS](http://kegs.sourceforge.net/), because at some point I got so tired of futzing with ADC/SBC code I just ported the KEGS C code for those opcodes to Javascript so I could stop worrying about it.
|
||||
- [Apple II History](http://apple2history.org/), for a lovely, informative site.
|
||||
- [Gil Megidish](http://www.megidish.net/apple2js/), for the kick in the pants to finally post my version, once I realized there was, in fact, another apple2js in the world.
|
||||
- [AppleWin](https://github.com/AppleWin/AppleWin/), whose source code is a goldmine of useful references.
|
||||
- [Zellyn Hunter](https://github.com/zellyn/a2audit) and a2audit, for allowing me to get really nitpicky in my memory emulation.
|
||||
|
||||
* Contributors
|
||||
* [Snapperfish](http://github.com/Snapperfish) Various fixes
|
||||
- Contributors
|
||||
- [Snapperfish](http://github.com/Snapperfish) Various fixes
|
||||
|
@ -12,16 +12,17 @@ module.exports = {
|
||||
[
|
||||
'@babel/typescript',
|
||||
{
|
||||
jsxPragma: 'h'
|
||||
}
|
||||
jsxPragma: 'h',
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: [
|
||||
[
|
||||
'@babel/plugin-transform-react-jsx', {
|
||||
'@babel/plugin-transform-react-jsx',
|
||||
{
|
||||
pragma: 'h',
|
||||
pragmaFrag: 'Fragment',
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
13
bin/dsk2json
13
bin/dsk2json
@ -44,7 +44,7 @@ function read2MG(fileData) {
|
||||
return {
|
||||
diskData: fileData.slice(headerLength),
|
||||
type: type,
|
||||
readOnly: readOnly
|
||||
readOnly: readOnly,
|
||||
};
|
||||
}
|
||||
|
||||
@ -53,11 +53,11 @@ function readTracks(type, diskData) {
|
||||
|
||||
if (type === 'nib') {
|
||||
let start = 0;
|
||||
let end = 0x1A00;
|
||||
let end = 0x1a00;
|
||||
while (start < diskData.length) {
|
||||
const trackData = diskData.slice(start, end);
|
||||
start += 0x1A00;
|
||||
end += 0x1A00;
|
||||
start += 0x1a00;
|
||||
end += 0x1a00;
|
||||
|
||||
tracks.push(trackData.toString('base64'));
|
||||
}
|
||||
@ -82,7 +82,6 @@ function readTracks(type, diskData) {
|
||||
|
||||
const fileName = argv._[0];
|
||||
|
||||
|
||||
let readOnly = argv.r || argv.readOnly ? true : undefined;
|
||||
const name = argv.n || argv.name;
|
||||
const category = argv.c || argv.category;
|
||||
@ -110,7 +109,7 @@ fs.readFile(fileName, (err, fileData) => {
|
||||
let diskData;
|
||||
|
||||
if (type === '2mg') {
|
||||
({diskData, readOnly, type} = read2MG(fileData));
|
||||
({ diskData, readOnly, type } = read2MG(fileData));
|
||||
} else {
|
||||
diskData = fileData;
|
||||
}
|
||||
@ -123,7 +122,7 @@ fs.readFile(fileName, (err, fileData) => {
|
||||
readOnly,
|
||||
disk,
|
||||
'2e': e,
|
||||
data: readTracks(type, diskData)
|
||||
data: readTracks(type, diskData),
|
||||
};
|
||||
|
||||
Object.keys(entry).forEach((key) => {
|
||||
|
@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
@ -13,8 +12,12 @@ for (const fileName of dir.sort()) {
|
||||
if (/\.json$/.test(fileName)) {
|
||||
const json = fs.readFileSync(path.resolve(diskPath, fileName));
|
||||
const data = JSON.parse(json);
|
||||
if (data.private) { continue; }
|
||||
if (!data.name || !data.category) { continue; }
|
||||
if (data.private) {
|
||||
continue;
|
||||
}
|
||||
if (!data.name || !data.category) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = {
|
||||
filename: `json/disks/${fileName}`,
|
||||
|
109
css/apple2.css
109
css/apple2.css
@ -1,6 +1,6 @@
|
||||
#header {
|
||||
width: 580px;
|
||||
margin: auto;
|
||||
width: 580px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
img {
|
||||
@ -16,22 +16,22 @@ img {
|
||||
margin: 0;
|
||||
padding: 3px 0 0 10;
|
||||
color: black;
|
||||
font-family: "Adobe Garamond Pro",Garamond,Times;
|
||||
font-family: 'Adobe Garamond Pro', Garamond, Times;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.motter {
|
||||
font-family: "Motter Tektura";
|
||||
font-family: 'Motter Tektura';
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
input[type="button"] {
|
||||
input[type='button'] {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
input[type='text'] {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
@ -73,7 +73,7 @@ body {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
position: fixed;
|
||||
top:0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
@ -100,7 +100,9 @@ body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.full-page #header, .full-page .inset, .full-page #reset {
|
||||
.full-page #header,
|
||||
.full-page .inset,
|
||||
.full-page #reset {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -187,9 +189,9 @@ th {
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
float: left;
|
||||
image-rendering: pixelated;
|
||||
display: block;
|
||||
float: left;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.mono {
|
||||
@ -199,7 +201,13 @@ canvas {
|
||||
.scanlines:after {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
background-image: repeating-linear-gradient(to bottom, transparent 0, transparent 1px, rgba(0,0,0,0.5) 1px, rgba(0,0,0,0.5) 2px);
|
||||
background-image: repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
transparent 1px,
|
||||
rgba(0, 0, 0, 0.5) 1px,
|
||||
rgba(0, 0, 0, 0.5) 2px
|
||||
);
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -209,7 +217,13 @@ canvas {
|
||||
}
|
||||
|
||||
.full-page .scanlines:after {
|
||||
background-image: repeating-linear-gradient(to bottom, transparent 0, transparent 0.25vh, rgba(0,0,0,0.5) 0.25vh, rgba(0,0,0,0.5) 0.5vh);
|
||||
background-image: repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
transparent 0.25vh,
|
||||
rgba(0, 0, 0, 0.5) 0.25vh,
|
||||
rgba(0, 0, 0, 0.5) 0.5vh
|
||||
);
|
||||
}
|
||||
|
||||
#screen {
|
||||
@ -238,7 +252,7 @@ canvas {
|
||||
#about {
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
font-family: "Adobe Garamond Pro",Garamond,Times;
|
||||
font-family: 'Adobe Garamond Pro', Garamond, Times;
|
||||
font-size: 14px;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
@ -287,7 +301,7 @@ canvas {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -307,10 +321,10 @@ canvas {
|
||||
font-size: 14px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #44372C;
|
||||
background: #44372c;
|
||||
color: #fff;
|
||||
padding: 5px 11px;
|
||||
border: 1px outset #66594E;
|
||||
border: 1px outset #66594e;
|
||||
border-radius: 3px;
|
||||
user-select: none;
|
||||
}
|
||||
@ -325,13 +339,15 @@ canvas {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal__close, .modal__close:active, .modal__close:hover {
|
||||
.modal__close,
|
||||
.modal__close:active,
|
||||
.modal__close:hover {
|
||||
background: transparent;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.modal__header .modal__close:before {
|
||||
content: "\2715";
|
||||
content: '\2715';
|
||||
}
|
||||
|
||||
.modal__content {
|
||||
@ -348,10 +364,10 @@ canvas {
|
||||
|
||||
button,
|
||||
a.button {
|
||||
background: #44372C;
|
||||
background: #44372c;
|
||||
color: #fff;
|
||||
padding: 2px 8px;
|
||||
border: 1px outset #66594E;
|
||||
border: 1px outset #66594e;
|
||||
border-radius: 3px;
|
||||
font-size: 15px;
|
||||
text-decoration: none;
|
||||
@ -359,14 +375,14 @@ a.button {
|
||||
|
||||
button:hover,
|
||||
a.button:hover {
|
||||
background-color: #55473D;
|
||||
border: 1px outset #66594E;
|
||||
background-color: #55473d;
|
||||
border: 1px outset #66594e;
|
||||
}
|
||||
|
||||
button:active,
|
||||
a.button:active {
|
||||
background-color: #22150A;
|
||||
border: 1px outset #44372C;
|
||||
background-color: #22150a;
|
||||
border: 1px outset #44372c;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
@ -374,7 +390,8 @@ a.button:hover {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#keyboard, #reset {
|
||||
#keyboard,
|
||||
#reset {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
@ -435,23 +452,23 @@ a.button:hover {
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: #44372C;
|
||||
background-color: #44372c;
|
||||
color: white;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
border-left: 5px solid #65594D;
|
||||
border-top: 5px solid #65594D;
|
||||
border-right: 5px solid #110E0D;
|
||||
border-bottom: 5px solid #110E0D;
|
||||
border-left: 5px solid #65594d;
|
||||
border-top: 5px solid #65594d;
|
||||
border-right: 5px solid #110e0d;
|
||||
border-bottom: 5px solid #110e0d;
|
||||
/* border: 5px outset #66594E; */
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#keyboard .pressed {
|
||||
background-color: #22150A;
|
||||
border-left: 5px solid #44372C;
|
||||
border-top: 5px solid #44372C;
|
||||
background-color: #22150a;
|
||||
border-left: 5px solid #44372c;
|
||||
border-top: 5px solid #44372c;
|
||||
border-right: 5px solid #000000;
|
||||
border-bottom: 5px solid #000000;
|
||||
/* border: 5px outset #44372C; */
|
||||
@ -602,11 +619,11 @@ a.button:hover {
|
||||
}
|
||||
|
||||
#reset {
|
||||
background: #44372C;
|
||||
border-left: 3px solid #65594D;
|
||||
border-top: 3px solid #65594D;
|
||||
border-right: 3px solid #110E0D;
|
||||
border-bottom: 3px solid #110E0D;
|
||||
background: #44372c;
|
||||
border-left: 3px solid #65594d;
|
||||
border-top: 3px solid #65594d;
|
||||
border-right: 3px solid #110e0d;
|
||||
border-bottom: 3px solid #110e0d;
|
||||
/* border: 5px outset #66594E; */
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
@ -621,14 +638,14 @@ a.button:hover {
|
||||
}
|
||||
|
||||
#reset:hover {
|
||||
background: #44372C;
|
||||
border: 3px outset #66594E;
|
||||
background: #44372c;
|
||||
border: 3px outset #66594e;
|
||||
}
|
||||
|
||||
#reset:active {
|
||||
background-color: #22150A;
|
||||
border-left: 3px solid #44372C;
|
||||
border-top: 3px solid #44372C;
|
||||
background-color: #22150a;
|
||||
border-left: 3px solid #44372c;
|
||||
border-top: 3px solid #44372c;
|
||||
border-right: 3px solid #000000;
|
||||
border-bottom: 3px solid #000000;
|
||||
}
|
||||
@ -654,8 +671,8 @@ a.button:hover {
|
||||
}
|
||||
|
||||
.standalone {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.standalone #header {
|
||||
|
52
index.html
52
index.html
@ -1,32 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
|
||||
<head>
|
||||
<title>PreApple II</title>
|
||||
<meta name="viewport" content="width=640, user-scalable=no" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta charset="utf-8" />
|
||||
<title>PreApple II</title>
|
||||
<meta name="viewport" content="width=640, user-scalable=no" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<link rel="apple-touch-icon" href="img/webapp-iphone.png" />
|
||||
<link rel="apple-touch-icon" size="72x72" href="img/webapp-ipad.png" />
|
||||
<link rel="shortcut icon" href="img/logoicon.png" />
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v6.1.1/css/all.css" />
|
||||
<style>
|
||||
body {
|
||||
background-color: #c4c1a0;
|
||||
margin: 16px 0;
|
||||
}
|
||||
</style>
|
||||
<link rel="apple-touch-icon" href="img/webapp-iphone.png" />
|
||||
<link rel="apple-touch-icon" size="72x72" href="img/webapp-ipad.png" />
|
||||
<link rel="shortcut icon" href="img/logoicon.png" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://use.fontawesome.com/releases/v6.1.1/css/all.css"
|
||||
/>
|
||||
<style>
|
||||
body {
|
||||
background-color: #c4c1a0;
|
||||
margin: 16px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
|
||||
<filter id="green">
|
||||
<feColorMatrix type="matrix" values="0.0 0.0 0.0 0.0 0
|
||||
<div id="app"></div>
|
||||
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
|
||||
<filter id="green">
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0.0 0.0 0.0 0.0 0
|
||||
0.0 1.0 0.0 0.0 0
|
||||
0.0 0.0 0.5 0.0 0
|
||||
0.0 0.0 0.0 1.0 0" />
|
||||
</filter>
|
||||
</svg>
|
||||
<script src="dist/preact.bundle.js"></script>
|
||||
0.0 0.0 0.0 1.0 0"
|
||||
/>
|
||||
</filter>
|
||||
</svg>
|
||||
<script src="dist/preact.bundle.js"></script>
|
||||
</body>
|
||||
|
@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
'moduleNameMapper': {
|
||||
moduleNameMapper: {
|
||||
'^js/(.*)': '<rootDir>/js/$1',
|
||||
'^test/(.*)': '<rootDir>/test/$1',
|
||||
'\\.css$': 'identity-obj-proxy',
|
||||
@ -9,28 +9,17 @@ module.exports = {
|
||||
// https://github.com/preactjs/enzyme-adapter-preact-pure/issues/179#issuecomment-1201096897
|
||||
'^preact(/(.*)|$)': 'preact$1',
|
||||
},
|
||||
'roots': [
|
||||
'js/',
|
||||
'test/',
|
||||
],
|
||||
'testMatch': [
|
||||
'**/?(*.)+(spec|test).+(ts|js|tsx)'
|
||||
],
|
||||
'transform': {
|
||||
roots: ['js/', 'test/'],
|
||||
testMatch: ['**/?(*.)+(spec|test).+(ts|js|tsx)'],
|
||||
transform: {
|
||||
'^.+\\.js$': 'babel-jest',
|
||||
'^.+\\.ts$': 'ts-jest',
|
||||
'^.*\\.tsx$': 'ts-jest',
|
||||
},
|
||||
'transformIgnorePatterns': [
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!(@testing-library/preact/dist/esm)/)',
|
||||
],
|
||||
'setupFilesAfterEnv': [
|
||||
'<rootDir>/test/jest-setup.ts'
|
||||
],
|
||||
'coveragePathIgnorePatterns': [
|
||||
'/node_modules/',
|
||||
'/js/roms/',
|
||||
'/test/',
|
||||
],
|
||||
'preset': 'ts-jest',
|
||||
setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'],
|
||||
coveragePathIgnorePatterns: ['/node_modules/', '/js/roms/', '/test/'],
|
||||
preset: 'ts-jest',
|
||||
};
|
||||
|
57
js/apple2.ts
57
js/apple2.ts
@ -5,16 +5,8 @@ import {
|
||||
VideoModes,
|
||||
VideoModesState,
|
||||
} from './videomodes';
|
||||
import {
|
||||
HiresPage2D,
|
||||
LoresPage2D,
|
||||
VideoModes2D,
|
||||
} from './canvas';
|
||||
import {
|
||||
HiresPageGL,
|
||||
LoresPageGL,
|
||||
VideoModesGL,
|
||||
} from './gl';
|
||||
import { HiresPage2D, LoresPage2D, VideoModes2D } from './canvas';
|
||||
import { HiresPageGL, LoresPageGL, VideoModesGL } from './gl';
|
||||
import ROM from './roms/rom';
|
||||
import { Apple2IOState } from './apple2io';
|
||||
import {
|
||||
@ -82,7 +74,7 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
|
||||
private stats: Stats = {
|
||||
cycles: 0,
|
||||
frames: 0,
|
||||
renderedFrames: 0
|
||||
renderedFrames: 0,
|
||||
};
|
||||
|
||||
public ready: Promise<void>;
|
||||
@ -92,23 +84,30 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
|
||||
}
|
||||
|
||||
async init(options: Apple2Options) {
|
||||
const romImportPromise = import(`./roms/system/${options.rom}`) as Promise<{ default: new () => ROM }>;
|
||||
const characterRomImportPromise = import(`./roms/character/${options.characterRom}`) as Promise<{ default: ReadonlyUint8Array }>;
|
||||
const romImportPromise = import(
|
||||
`./roms/system/${options.rom}`
|
||||
) as Promise<{
|
||||
default: new () => ROM;
|
||||
}>;
|
||||
const characterRomImportPromise = import(
|
||||
`./roms/character/${options.characterRom}`
|
||||
) as Promise<{ default: ReadonlyUint8Array }>;
|
||||
|
||||
const LoresPage = options.gl ? LoresPageGL : LoresPage2D;
|
||||
const HiresPage = options.gl ? HiresPageGL : HiresPage2D;
|
||||
const VideoModes = options.gl ? VideoModesGL : VideoModes2D;
|
||||
|
||||
this.cpu = new CPU6502({
|
||||
flavor: options.enhanced ? FLAVOR_ROCKWELL_65C02 : FLAVOR_6502
|
||||
flavor: options.enhanced ? FLAVOR_ROCKWELL_65C02 : FLAVOR_6502,
|
||||
});
|
||||
this.vm = new VideoModes(options.canvas, options.e);
|
||||
|
||||
const [{ default: Apple2ROM }, { default: characterRom }] = await Promise.all([
|
||||
romImportPromise,
|
||||
characterRomImportPromise,
|
||||
this.vm.ready,
|
||||
]);
|
||||
const [{ default: Apple2ROM }, { default: characterRom }] =
|
||||
await Promise.all([
|
||||
romImportPromise,
|
||||
characterRomImportPromise,
|
||||
this.vm.ready,
|
||||
]);
|
||||
|
||||
this.rom = new Apple2ROM();
|
||||
this.characterRom = characterRom;
|
||||
@ -121,13 +120,22 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
|
||||
this.tick = options.tick;
|
||||
|
||||
if (options.e) {
|
||||
this.mmu = new MMU(this.cpu, this.vm, this.gr, this.gr2, this.hgr, this.hgr2, this.io, this.rom);
|
||||
this.mmu = new MMU(
|
||||
this.cpu,
|
||||
this.vm,
|
||||
this.gr,
|
||||
this.gr2,
|
||||
this.hgr,
|
||||
this.hgr2,
|
||||
this.io,
|
||||
this.rom
|
||||
);
|
||||
this.cpu.addPageHandler(this.mmu);
|
||||
} else {
|
||||
this.ram = [
|
||||
new RAM(0x00, 0x03),
|
||||
new RAM(0x0C, 0x1F),
|
||||
new RAM(0x60, 0xBF)
|
||||
new RAM(0x0c, 0x1f),
|
||||
new RAM(0x60, 0xbf),
|
||||
];
|
||||
|
||||
this.cpu.addPageHandler(this.ram[0]);
|
||||
@ -158,7 +166,8 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
|
||||
|
||||
const interval = 30;
|
||||
|
||||
let now, last = Date.now();
|
||||
let now,
|
||||
last = Date.now();
|
||||
const runFn = () => {
|
||||
const kHz = this.io.getKHz();
|
||||
now = Date.now();
|
||||
@ -228,7 +237,7 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
|
||||
vm: this.vm.getState(),
|
||||
io: this.io.getState(),
|
||||
mmu: this.mmu?.getState(),
|
||||
ram: this.ram?.map(bank => bank.getState()),
|
||||
ram: this.ram?.map((bank) => bank.getState()),
|
||||
};
|
||||
|
||||
return state;
|
||||
|
@ -12,7 +12,7 @@ type Annunciators = Record<annunciator, boolean>;
|
||||
|
||||
export interface Apple2IOState {
|
||||
annunciators: Annunciators;
|
||||
cards: Array<unknown | null>;
|
||||
cards: Array<unknown>;
|
||||
}
|
||||
|
||||
export type SampleListener = (sample: number[]) => void;
|
||||
@ -33,12 +33,12 @@ const LOC = {
|
||||
SETHIRES: 0x57, // select Hi-res
|
||||
CLRAN0: 0x58, // Set annunciator-0 output to 0
|
||||
SETAN0: 0x59, // Set annunciator-0 output to 1
|
||||
CLRAN1: 0x5A, // Set annunciator-1 output to 0
|
||||
SETAN1: 0x5B, // Set annunciator-1 output to 1
|
||||
CLRAN2: 0x5C, // Set annunciator-2 output to 0
|
||||
SETAN2: 0x5D, // Set annunciator-2 output to 1
|
||||
CLRAN3: 0x5E, // Set annunciator-3 output to 0
|
||||
SETAN3: 0x5F, // Set annunciator-3 output to 1
|
||||
CLRAN1: 0x5a, // Set annunciator-1 output to 0
|
||||
SETAN1: 0x5b, // Set annunciator-1 output to 1
|
||||
CLRAN2: 0x5c, // Set annunciator-2 output to 0
|
||||
SETAN2: 0x5d, // Set annunciator-2 output to 1
|
||||
CLRAN3: 0x5e, // Set annunciator-3 output to 0
|
||||
SETAN3: 0x5f, // Set annunciator-3 output to 1
|
||||
TAPEIN: 0x60, // bit 7: data from cassette
|
||||
PB0: 0x61, // game Pushbutton 0 / open apple (command) key data
|
||||
PB1: 0x62, // game Pushbutton 1 / closed apple (option) key data
|
||||
@ -51,7 +51,9 @@ const LOC = {
|
||||
ACCEL: 0x74, // CPU Speed control
|
||||
};
|
||||
|
||||
export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState> {
|
||||
export default class Apple2IO
|
||||
implements MemoryPages, Restorable<Apple2IOState>
|
||||
{
|
||||
private _slot: Array<Card | null> = new Array<Card | null>(7).fill(null);
|
||||
private _auxRom: Memory | null = null;
|
||||
|
||||
@ -85,7 +87,10 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
|
||||
private _tapeNext: number = 0;
|
||||
private _tapeCurrent = false;
|
||||
|
||||
constructor(private readonly cpu: CPU6502, private readonly vm: VideoModes) {
|
||||
constructor(
|
||||
private readonly cpu: CPU6502,
|
||||
private readonly vm: VideoModes
|
||||
) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
@ -99,8 +104,16 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
|
||||
|
||||
_tick() {
|
||||
const now = this.cpu.getCycles();
|
||||
const phase = this._didAudio ? (this._phase > 0 ? this._high : this._low) : 0.0;
|
||||
for (; this._sampleTime < now; this._sampleTime += this._cycles_per_sample) {
|
||||
const phase = this._didAudio
|
||||
? this._phase > 0
|
||||
? this._high
|
||||
: this._low
|
||||
: 0.0;
|
||||
for (
|
||||
;
|
||||
this._sampleTime < now;
|
||||
this._sampleTime += this._cycles_per_sample
|
||||
) {
|
||||
this._sample[this._sampleIdx++] = phase;
|
||||
if (this._sampleIdx === this._sample_size) {
|
||||
if (this._audioListener) {
|
||||
@ -114,7 +127,7 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
|
||||
}
|
||||
|
||||
_calcSampleRate() {
|
||||
this._cycles_per_sample = this._khz * 1000 / this._rate;
|
||||
this._cycles_per_sample = (this._khz * 1000) / this._rate;
|
||||
}
|
||||
|
||||
_updateKHz(khz: number) {
|
||||
@ -200,16 +213,16 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
|
||||
result = this._button[2] ? 0x80 : 0;
|
||||
break;
|
||||
case LOC.PADDLE0:
|
||||
result = (delta < (this._paddle[0] * 2756) ? 0x80 : 0x00);
|
||||
result = delta < this._paddle[0] * 2756 ? 0x80 : 0x00;
|
||||
break;
|
||||
case LOC.PADDLE1:
|
||||
result = (delta < (this._paddle[1] * 2756) ? 0x80 : 0x00);
|
||||
result = delta < this._paddle[1] * 2756 ? 0x80 : 0x00;
|
||||
break;
|
||||
case LOC.PADDLE2:
|
||||
result = (delta < (this._paddle[2] * 2756) ? 0x80 : 0x00);
|
||||
result = delta < this._paddle[2] * 2756 ? 0x80 : 0x00;
|
||||
break;
|
||||
case LOC.PADDLE3:
|
||||
result = (delta < (this._paddle[3] * 2756) ? 0x80 : 0x00);
|
||||
result = delta < this._paddle[3] * 2756 ? 0x80 : 0x00;
|
||||
break;
|
||||
case LOC.ACCEL:
|
||||
if (val !== undefined) {
|
||||
@ -225,13 +238,12 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
|
||||
if (this._tapeOffset < this._tape.length) {
|
||||
this._tapeCurrent = this._tape[this._tapeOffset][1];
|
||||
while (now >= this._tapeNext) {
|
||||
if ((this._tapeOffset % 1000) === 0) {
|
||||
if (this._tapeOffset % 1000 === 0) {
|
||||
debug(`Read ${this._tapeOffset / 1000}`);
|
||||
}
|
||||
this._tapeCurrent = this._tape[this._tapeOffset][1];
|
||||
this._tapeNext += this._tape[this._tapeOffset++][0];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
result = this._tapeCurrent ? 0x80 : 0x00;
|
||||
@ -344,7 +356,7 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
|
||||
slot = page & 0x0f;
|
||||
card = this._slot[slot];
|
||||
if (this._auxRom !== card) {
|
||||
// _debug('Setting auxRom to slot', slot);
|
||||
// _debug('Setting auxRom to slot', slot);
|
||||
this._auxRom = card;
|
||||
}
|
||||
if (card) {
|
||||
@ -380,7 +392,7 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
|
||||
slot = page & 0x0f;
|
||||
card = this._slot[slot];
|
||||
if (this._auxRom !== card) {
|
||||
// _debug('Setting auxRom to slot', slot);
|
||||
// _debug('Setting auxRom to slot', slot);
|
||||
this._auxRom = card;
|
||||
}
|
||||
if (card) {
|
||||
@ -399,13 +411,15 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
|
||||
// TODO vet more potential state
|
||||
return {
|
||||
annunciators: this._annunciators,
|
||||
cards: this._slot.map((card) => card ? card.getState() : null)
|
||||
cards: this._slot.map((card) => (card ? card.getState() : null)),
|
||||
};
|
||||
}
|
||||
|
||||
setState(state: Apple2IOState) {
|
||||
this._annunciators = state.annunciators;
|
||||
state.cards.map((cardState, idx) => this._slot[idx]?.setState(cardState));
|
||||
state.cards.map(
|
||||
(cardState, idx) => this._slot[idx]?.setState(cardState)
|
||||
);
|
||||
}
|
||||
|
||||
setSlot(slot: slot, card: Card) {
|
||||
|
@ -51,7 +51,10 @@ function writeWord(mem: Memory, addr: word, val: byte) {
|
||||
|
||||
class LineBuffer implements IterableIterator<string> {
|
||||
private prevChar: number = 0;
|
||||
constructor(private readonly line: string, private curChar: number = 0) { }
|
||||
constructor(
|
||||
private readonly line: string,
|
||||
private curChar: number = 0
|
||||
) {}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<string> {
|
||||
return this;
|
||||
@ -132,7 +135,11 @@ export default class ApplesoftCompiler {
|
||||
* @param programStart Optional start address of the program. Defaults to
|
||||
* standard Applesoft program address, 0x801.
|
||||
*/
|
||||
static compileToMemory(mem: Memory, program: string, programStart: word = PROGRAM_START) {
|
||||
static compileToMemory(
|
||||
mem: Memory,
|
||||
program: string,
|
||||
programStart: word = PROGRAM_START
|
||||
) {
|
||||
const compiler = new ApplesoftCompiler();
|
||||
compiler.compile(program);
|
||||
const compiledProgram: Uint8Array = compiler.program(programStart);
|
||||
@ -305,8 +312,8 @@ export default class ApplesoftCompiler {
|
||||
|
||||
for (const lineNo of lineNumbers) {
|
||||
const lineBytes = this.lines.get(lineNo) || [];
|
||||
const nextLineAddr = programStart + result.length + 4
|
||||
+ lineBytes.length + 1; // +1 for the zero at end of line
|
||||
const nextLineAddr =
|
||||
programStart + result.length + 4 + lineBytes.length + 1; // +1 for the zero at end of line
|
||||
result.push(nextLineAddr & 0xff, nextLineAddr >> 8);
|
||||
result.push(lineNo & 0xff, lineNo >> 8);
|
||||
result.push(...lineBytes);
|
||||
|
@ -48,7 +48,6 @@ const DEFAULT_DECOMPILE_OPTIONS: DecompileOptions = {
|
||||
const MAX_LINES = 32768;
|
||||
|
||||
export default class ApplesoftDecompiler {
|
||||
|
||||
/**
|
||||
* Returns a decompiler for the program in the given memory.
|
||||
*
|
||||
@ -57,10 +56,16 @@ export default class ApplesoftDecompiler {
|
||||
static decompilerFromMemory(ram: Memory): ApplesoftDecompiler {
|
||||
const program: byte[] = [];
|
||||
|
||||
const start = ram.read(0x00, TXTTAB) + (ram.read(0x00, TXTTAB + 1) << 8);
|
||||
const start =
|
||||
ram.read(0x00, TXTTAB) + (ram.read(0x00, TXTTAB + 1) << 8);
|
||||
const end = ram.read(0x00, PRGEND) + (ram.read(0x00, PRGEND + 1) << 8);
|
||||
if (start >= 0xc000 || end >= 0xc000) {
|
||||
throw new Error(`Program memory ${toHex(start, 4)}-${toHex(end, 4)} out of range`);
|
||||
throw new Error(
|
||||
`Program memory ${toHex(start, 4)}-${toHex(
|
||||
end,
|
||||
4
|
||||
)} out of range`
|
||||
);
|
||||
}
|
||||
for (let addr = start; addr <= end; addr++) {
|
||||
program.push(ram.read(addr >> 8, addr & 0xff));
|
||||
@ -117,8 +122,10 @@ export default class ApplesoftDecompiler {
|
||||
* is the offset of the line number of the line; the tokens follow.
|
||||
*/
|
||||
private forEachLine(
|
||||
from: number, to: number,
|
||||
callback: (offset: word) => void): void {
|
||||
from: number,
|
||||
to: number,
|
||||
callback: (offset: word) => void
|
||||
): void {
|
||||
let count = 0;
|
||||
let offset = 0;
|
||||
let nextLineAddr = this.wordAt(offset);
|
||||
@ -223,12 +230,15 @@ export default class ApplesoftDecompiler {
|
||||
* @param from The first line to print (default 0).
|
||||
* @param to The last line to print (default end of program).
|
||||
*/
|
||||
list(options: Partial<ListOptions> = {},
|
||||
from: number = 0, to: number = 65536): string {
|
||||
list(
|
||||
options: Partial<ListOptions> = {},
|
||||
from: number = 0,
|
||||
to: number = 65536
|
||||
): string {
|
||||
const allOptions = { ...DEFAULT_LIST_OPTIONS, ...options };
|
||||
|
||||
let result = '';
|
||||
this.forEachLine(from, to, offset => {
|
||||
this.forEachLine(from, to, (offset) => {
|
||||
result += this.listLine(offset, allOptions);
|
||||
});
|
||||
return result;
|
||||
@ -264,7 +274,8 @@ export default class ApplesoftDecompiler {
|
||||
|
||||
spaceIf = () => false;
|
||||
if (token === STRING_TO_TOKEN['AT']) {
|
||||
spaceIf = (nextToken) => nextToken.toUpperCase().startsWith('N');
|
||||
spaceIf = (nextToken) =>
|
||||
nextToken.toUpperCase().startsWith('N');
|
||||
}
|
||||
|
||||
offset++;
|
||||
@ -329,13 +340,20 @@ export default class ApplesoftDecompiler {
|
||||
/**
|
||||
* Decompiles the program based on the given options.
|
||||
*/
|
||||
decompile(options: Partial<DecompileOptions> = {},
|
||||
from: number = 0, to: number = 65536): string {
|
||||
decompile(
|
||||
options: Partial<DecompileOptions> = {},
|
||||
from: number = 0,
|
||||
to: number = 65536
|
||||
): string {
|
||||
const allOptions = { ...DEFAULT_DECOMPILE_OPTIONS, ...options };
|
||||
|
||||
const results: string[] = [];
|
||||
this.forEachLine(from, to, offset => {
|
||||
results.push(allOptions.style === 'compact' ? this.compactLine(offset) : this.prettyLine(offset));
|
||||
this.forEachLine(from, to, (offset) => {
|
||||
results.push(
|
||||
allOptions.style === 'compact'
|
||||
? this.compactLine(offset)
|
||||
: this.prettyLine(offset)
|
||||
);
|
||||
});
|
||||
return results.join('\n');
|
||||
}
|
||||
|
@ -1,23 +1,15 @@
|
||||
import { byte, word, Memory } from 'js/types';
|
||||
import { toHex } from 'js/util';
|
||||
import {
|
||||
CURLINE,
|
||||
ARG,
|
||||
FAC,
|
||||
ARYTAB,
|
||||
STREND,
|
||||
TXTTAB,
|
||||
VARTAB
|
||||
} from './zeropage';
|
||||
import { CURLINE, ARG, FAC, ARYTAB, STREND, TXTTAB, VARTAB } from './zeropage';
|
||||
|
||||
export type ApplesoftValue = word | number | string | ApplesoftArray;
|
||||
export type ApplesoftValue = word | string | ApplesoftArray;
|
||||
export type ApplesoftArray = Array<ApplesoftValue>;
|
||||
|
||||
export enum VariableType {
|
||||
Float = 0,
|
||||
String = 1,
|
||||
Function = 2,
|
||||
Integer = 3
|
||||
Integer = 3,
|
||||
}
|
||||
|
||||
export interface ApplesoftVariable {
|
||||
@ -27,7 +19,6 @@ export interface ApplesoftVariable {
|
||||
value: ApplesoftValue | undefined;
|
||||
}
|
||||
|
||||
|
||||
export class ApplesoftHeap {
|
||||
constructor(private mem: Memory) {}
|
||||
|
||||
@ -61,7 +52,7 @@ export class ApplesoftHeap {
|
||||
if (exponent === 0) {
|
||||
return 0;
|
||||
}
|
||||
exponent = (exponent & 0x80 ? 1 : -1) * ((exponent & 0x7F) - 1);
|
||||
exponent = (exponent & 0x80 ? 1 : -1) * ((exponent & 0x7f) - 1);
|
||||
|
||||
let msb = this.readByte(addr + 1);
|
||||
const sb3 = this.readByte(addr + 2);
|
||||
@ -74,7 +65,7 @@ export class ApplesoftHeap {
|
||||
} else {
|
||||
sign = msb & 0x80 ? -1 : 1;
|
||||
}
|
||||
msb &= 0x7F;
|
||||
msb &= 0x7f;
|
||||
const mantissa = (msb << 24) | (sb3 << 16) | (sb2 << 8) | lsb;
|
||||
|
||||
return sign * (1 + mantissa / 0x80000000) * Math.pow(2, exponent);
|
||||
@ -83,7 +74,7 @@ export class ApplesoftHeap {
|
||||
private readString(len: byte, addr: word): string {
|
||||
let str = '';
|
||||
for (let idx = 0; idx < len; idx++) {
|
||||
str += String.fromCharCode(this.readByte(addr + idx) & 0x7F);
|
||||
str += String.fromCharCode(this.readByte(addr + idx) & 0x7f);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
@ -91,13 +82,13 @@ export class ApplesoftHeap {
|
||||
private readVar(addr: word) {
|
||||
const firstByte = this.readByte(addr);
|
||||
const lastByte = this.readByte(addr + 1);
|
||||
const firstLetter = firstByte & 0x7F;
|
||||
const lastLetter = lastByte & 0x7F;
|
||||
const firstLetter = firstByte & 0x7f;
|
||||
const lastLetter = lastByte & 0x7f;
|
||||
|
||||
const name =
|
||||
String.fromCharCode(firstLetter) +
|
||||
(lastLetter ? String.fromCharCode(lastLetter) : '');
|
||||
const type = (lastByte & 0x80) >> 7 | (firstByte & 0x80) >> 6;
|
||||
const type = ((lastByte & 0x80) >> 7) | ((firstByte & 0x80) >> 6);
|
||||
|
||||
return { name, type };
|
||||
}
|
||||
@ -158,7 +149,7 @@ export class ApplesoftHeap {
|
||||
for (addr = simpleVariableTable; addr < arrayVariableTable; addr += 7) {
|
||||
const { name, type } = this.readVar(addr);
|
||||
|
||||
switch (type) {
|
||||
switch (type as VariableType) {
|
||||
case VariableType.Float:
|
||||
value = this.readFloat(addr + 2);
|
||||
break;
|
||||
|
@ -108,116 +108,116 @@ export const TOKEN_TO_STRING: Record<byte, string> = {
|
||||
0xe7: 'CHR$',
|
||||
0xe8: 'LEFT$',
|
||||
0xe9: 'RIGHT$',
|
||||
0xea: 'MID$'
|
||||
0xea: 'MID$',
|
||||
};
|
||||
|
||||
/** Map from keyword to token. */
|
||||
export const STRING_TO_TOKEN: Record<string, byte> = {
|
||||
'END': 0x80,
|
||||
'FOR': 0x81,
|
||||
'NEXT': 0x82,
|
||||
'DATA': 0x83,
|
||||
'INPUT': 0x84,
|
||||
'DEL': 0x85,
|
||||
'DIM': 0x86,
|
||||
'READ': 0x87,
|
||||
'GR': 0x88,
|
||||
'TEXT': 0x89,
|
||||
END: 0x80,
|
||||
FOR: 0x81,
|
||||
NEXT: 0x82,
|
||||
DATA: 0x83,
|
||||
INPUT: 0x84,
|
||||
DEL: 0x85,
|
||||
DIM: 0x86,
|
||||
READ: 0x87,
|
||||
GR: 0x88,
|
||||
TEXT: 0x89,
|
||||
'PR#': 0x8a,
|
||||
'IN#': 0x8b,
|
||||
'CALL': 0x8c,
|
||||
'PLOT': 0x8d,
|
||||
'HLIN': 0x8e,
|
||||
'VLIN': 0x8f,
|
||||
'HGR2': 0x90,
|
||||
'HGR': 0x91,
|
||||
CALL: 0x8c,
|
||||
PLOT: 0x8d,
|
||||
HLIN: 0x8e,
|
||||
VLIN: 0x8f,
|
||||
HGR2: 0x90,
|
||||
HGR: 0x91,
|
||||
'HCOLOR=': 0x92,
|
||||
'HPLOT': 0x93,
|
||||
'DRAW': 0x94,
|
||||
'XDRAW': 0x95,
|
||||
'HTAB': 0x96,
|
||||
'HOME': 0x97,
|
||||
HPLOT: 0x93,
|
||||
DRAW: 0x94,
|
||||
XDRAW: 0x95,
|
||||
HTAB: 0x96,
|
||||
HOME: 0x97,
|
||||
'ROT=': 0x98,
|
||||
'SCALE=': 0x99,
|
||||
'SHLOAD': 0x9a,
|
||||
'TRACE': 0x9b,
|
||||
'NOTRACE': 0x9c,
|
||||
'NORMAL': 0x9d,
|
||||
'INVERSE': 0x9e,
|
||||
'FLASH': 0x9f,
|
||||
SHLOAD: 0x9a,
|
||||
TRACE: 0x9b,
|
||||
NOTRACE: 0x9c,
|
||||
NORMAL: 0x9d,
|
||||
INVERSE: 0x9e,
|
||||
FLASH: 0x9f,
|
||||
'COLOR=': 0xa0,
|
||||
'POP=': 0xa1,
|
||||
'VTAB': 0xa2,
|
||||
VTAB: 0xa2,
|
||||
'HIMEM:': 0xa3,
|
||||
'LOMEM:': 0xa4,
|
||||
'ONERR': 0xa5,
|
||||
'RESUME': 0xa6,
|
||||
'RECALL': 0xa7,
|
||||
'STORE': 0xa8,
|
||||
ONERR: 0xa5,
|
||||
RESUME: 0xa6,
|
||||
RECALL: 0xa7,
|
||||
STORE: 0xa8,
|
||||
'SPEED=': 0xa9,
|
||||
'LET': 0xaa,
|
||||
'GOTO': 0xab,
|
||||
'RUN': 0xac,
|
||||
'IF': 0xad,
|
||||
'RESTORE': 0xae,
|
||||
LET: 0xaa,
|
||||
GOTO: 0xab,
|
||||
RUN: 0xac,
|
||||
IF: 0xad,
|
||||
RESTORE: 0xae,
|
||||
'&': 0xaf,
|
||||
'GOSUB': 0xb0,
|
||||
'RETURN': 0xb1,
|
||||
'REM': 0xb2,
|
||||
'STOP': 0xb3,
|
||||
'ON': 0xb4,
|
||||
'WAIT': 0xb5,
|
||||
'LOAD': 0xb6,
|
||||
'SAVE': 0xb7,
|
||||
'DEF': 0xb8,
|
||||
'POKE': 0xb9,
|
||||
'PRINT': 0xba,
|
||||
'CONT': 0xbb,
|
||||
'LIST': 0xbc,
|
||||
'CLEAR': 0xbd,
|
||||
'GET': 0xbe,
|
||||
'NEW': 0xbf,
|
||||
GOSUB: 0xb0,
|
||||
RETURN: 0xb1,
|
||||
REM: 0xb2,
|
||||
STOP: 0xb3,
|
||||
ON: 0xb4,
|
||||
WAIT: 0xb5,
|
||||
LOAD: 0xb6,
|
||||
SAVE: 0xb7,
|
||||
DEF: 0xb8,
|
||||
POKE: 0xb9,
|
||||
PRINT: 0xba,
|
||||
CONT: 0xbb,
|
||||
LIST: 0xbc,
|
||||
CLEAR: 0xbd,
|
||||
GET: 0xbe,
|
||||
NEW: 0xbf,
|
||||
'TAB(': 0xc0,
|
||||
'TO': 0xc1,
|
||||
'FN': 0xc2,
|
||||
TO: 0xc1,
|
||||
FN: 0xc2,
|
||||
'SPC(': 0xc3,
|
||||
'THEN': 0xc4,
|
||||
'AT': 0xc5,
|
||||
'NOT': 0xc6,
|
||||
'STEP': 0xc7,
|
||||
THEN: 0xc4,
|
||||
AT: 0xc5,
|
||||
NOT: 0xc6,
|
||||
STEP: 0xc7,
|
||||
'+': 0xc8,
|
||||
'-': 0xc9,
|
||||
'*': 0xca,
|
||||
'/': 0xcb,
|
||||
'^': 0xcc,
|
||||
'AND': 0xcd,
|
||||
'OR': 0xce,
|
||||
AND: 0xcd,
|
||||
OR: 0xce,
|
||||
'>': 0xcf,
|
||||
'=': 0xd0,
|
||||
'<': 0xd1,
|
||||
'SGN': 0xd2,
|
||||
'INT': 0xd3,
|
||||
'ABS': 0xd4,
|
||||
'USR': 0xd5,
|
||||
'FRE': 0xd6,
|
||||
SGN: 0xd2,
|
||||
INT: 0xd3,
|
||||
ABS: 0xd4,
|
||||
USR: 0xd5,
|
||||
FRE: 0xd6,
|
||||
'SCRN(': 0xd7,
|
||||
'PDL': 0xd8,
|
||||
'POS': 0xd9,
|
||||
'SQR': 0xda,
|
||||
'RND': 0xdb,
|
||||
'LOG': 0xdc,
|
||||
'EXP': 0xdd,
|
||||
'COS': 0xde,
|
||||
'SIN': 0xdf,
|
||||
'TAN': 0xe0,
|
||||
'ATN': 0xe1,
|
||||
'PEEK': 0xe2,
|
||||
'LEN': 0xe3,
|
||||
'STR$': 0xe4,
|
||||
'VAL': 0xe5,
|
||||
'ASC': 0xe6,
|
||||
'CHR$': 0xe7,
|
||||
'LEFT$': 0xe8,
|
||||
'RIGHT$': 0xe9,
|
||||
'MID$': 0xea
|
||||
PDL: 0xd8,
|
||||
POS: 0xd9,
|
||||
SQR: 0xda,
|
||||
RND: 0xdb,
|
||||
LOG: 0xdc,
|
||||
EXP: 0xdd,
|
||||
COS: 0xde,
|
||||
SIN: 0xdf,
|
||||
TAN: 0xe0,
|
||||
ATN: 0xe1,
|
||||
PEEK: 0xe2,
|
||||
LEN: 0xe3,
|
||||
STR$: 0xe4,
|
||||
VAL: 0xe5,
|
||||
ASC: 0xe6,
|
||||
CHR$: 0xe7,
|
||||
LEFT$: 0xe8,
|
||||
RIGHT$: 0xe9,
|
||||
MID$: 0xea,
|
||||
};
|
||||
|
@ -11,17 +11,17 @@ export const TXTTAB = 0x67;
|
||||
/** Start of variables (word) */
|
||||
export const VARTAB = 0x69;
|
||||
/** Start of arrays (word) */
|
||||
export const ARYTAB = 0x6B;
|
||||
export const ARYTAB = 0x6b;
|
||||
/** End of strings (word). (Strings are allocated down from HIMEM.) */
|
||||
export const STREND = 0x6D;
|
||||
export const STREND = 0x6d;
|
||||
/** Current line */
|
||||
export const CURLINE = 0x75;
|
||||
/** Floating Point accumulator (float) */
|
||||
export const FAC = 0x9D;
|
||||
export const FAC = 0x9d;
|
||||
/** Floating Point arguments (float) */
|
||||
export const ARG = 0xA5;
|
||||
export const ARG = 0xa5;
|
||||
/**
|
||||
* End of program (word). This is actually 1 or 2 bytes past the three
|
||||
* zero bytes that end the program.
|
||||
*/
|
||||
export const PRGEND = 0xAF;
|
||||
export const PRGEND = 0xaf;
|
||||
|
57
js/base64.ts
57
js/base64.ts
@ -5,7 +5,9 @@ const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||
/** Encode an array of bytes in base64. */
|
||||
export function base64_encode(data: null | undefined): undefined;
|
||||
export function base64_encode(data: memory): string;
|
||||
export function base64_encode(data: memory | null | undefined): string | undefined {
|
||||
export function base64_encode(
|
||||
data: memory | null | undefined
|
||||
): string | undefined {
|
||||
// Twacked by Will Scullin to handle arrays of 'bytes'
|
||||
|
||||
// http://kevin.vanzonneveld.net
|
||||
@ -25,28 +27,39 @@ export function base64_encode(data: memory | null | undefined): string | undefin
|
||||
// return atob(data);
|
||||
//}
|
||||
|
||||
|
||||
let o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc='';
|
||||
let o1,
|
||||
o2,
|
||||
o3,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
bits,
|
||||
i = 0,
|
||||
ac = 0,
|
||||
enc = '';
|
||||
const tmp_arr = [];
|
||||
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
do { // pack three octets into four hexets
|
||||
do {
|
||||
// pack three octets into four hexets
|
||||
o1 = data[i++];
|
||||
o2 = data[i++];
|
||||
o3 = data[i++];
|
||||
|
||||
bits = o1<<16 | o2<<8 | o3;
|
||||
bits = (o1 << 16) | (o2 << 8) | o3;
|
||||
|
||||
h1 = bits>>18 & 0x3f;
|
||||
h2 = bits>>12 & 0x3f;
|
||||
h3 = bits>>6 & 0x3f;
|
||||
h1 = (bits >> 18) & 0x3f;
|
||||
h2 = (bits >> 12) & 0x3f;
|
||||
h3 = (bits >> 6) & 0x3f;
|
||||
h4 = bits & 0x3f;
|
||||
|
||||
// use hexets to index into b64, and append result to encoded string
|
||||
tmp_arr[ac++] = B64.charAt(h1) + B64.charAt(h2) + B64.charAt(h3) + B64.charAt(h4);
|
||||
tmp_arr[ac++] =
|
||||
B64.charAt(h1) + B64.charAt(h2) + B64.charAt(h3) + B64.charAt(h4);
|
||||
} while (i < data.length);
|
||||
|
||||
enc = tmp_arr.join('');
|
||||
@ -68,7 +81,9 @@ export function base64_decode(data: null | undefined): undefined;
|
||||
/** Returns an array of bytes from the given base64-encoded string. */
|
||||
export function base64_decode(data: string): memory;
|
||||
/** Returns an array of bytes from the given base64-encoded string. */
|
||||
export function base64_decode(data: string | null | undefined): memory | undefined {
|
||||
export function base64_decode(
|
||||
data: string | null | undefined
|
||||
): memory | undefined {
|
||||
// Twacked by Will Scullin to handle arrays of 'bytes'
|
||||
|
||||
// http://kevin.vanzonneveld.net
|
||||
@ -91,23 +106,33 @@ export function base64_decode(data: string | null | undefined): memory | undefin
|
||||
// return btoa(data);
|
||||
//}
|
||||
|
||||
let o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0;
|
||||
let o1,
|
||||
o2,
|
||||
o3,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
bits,
|
||||
i = 0,
|
||||
ac = 0;
|
||||
const tmp_arr = [];
|
||||
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
do { // unpack four hexets into three octets using index points in B64
|
||||
do {
|
||||
// unpack four hexets into three octets using index points in B64
|
||||
h1 = B64.indexOf(data.charAt(i++));
|
||||
h2 = B64.indexOf(data.charAt(i++));
|
||||
h3 = B64.indexOf(data.charAt(i++));
|
||||
h4 = B64.indexOf(data.charAt(i++));
|
||||
|
||||
bits = h1<<18 | h2<<12 | h3<<6 | h4;
|
||||
bits = (h1 << 18) | (h2 << 12) | (h3 << 6) | h4;
|
||||
|
||||
o1 = bits>>16 & 0xff;
|
||||
o2 = bits>>8 & 0xff;
|
||||
o1 = (bits >> 16) & 0xff;
|
||||
o2 = (bits >> 8) & 0xff;
|
||||
o3 = bits & 0xff;
|
||||
|
||||
tmp_arr[ac++] = o1;
|
||||
@ -126,7 +151,7 @@ const DATA_URL_PREFIX = 'data:application/octet-stream;base64,';
|
||||
|
||||
export function base64_json_parse(json: string): unknown {
|
||||
const reviver = (_key: string, value: unknown) => {
|
||||
if (typeof value ==='string' && value.startsWith(DATA_URL_PREFIX)) {
|
||||
if (typeof value === 'string' && value.startsWith(DATA_URL_PREFIX)) {
|
||||
return base64_decode(value.slice(DATA_URL_PREFIX.length));
|
||||
}
|
||||
return value;
|
||||
|
301
js/canvas.ts
301
js/canvas.ts
@ -8,15 +8,11 @@ import {
|
||||
VideoModes,
|
||||
VideoModesState,
|
||||
bank,
|
||||
pageNo
|
||||
pageNo,
|
||||
} from './videomodes';
|
||||
|
||||
const dim = (c: Color): Color => {
|
||||
return [
|
||||
c[0] * 0.75 & 0xff,
|
||||
c[1] * 0.75 & 0xff,
|
||||
c[2] * 0.75 & 0xff
|
||||
];
|
||||
return [(c[0] * 0.75) & 0xff, (c[1] * 0.75) & 0xff, (c[2] * 0.75) & 0xff];
|
||||
};
|
||||
|
||||
// hires colors
|
||||
@ -49,25 +45,25 @@ const _colors: Color[] = [
|
||||
|
||||
//
|
||||
const r4 = [
|
||||
0, // Black
|
||||
2, // Dark Blue
|
||||
4, // Dark Green
|
||||
6, // Medium Blue
|
||||
0, // Black
|
||||
2, // Dark Blue
|
||||
4, // Dark Green
|
||||
6, // Medium Blue
|
||||
|
||||
8, // Brown
|
||||
5, // Gray 1
|
||||
12, // Light Green
|
||||
14, // Aqua
|
||||
8, // Brown
|
||||
5, // Gray 1
|
||||
12, // Light Green
|
||||
14, // Aqua
|
||||
|
||||
1, // Red
|
||||
3, // Purple
|
||||
10, // Gray 2
|
||||
7, // Pink
|
||||
1, // Red
|
||||
3, // Purple
|
||||
10, // Gray 2
|
||||
7, // Pink
|
||||
|
||||
9, // Orange
|
||||
11, // Light Blue
|
||||
13, // Yellow
|
||||
15 // White
|
||||
9, // Orange
|
||||
11, // Light Blue
|
||||
13, // Yellow
|
||||
15, // White
|
||||
] as const;
|
||||
|
||||
const dcolors: Color[] = [
|
||||
@ -93,7 +89,7 @@ const notDirty: Region = {
|
||||
top: 193,
|
||||
bottom: -1,
|
||||
left: 561,
|
||||
right: -1
|
||||
right: -1,
|
||||
} as const;
|
||||
|
||||
/****************************************************************************
|
||||
@ -131,14 +127,18 @@ export class LoresPage2D implements LoresPage {
|
||||
}
|
||||
|
||||
private _drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
const c0 = color[0],
|
||||
c1 = color[1],
|
||||
c2 = color[2];
|
||||
data[off + 0] = data[off + 4] = c0;
|
||||
data[off + 1] = data[off + 5] = c1;
|
||||
data[off + 2] = data[off + 6] = c2;
|
||||
}
|
||||
|
||||
private _drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
const c0 = color[0],
|
||||
c1 = color[1],
|
||||
c2 = color[2];
|
||||
data[off + 0] = c0;
|
||||
data[off + 1] = c1;
|
||||
data[off + 2] = c2;
|
||||
@ -148,10 +148,10 @@ export class LoresPage2D implements LoresPage {
|
||||
let inverse = false;
|
||||
if (this.e) {
|
||||
if (!this.vm._80colMode && !this.vm.altCharMode) {
|
||||
inverse = ((val & 0xc0) === 0x40) && this._blink;
|
||||
inverse = (val & 0xc0) === 0x40 && this._blink;
|
||||
}
|
||||
} else {
|
||||
inverse = !((val & 0x80) || (val & 0x40) && this._blink);
|
||||
inverse = !(val & 0x80 || (val & 0x40 && this._blink));
|
||||
}
|
||||
return inverse;
|
||||
}
|
||||
@ -177,19 +177,22 @@ export class LoresPage2D implements LoresPage {
|
||||
// These are used by both bank 0 and 1
|
||||
|
||||
private _start() {
|
||||
return (0x04 * this.page);
|
||||
return 0x04 * this.page;
|
||||
}
|
||||
|
||||
private _end() { return (0x04 * this.page) + 0x03; }
|
||||
private _end() {
|
||||
return 0x04 * this.page + 0x03;
|
||||
}
|
||||
|
||||
private _read(page: byte, off: byte, bank: bank) {
|
||||
const addr = (page << 8) | off, base = addr & 0x3FF;
|
||||
const addr = (page << 8) | off,
|
||||
base = addr & 0x3ff;
|
||||
return this._buffer[bank][base];
|
||||
}
|
||||
|
||||
private _write(page: byte, off: byte, val: byte, bank: bank) {
|
||||
const addr = (page << 8) | off;
|
||||
const base = addr & 0x3FF;
|
||||
const base = addr & 0x3ff;
|
||||
let fore, back;
|
||||
|
||||
if (this._buffer[bank][base] === val && !this._refreshing) {
|
||||
@ -201,23 +204,35 @@ export class LoresPage2D implements LoresPage {
|
||||
const adj = off - col;
|
||||
|
||||
// 000001cd eabab000 -> 000abcde
|
||||
const ab = (adj & 0x18);
|
||||
const ab = adj & 0x18;
|
||||
const cd = (page & 0x03) << 1;
|
||||
const ee = adj >> 7;
|
||||
const row = ab | cd | ee;
|
||||
|
||||
const data = this.imageData.data;
|
||||
if ((row < 24) && (col < 40)) {
|
||||
if (row < 24 && col < 40) {
|
||||
let y = row << 3;
|
||||
if (y < this.dirty.top) { this.dirty.top = y; }
|
||||
if (y < this.dirty.top) {
|
||||
this.dirty.top = y;
|
||||
}
|
||||
y += 8;
|
||||
if (y > this.dirty.bottom) { this.dirty.bottom = y; }
|
||||
if (y > this.dirty.bottom) {
|
||||
this.dirty.bottom = y;
|
||||
}
|
||||
let x = col * 14;
|
||||
if (x < this.dirty.left) { this.dirty.left = x; }
|
||||
if (x < this.dirty.left) {
|
||||
this.dirty.left = x;
|
||||
}
|
||||
x += 14;
|
||||
if (x > this.dirty.right) { this.dirty.right = x; }
|
||||
if (x > this.dirty.right) {
|
||||
this.dirty.right = x;
|
||||
}
|
||||
|
||||
if (this.vm.textMode || this.vm.hiresMode || (this.vm.mixedMode && row > 19)) {
|
||||
if (
|
||||
this.vm.textMode ||
|
||||
this.vm.hiresMode ||
|
||||
(this.vm.mixedMode && row > 19)
|
||||
) {
|
||||
if (this.vm._80colMode) {
|
||||
const inverse = this._checkInverse(val);
|
||||
|
||||
@ -225,15 +240,16 @@ export class LoresPage2D implements LoresPage {
|
||||
back = inverse ? whiteCol : blackCol;
|
||||
|
||||
if (!this.vm.altCharMode) {
|
||||
val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
|
||||
val = val >= 0x40 && val < 0x80 ? val - 0x40 : val;
|
||||
}
|
||||
|
||||
let offset = (col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
|
||||
let offset =
|
||||
(col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
|
||||
|
||||
for (let jdx = 0; jdx < 8; jdx++) {
|
||||
let b = this.charset[val * 8 + jdx];
|
||||
for (let idx = 0; idx < 7; idx++) {
|
||||
const color = (b & 0x01) ? back : fore;
|
||||
const color = b & 0x01 ? back : fore;
|
||||
this._drawHalfPixel(data, offset, color);
|
||||
b >>= 1;
|
||||
offset += 4;
|
||||
@ -249,7 +265,7 @@ export class LoresPage2D implements LoresPage {
|
||||
back = inverse ? whiteCol : blackCol;
|
||||
|
||||
if (!this.vm.altCharMode) {
|
||||
val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
|
||||
val = val >= 0x40 && val < 0x80 ? val - 0x40 : val;
|
||||
}
|
||||
|
||||
let offset = (col * 14 + row * 560 * 8) * 4;
|
||||
@ -263,7 +279,7 @@ export class LoresPage2D implements LoresPage {
|
||||
for (let jdx = 0; jdx < 8; jdx++) {
|
||||
let b = this.charset[val * 8 + jdx];
|
||||
for (let idx = 0; idx < 7; idx++) {
|
||||
const color = (b & 0x01) ? back : fore;
|
||||
const color = b & 0x01 ? back : fore;
|
||||
this._drawPixel(data, offset, color);
|
||||
b >>= 1;
|
||||
offset += 8;
|
||||
@ -271,7 +287,10 @@ export class LoresPage2D implements LoresPage {
|
||||
offset += 546 * 4;
|
||||
}
|
||||
} else {
|
||||
const colorMode = this.vm.mixedMode && !this.vm.textMode && !this.vm.monoMode;
|
||||
const colorMode =
|
||||
this.vm.mixedMode &&
|
||||
!this.vm.textMode &&
|
||||
!this.vm.monoMode;
|
||||
// var val0 = col > 0 ? _buffer[0][base - 1] : 0;
|
||||
// var val2 = col < 39 ? _buffer[0][base + 1] : 0;
|
||||
|
||||
@ -281,7 +300,9 @@ export class LoresPage2D implements LoresPage {
|
||||
if (colorMode) {
|
||||
// var b0 = charset[val0 * 8 + jdx];
|
||||
// var b2 = charset[val2 * 8 + jdx];
|
||||
if (inverse) { b ^= 0x1ff; }
|
||||
if (inverse) {
|
||||
b ^= 0x1ff;
|
||||
}
|
||||
}
|
||||
|
||||
for (let idx = 0; idx < 7; idx++) {
|
||||
@ -298,7 +319,7 @@ export class LoresPage2D implements LoresPage {
|
||||
}
|
||||
odd = !odd;
|
||||
} else {
|
||||
color = (b & 0x80) ? fore : back;
|
||||
color = b & 0x80 ? fore : back;
|
||||
}
|
||||
this._drawPixel(data, offset, color);
|
||||
b <<= 1;
|
||||
@ -310,17 +331,18 @@ export class LoresPage2D implements LoresPage {
|
||||
}
|
||||
} else {
|
||||
if (this.vm._80colMode && !this.vm.an3State) {
|
||||
let offset = (col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
|
||||
let offset =
|
||||
(col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
|
||||
if (this.vm.monoMode) {
|
||||
for (let jdx = 0; jdx < 8; jdx++) {
|
||||
let b = (jdx < 4) ? (val & 0x0f) : (val >> 4);
|
||||
b |= (b << 4);
|
||||
b |= (b << 8);
|
||||
let b = jdx < 4 ? val & 0x0f : val >> 4;
|
||||
b |= b << 4;
|
||||
b |= b << 8;
|
||||
if (col & 0x1) {
|
||||
b >>= 2;
|
||||
}
|
||||
for (let idx = 0; idx < 7; idx++) {
|
||||
const color = (b & 0x01) ? whiteCol : blackCol;
|
||||
const color = b & 0x01 ? whiteCol : blackCol;
|
||||
this._drawHalfPixel(data, offset, color);
|
||||
b >>= 1;
|
||||
offset += 4;
|
||||
@ -332,8 +354,8 @@ export class LoresPage2D implements LoresPage {
|
||||
val = ((val & 0x77) << 1) | ((val & 0x88) >> 3);
|
||||
}
|
||||
for (let jdx = 0; jdx < 8; jdx++) {
|
||||
const color = _colors[(jdx < 4) ?
|
||||
(val & 0x0f) : (val >> 4)];
|
||||
const color =
|
||||
_colors[jdx < 4 ? val & 0x0f : val >> 4];
|
||||
for (let idx = 0; idx < 7; idx++) {
|
||||
this._drawHalfPixel(data, offset, color);
|
||||
offset += 4;
|
||||
@ -346,14 +368,14 @@ export class LoresPage2D implements LoresPage {
|
||||
|
||||
if (this.vm.monoMode) {
|
||||
for (let jdx = 0; jdx < 8; jdx++) {
|
||||
let b = (jdx < 4) ? (val & 0x0f) : (val >> 4);
|
||||
b |= (b << 4);
|
||||
b |= (b << 8);
|
||||
let b = jdx < 4 ? val & 0x0f : val >> 4;
|
||||
b |= b << 4;
|
||||
b |= b << 8;
|
||||
if (col & 0x1) {
|
||||
b >>= 2;
|
||||
}
|
||||
for (let idx = 0; idx < 14; idx++) {
|
||||
const color = (b & 0x0001) ? whiteCol : blackCol;
|
||||
const color = b & 0x0001 ? whiteCol : blackCol;
|
||||
this._drawHalfPixel(data, offset, color);
|
||||
b >>= 1;
|
||||
offset += 4;
|
||||
@ -362,7 +384,8 @@ export class LoresPage2D implements LoresPage {
|
||||
}
|
||||
} else {
|
||||
for (let jdx = 0; jdx < 8; jdx++) {
|
||||
const color = _colors[(jdx < 4) ? (val & 0x0f) : (val >> 4)];
|
||||
const color =
|
||||
_colors[jdx < 4 ? val & 0x0f : val >> 4];
|
||||
for (let idx = 0; idx < 7; idx++) {
|
||||
this._drawPixel(data, offset, color);
|
||||
offset += 8;
|
||||
@ -376,7 +399,8 @@ export class LoresPage2D implements LoresPage {
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.highColorTextMode = !this.vm.an3State && this.vm.textMode && !this.vm._80colMode;
|
||||
this.highColorTextMode =
|
||||
!this.vm.an3State && this.vm.textMode && !this.vm._80colMode;
|
||||
|
||||
let addr = 0x400 * this.page;
|
||||
this._refreshing = true;
|
||||
@ -395,7 +419,7 @@ export class LoresPage2D implements LoresPage {
|
||||
this._blink = !this._blink;
|
||||
for (let idx = 0; idx < 0x400; idx++, addr++) {
|
||||
const b = this._buffer[0][idx];
|
||||
if ((b & 0xC0) === 0x40) {
|
||||
if ((b & 0xc0) === 0x40) {
|
||||
this._write(addr >> 8, addr & 0xff, this._buffer[0][idx], 0);
|
||||
}
|
||||
}
|
||||
@ -424,7 +448,7 @@ export class LoresPage2D implements LoresPage {
|
||||
buffer: [
|
||||
new Uint8Array(this._buffer[0]),
|
||||
new Uint8Array(this._buffer[1]),
|
||||
]
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@ -443,25 +467,29 @@ export class LoresPage2D implements LoresPage {
|
||||
}
|
||||
|
||||
private mapCharCode(charCode: byte) {
|
||||
charCode &= 0x7F;
|
||||
charCode &= 0x7f;
|
||||
if (charCode < 0x20) {
|
||||
charCode += 0x40;
|
||||
}
|
||||
if (!this.e && (charCode >= 0x60)) {
|
||||
if (!this.e && charCode >= 0x60) {
|
||||
charCode -= 0x40;
|
||||
}
|
||||
return charCode;
|
||||
}
|
||||
|
||||
getText() {
|
||||
let buffer = '', line, charCode;
|
||||
let buffer = '',
|
||||
line,
|
||||
charCode;
|
||||
let row, col, base;
|
||||
for (row = 0; row < 24; row++) {
|
||||
base = this.rowToBase(row);
|
||||
line = '';
|
||||
if (this.e && this.vm._80colMode) {
|
||||
for (col = 0; col < 80; col++) {
|
||||
charCode = this.mapCharCode(this._buffer[1 - col % 2][base + Math.floor(col / 2)]);
|
||||
charCode = this.mapCharCode(
|
||||
this._buffer[1 - (col % 2)][base + Math.floor(col / 2)]
|
||||
);
|
||||
line += String.fromCharCode(charCode);
|
||||
}
|
||||
} else {
|
||||
@ -498,7 +526,7 @@ export class HiresPage2D implements HiresPage {
|
||||
|
||||
constructor(
|
||||
private vm: VideoModes,
|
||||
private page: pageNo,
|
||||
private page: pageNo
|
||||
) {
|
||||
this.imageData = this.vm.context.createImageData(560, 192);
|
||||
this.imageData.data.fill(0xff);
|
||||
@ -509,7 +537,9 @@ export class HiresPage2D implements HiresPage {
|
||||
}
|
||||
|
||||
private _drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
const c0 = color[0],
|
||||
c1 = color[1],
|
||||
c2 = color[2];
|
||||
|
||||
data[off + 0] = data[off + 4] = c0;
|
||||
data[off + 1] = data[off + 5] = c1;
|
||||
@ -517,7 +547,9 @@ export class HiresPage2D implements HiresPage {
|
||||
}
|
||||
|
||||
private _drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
const c0 = color[0],
|
||||
c1 = color[1],
|
||||
c2 = color[2];
|
||||
|
||||
data[off + 0] = c0;
|
||||
data[off + 1] = c1;
|
||||
@ -529,7 +561,9 @@ export class HiresPage2D implements HiresPage {
|
||||
//
|
||||
|
||||
private _draw3Pixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
const c0 = color[0],
|
||||
c1 = color[1],
|
||||
c2 = color[2];
|
||||
|
||||
data[off + 0] = data[off + 4] = data[off + 8] = c0;
|
||||
data[off + 1] = data[off + 5] = data[off + 9] = c1;
|
||||
@ -537,7 +571,9 @@ export class HiresPage2D implements HiresPage {
|
||||
}
|
||||
|
||||
private _draw4Pixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
const c0 = color[0], c1 = color[1], c2 = color[2];
|
||||
const c0 = color[0],
|
||||
c1 = color[1],
|
||||
c2 = color[2];
|
||||
|
||||
data[off + 0] = data[off + 4] = data[off + 8] = data[off + 12] = c0;
|
||||
data[off + 1] = data[off + 5] = data[off + 9] = data[off + 13] = c1;
|
||||
@ -562,18 +598,23 @@ export class HiresPage2D implements HiresPage {
|
||||
};
|
||||
}
|
||||
|
||||
private _start() { return (0x20 * this.page); }
|
||||
private _start() {
|
||||
return 0x20 * this.page;
|
||||
}
|
||||
|
||||
private _end() { return (0x020 * this.page) + 0x1f; }
|
||||
private _end() {
|
||||
return 0x020 * this.page + 0x1f;
|
||||
}
|
||||
|
||||
private _read(page: byte, off: byte, bank: bank) {
|
||||
const addr = (page << 8) | off, base = addr & 0x1FFF;
|
||||
const addr = (page << 8) | off,
|
||||
base = addr & 0x1fff;
|
||||
return this._buffer[bank][base];
|
||||
}
|
||||
|
||||
private _write(page: byte, off: byte, val: byte, bank: bank) {
|
||||
const addr = (page << 8) | off;
|
||||
const base = addr & 0x1FFF;
|
||||
const base = addr & 0x1fff;
|
||||
|
||||
if (this._buffer[bank][base] === val && !this._refreshing) {
|
||||
return;
|
||||
@ -584,7 +625,7 @@ export class HiresPage2D implements HiresPage {
|
||||
const adj = off - col;
|
||||
|
||||
// 000001cd eabab000 -> 000abcde
|
||||
const ab = (adj & 0x18);
|
||||
const ab = adj & 0x18;
|
||||
const cd = (page & 0x03) << 1;
|
||||
const e = adj >> 7;
|
||||
|
||||
@ -593,17 +634,25 @@ export class HiresPage2D implements HiresPage {
|
||||
|
||||
const data = this.imageData.data;
|
||||
let dx, dy;
|
||||
if ((rowa < 24) && (col < 40) && (this.vm.hiresMode || this._refreshing)) {
|
||||
let y = rowa << 4 | rowb << 1;
|
||||
if (y < this.dirty.top) { this.dirty.top = y; }
|
||||
if (rowa < 24 && col < 40 && (this.vm.hiresMode || this._refreshing)) {
|
||||
let y = (rowa << 4) | (rowb << 1);
|
||||
if (y < this.dirty.top) {
|
||||
this.dirty.top = y;
|
||||
}
|
||||
y += 1;
|
||||
if (y > this.dirty.bottom) { this.dirty.bottom = y; }
|
||||
if (y > this.dirty.bottom) {
|
||||
this.dirty.bottom = y;
|
||||
}
|
||||
let x = col * 14 - 2;
|
||||
if (x < this.dirty.left) { this.dirty.left = x; }
|
||||
if (x < this.dirty.left) {
|
||||
this.dirty.left = x;
|
||||
}
|
||||
x += 18;
|
||||
if (x > this.dirty.right) { this.dirty.right = x; }
|
||||
if (x > this.dirty.right) {
|
||||
this.dirty.right = x;
|
||||
}
|
||||
|
||||
dy = rowa << 4 | rowb << 1;
|
||||
dy = (rowa << 4) | (rowb << 1);
|
||||
let bz, b0, b1, b2, b3, b4, c;
|
||||
if (this.oneSixtyMode && !this.vm.monoMode) {
|
||||
// 1 byte = two pixels, but 3:4 ratio
|
||||
@ -626,7 +675,9 @@ export class HiresPage2D implements HiresPage {
|
||||
// 76543210 76543210 76543210 76543210
|
||||
// 1111222 2333344 4455556 6667777
|
||||
|
||||
const mod = col % 2, mcol = col - mod, baseOff = base - mod;
|
||||
const mod = col % 2,
|
||||
mcol = col - mod,
|
||||
baseOff = base - mod;
|
||||
bz = this._buffer[0][baseOff - 1];
|
||||
b0 = this._buffer[1][baseOff];
|
||||
b1 = this._buffer[0][baseOff];
|
||||
@ -635,14 +686,14 @@ export class HiresPage2D implements HiresPage {
|
||||
b4 = this._buffer[1][baseOff + 2];
|
||||
c = [
|
||||
0,
|
||||
((b0 & 0x0f) >> 0), // 0
|
||||
(b0 & 0x0f) >> 0, // 0
|
||||
((b0 & 0x70) >> 4) | ((b1 & 0x01) << 3), // 1
|
||||
((b1 & 0x1e) >> 1), // 2
|
||||
(b1 & 0x1e) >> 1, // 2
|
||||
((b1 & 0x60) >> 5) | ((b2 & 0x03) << 2), // 3
|
||||
((b2 & 0x3c) >> 2), // 4
|
||||
(b2 & 0x3c) >> 2, // 4
|
||||
((b2 & 0x40) >> 6) | ((b3 & 0x07) << 1), // 5
|
||||
((b3 & 0x78) >> 3), // 6
|
||||
0
|
||||
(b3 & 0x78) >> 3, // 6
|
||||
0,
|
||||
]; // 7
|
||||
const hb = [
|
||||
0,
|
||||
@ -653,7 +704,7 @@ export class HiresPage2D implements HiresPage {
|
||||
b2 & 0x80, // 4
|
||||
b3 & 0x80, // 5
|
||||
b3 & 0x80, // 6
|
||||
0
|
||||
0,
|
||||
]; // 7
|
||||
if (col > 0) {
|
||||
c[0] = (bz & 0x78) >> 3;
|
||||
@ -695,16 +746,17 @@ export class HiresPage2D implements HiresPage {
|
||||
} else if (this.colorDHRMode) {
|
||||
this._drawHalfPixel(data, offset, dcolor);
|
||||
} else if (
|
||||
((c[idx] !== c[idx - 1]) && (c[idx] !== c[idx + 1])) &&
|
||||
(((bits & 0x1c) === 0x1c) ||
|
||||
((bits & 0x70) === 0x70) ||
|
||||
((bits & 0x38) === 0x38))
|
||||
c[idx] !== c[idx - 1] &&
|
||||
c[idx] !== c[idx + 1] &&
|
||||
((bits & 0x1c) === 0x1c ||
|
||||
(bits & 0x70) === 0x70 ||
|
||||
(bits & 0x38) === 0x38)
|
||||
) {
|
||||
this._drawHalfPixel(data, offset, whiteCol);
|
||||
} else if (
|
||||
(bits & 0x38) ||
|
||||
(c[idx] === c[idx + 1]) ||
|
||||
(c[idx] === c[idx - 1])
|
||||
bits & 0x38 ||
|
||||
c[idx] === c[idx + 1] ||
|
||||
c[idx] === c[idx - 1]
|
||||
) {
|
||||
this._drawHalfPixel(data, offset, dcolor);
|
||||
} else if (bits & 0x28) {
|
||||
@ -733,11 +785,13 @@ export class HiresPage2D implements HiresPage {
|
||||
b0 = col > 0 ? this._buffer[0][base - 1] : 0;
|
||||
b2 = col < 39 ? this._buffer[0][base + 1] : 0;
|
||||
val |= (b2 & 0x3) << 7;
|
||||
let v0 = b0 & 0x20, v1 = b0 & 0x40, v2 = val & 0x1,
|
||||
let v0 = b0 & 0x20,
|
||||
v1 = b0 & 0x40,
|
||||
v2 = val & 0x1,
|
||||
odd = !(col & 0x1),
|
||||
color;
|
||||
const oddCol = (hbs ? orangeCol : greenCol);
|
||||
const evenCol = (hbs ? blueCol : violetCol);
|
||||
const oddCol = hbs ? orangeCol : greenCol;
|
||||
const evenCol = hbs ? blueCol : violetCol;
|
||||
|
||||
let offset = dx * 4 + dy * 280 * 4;
|
||||
|
||||
@ -785,7 +839,8 @@ export class HiresPage2D implements HiresPage {
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.highColorHGRMode = !this.vm.an3State && this.vm.hiresMode && !this.vm._80colMode;
|
||||
this.highColorHGRMode =
|
||||
!this.vm.an3State && this.vm.hiresMode && !this.vm._80colMode;
|
||||
this.oneSixtyMode = this.vm.flag === 1 && this.vm.doubleHiresMode;
|
||||
this.mixedDHRMode = this.vm.flag === 2 && this.vm.doubleHiresMode;
|
||||
this.monoDHRMode = this.vm.flag === 3 && this.vm.doubleHiresMode;
|
||||
@ -824,7 +879,7 @@ export class HiresPage2D implements HiresPage {
|
||||
buffer: [
|
||||
new Uint8Array(this._buffer[0]),
|
||||
new Uint8Array(this._buffer[1]),
|
||||
]
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@ -884,7 +939,8 @@ export class VideoModes2D implements VideoModes {
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
this.doubleHiresMode = !this.an3State && this.hiresMode && this._80colMode;
|
||||
this.doubleHiresMode =
|
||||
!this.an3State && this.hiresMode && this._80colMode;
|
||||
|
||||
this._refreshFlag = true;
|
||||
}
|
||||
@ -936,7 +992,9 @@ export class VideoModes2D implements VideoModes {
|
||||
}
|
||||
|
||||
_80col(on: boolean) {
|
||||
if (!this.e) { return; }
|
||||
if (!this.e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const old = this._80colMode;
|
||||
this._80colMode = on;
|
||||
@ -947,7 +1005,9 @@ export class VideoModes2D implements VideoModes {
|
||||
}
|
||||
|
||||
altChar(on: boolean) {
|
||||
if (!this.e) { return; }
|
||||
if (!this.e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const old = this.altCharMode;
|
||||
this.altCharMode = on;
|
||||
@ -969,13 +1029,16 @@ export class VideoModes2D implements VideoModes {
|
||||
}
|
||||
|
||||
an3(on: boolean) {
|
||||
if (!this.e) { return; }
|
||||
if (!this.e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const old = this.an3State;
|
||||
this.an3State = on;
|
||||
|
||||
if (on) {
|
||||
this.flag = ((this.flag << 1) | (this._80colMode ? 0x0 : 0x1)) & 0x3;
|
||||
this.flag =
|
||||
((this.flag << 1) | (this._80colMode ? 0x0 : 0x1)) & 0x3;
|
||||
}
|
||||
|
||||
if (old !== on) {
|
||||
@ -1056,8 +1119,14 @@ export class VideoModes2D implements VideoModes {
|
||||
const imageData = this.buildScreen(mainData, mixData);
|
||||
this._screenContext.drawImage(
|
||||
imageData,
|
||||
0, 0, 560, 192,
|
||||
this._left, this._top, 560, 384
|
||||
0,
|
||||
0,
|
||||
560,
|
||||
192,
|
||||
this._left,
|
||||
this._top,
|
||||
560,
|
||||
384
|
||||
);
|
||||
blitted = true;
|
||||
}
|
||||
@ -1076,10 +1145,12 @@ export class VideoModes2D implements VideoModes {
|
||||
}
|
||||
|
||||
if (altData) {
|
||||
blitted = this.updateImage(
|
||||
altData,
|
||||
{ top: 0, left: 0, right: 560, bottom: 192 }
|
||||
);
|
||||
blitted = this.updateImage(altData, {
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 560,
|
||||
bottom: 192,
|
||||
});
|
||||
} else if (this.hiresMode && !this.textMode) {
|
||||
blitted = this.updateImage(
|
||||
hgr.imageData,
|
||||
@ -1088,9 +1159,7 @@ export class VideoModes2D implements VideoModes {
|
||||
this.mixedMode ? gr.dirty : null
|
||||
);
|
||||
} else {
|
||||
blitted = this.updateImage(
|
||||
gr.imageData, gr.dirty
|
||||
);
|
||||
blitted = this.updateImage(gr.imageData, gr.dirty);
|
||||
}
|
||||
hgr.dirty = { ...notDirty };
|
||||
gr.dirty = { ...notDirty };
|
||||
@ -1109,7 +1178,7 @@ export class VideoModes2D implements VideoModes {
|
||||
_80colMode: this._80colMode,
|
||||
altCharMode: this.altCharMode,
|
||||
an3State: this.an3State,
|
||||
flag: this.flag
|
||||
flag: this.flag,
|
||||
};
|
||||
}
|
||||
|
||||
|
280
js/cards/cffa.ts
280
js/cards/cffa.ts
@ -1,7 +1,11 @@
|
||||
import type { byte, Card, Restorable } from '../types';
|
||||
import { debug, toHex } from '../util';
|
||||
import { rom as readOnlyRom } from '../roms/cards/cffa';
|
||||
import { create2MGFromBlockDisk, HeaderData, read2MGHeader } from '../formats/2mg';
|
||||
import {
|
||||
create2MGFromBlockDisk,
|
||||
HeaderData,
|
||||
read2MGHeader,
|
||||
} from '../formats/2mg';
|
||||
import { ProDOSVolume } from '../formats/prodos';
|
||||
import createBlockDisk from '../formats/block';
|
||||
import {
|
||||
@ -15,9 +19,9 @@ import {
|
||||
const rom = new Uint8Array(readOnlyRom);
|
||||
|
||||
const COMMANDS = {
|
||||
ATACRead: 0x20,
|
||||
ATACWrite: 0x30,
|
||||
ATAIdentify: 0xEC
|
||||
ATACRead: 0x20,
|
||||
ATACWrite: 0x30,
|
||||
ATAIdentify: 0xec,
|
||||
};
|
||||
|
||||
// CFFA Card Settings
|
||||
@ -25,68 +29,69 @@ const COMMANDS = {
|
||||
const SETTINGS = {
|
||||
Max32MBPartitionsDev0: 0x800,
|
||||
Max32MBPartitionsDev1: 0x801,
|
||||
DefaultBootDevice: 0x802,
|
||||
DefaultBootPartition: 0x803,
|
||||
Reserved: 0x804, // 4 bytes
|
||||
WriteProtectBits: 0x808,
|
||||
MenuSnagMask: 0x809,
|
||||
MenuSnagKey: 0x80A,
|
||||
BootTimeDelayTenths: 0x80B,
|
||||
BusResetSeconds: 0x80C,
|
||||
CheckDeviceTenths: 0x80D,
|
||||
ConfigOptionBits: 0x80E,
|
||||
BlockOffsetDev0: 0x80F, // 3 bytes
|
||||
BlockOffsetDev1: 0x812, // 3 bytes
|
||||
Unused: 0x815
|
||||
DefaultBootDevice: 0x802,
|
||||
DefaultBootPartition: 0x803,
|
||||
Reserved: 0x804, // 4 bytes
|
||||
WriteProtectBits: 0x808,
|
||||
MenuSnagMask: 0x809,
|
||||
MenuSnagKey: 0x80a,
|
||||
BootTimeDelayTenths: 0x80b,
|
||||
BusResetSeconds: 0x80c,
|
||||
CheckDeviceTenths: 0x80d,
|
||||
ConfigOptionBits: 0x80e,
|
||||
BlockOffsetDev0: 0x80f, // 3 bytes
|
||||
BlockOffsetDev1: 0x812, // 3 bytes
|
||||
Unused: 0x815,
|
||||
};
|
||||
|
||||
// CFFA ATA Register Locations
|
||||
|
||||
const LOC = {
|
||||
ATADataHigh: 0x80,
|
||||
SetCSMask: 0x81,
|
||||
ClearCSMask: 0x82,
|
||||
WriteEEPROM: 0x83,
|
||||
ATADataHigh: 0x80,
|
||||
SetCSMask: 0x81,
|
||||
ClearCSMask: 0x82,
|
||||
WriteEEPROM: 0x83,
|
||||
NoWriteEEPROM: 0x84,
|
||||
ATADevCtrl: 0x86,
|
||||
ATAAltStatus: 0x86,
|
||||
ATADataLow: 0x88,
|
||||
AError: 0x89,
|
||||
ASectorCnt: 0x8a,
|
||||
ASector: 0x8b,
|
||||
ATACylinder: 0x8c,
|
||||
ATACylinderH: 0x8d,
|
||||
ATAHead: 0x8e,
|
||||
ATACommand: 0x8f,
|
||||
ATAStatus: 0x8f
|
||||
ATADevCtrl: 0x86,
|
||||
ATAAltStatus: 0x86,
|
||||
ATADataLow: 0x88,
|
||||
AError: 0x89,
|
||||
ASectorCnt: 0x8a,
|
||||
ASector: 0x8b,
|
||||
ATACylinder: 0x8c,
|
||||
ATACylinderH: 0x8d,
|
||||
ATAHead: 0x8e,
|
||||
ATACommand: 0x8f,
|
||||
ATAStatus: 0x8f,
|
||||
};
|
||||
|
||||
// ATA Status Bits
|
||||
|
||||
const STATUS = {
|
||||
BSY: 0x80, // Busy
|
||||
BSY: 0x80, // Busy
|
||||
DRDY: 0x40, // Drive ready. 1 when ready
|
||||
DWF: 0x20, // Drive write fault. 1 when fault
|
||||
DSC: 0x10, // Disk seek complete. 1 when not seeking
|
||||
DRQ: 0x08, // Data request. 1 when ready to write
|
||||
DWF: 0x20, // Drive write fault. 1 when fault
|
||||
DSC: 0x10, // Disk seek complete. 1 when not seeking
|
||||
DRQ: 0x08, // Data request. 1 when ready to write
|
||||
CORR: 0x04, // Correct data. 1 on correctable error
|
||||
IDX: 0x02, // 1 once per revolution
|
||||
ERR: 0x01 // Error. 1 on error
|
||||
IDX: 0x02, // 1 once per revolution
|
||||
ERR: 0x01, // Error. 1 on error
|
||||
};
|
||||
|
||||
// ATA Identity Block Locations
|
||||
|
||||
const IDENTITY = {
|
||||
SectorCountLow: 58,
|
||||
SectorCountHigh: 57
|
||||
SectorCountLow: 58,
|
||||
SectorCountHigh: 57,
|
||||
};
|
||||
|
||||
export interface CFFAState {
|
||||
disks: Array<BlockDisk | null>;
|
||||
}
|
||||
|
||||
export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<CFFAState> {
|
||||
|
||||
export default class CFFA
|
||||
implements Card, MassStorage<BlockFormat>, Restorable<CFFAState>
|
||||
{
|
||||
// CFFA internal Flags
|
||||
|
||||
private _disableSignalling = false;
|
||||
@ -122,22 +127,22 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
||||
|
||||
// Disk data
|
||||
|
||||
private _partitions: Array<ProDOSVolume|null> = [
|
||||
private _partitions: Array<ProDOSVolume | null> = [
|
||||
// Drive 1
|
||||
null,
|
||||
// Drive 2
|
||||
null
|
||||
null,
|
||||
];
|
||||
|
||||
private _sectors: Uint16Array[][] = [
|
||||
// Drive 1
|
||||
[],
|
||||
// Drive 2
|
||||
[]
|
||||
[],
|
||||
];
|
||||
|
||||
private _name: string[] = [];
|
||||
private _metadata: Array<HeaderData|null> = [];
|
||||
private _metadata: Array<HeaderData | null> = [];
|
||||
|
||||
constructor() {
|
||||
debug('CFFA');
|
||||
@ -178,7 +183,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
||||
const statusArray = [];
|
||||
let flag: keyof typeof STATUS;
|
||||
for (flag in STATUS) {
|
||||
if(status & STATUS[flag]) {
|
||||
if (status & STATUS[flag]) {
|
||||
statusArray.push(flag);
|
||||
}
|
||||
}
|
||||
@ -199,7 +204,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
||||
const val = this._sectors[this._drive][sector][idx * 16 + jdx];
|
||||
row.push(toHex(val, 4));
|
||||
const low = val & 0x7f;
|
||||
const hi = val >> 8 & 0x7f;
|
||||
const hi = (val >> 8) & 0x7f;
|
||||
charRow.push(low > 0x1f ? String.fromCharCode(low) : '.');
|
||||
charRow.push(hi > 0x1f ? String.fromCharCode(hi) : '.');
|
||||
}
|
||||
@ -217,106 +222,119 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
||||
if (readMode) {
|
||||
retVal = 0;
|
||||
switch (off & 0x8f) {
|
||||
case LOC.ATADataHigh: // 0x00
|
||||
case LOC.ATADataHigh: // 0x00
|
||||
retVal = this._dataHigh;
|
||||
break;
|
||||
case LOC.SetCSMask: // 0x01
|
||||
case LOC.SetCSMask: // 0x01
|
||||
this._disableSignalling = true;
|
||||
break;
|
||||
case LOC.ClearCSMask: // 0x02
|
||||
case LOC.ClearCSMask: // 0x02
|
||||
this._disableSignalling = false;
|
||||
break;
|
||||
case LOC.WriteEEPROM: // 0x03
|
||||
case LOC.WriteEEPROM: // 0x03
|
||||
this._writeEEPROM = true;
|
||||
break;
|
||||
case LOC.NoWriteEEPROM: // 0x04
|
||||
this._writeEEPROM = false;
|
||||
break;
|
||||
case LOC.ATAAltStatus: // 0x06
|
||||
case LOC.ATAAltStatus: // 0x06
|
||||
retVal = this._altStatus;
|
||||
break;
|
||||
case LOC.ATADataLow: // 0x08
|
||||
case LOC.ATADataLow: // 0x08
|
||||
this._dataHigh = this._curSector[this._curWord] >> 8;
|
||||
retVal = this._curSector[this._curWord] & 0xff;
|
||||
if (!this._disableSignalling) {
|
||||
this._curWord++;
|
||||
}
|
||||
break;
|
||||
case LOC.AError: // 0x09
|
||||
case LOC.AError: // 0x09
|
||||
retVal = this._error;
|
||||
break;
|
||||
case LOC.ASectorCnt: // 0x0A
|
||||
case LOC.ASectorCnt: // 0x0A
|
||||
retVal = this._sectorCnt;
|
||||
break;
|
||||
case LOC.ASector: // 0x0B
|
||||
case LOC.ASector: // 0x0B
|
||||
retVal = this._sector;
|
||||
break;
|
||||
case LOC.ATACylinder: // 0x0C
|
||||
case LOC.ATACylinder: // 0x0C
|
||||
retVal = this._cylinder;
|
||||
break;
|
||||
case LOC.ATACylinderH: // 0x0D
|
||||
case LOC.ATACylinderH: // 0x0D
|
||||
retVal = this._cylinderH;
|
||||
break;
|
||||
case LOC.ATAHead: // 0x0E
|
||||
retVal = this._head | (this._lba ? 0x40 : 0) | (this._drive ? 0x10 : 0) | 0xA0;
|
||||
case LOC.ATAHead: // 0x0E
|
||||
retVal =
|
||||
this._head |
|
||||
(this._lba ? 0x40 : 0) |
|
||||
(this._drive ? 0x10 : 0) |
|
||||
0xa0;
|
||||
break;
|
||||
case LOC.ATAStatus: // 0x0F
|
||||
retVal = this._sectors[this._drive].length > 0 ? STATUS.DRDY | STATUS.DSC : 0;
|
||||
case LOC.ATAStatus: // 0x0F
|
||||
retVal =
|
||||
this._sectors[this._drive].length > 0
|
||||
? STATUS.DRDY | STATUS.DSC
|
||||
: 0;
|
||||
this._debug('returning status', this._statusString(retVal));
|
||||
break;
|
||||
default:
|
||||
debug('read unknown soft switch', toHex(off));
|
||||
}
|
||||
|
||||
if (off & 0x7) { // Anything but data high/low
|
||||
if (off & 0x7) {
|
||||
// Anything but data high/low
|
||||
this._debug('read soft switch', toHex(off), toHex(retVal));
|
||||
}
|
||||
} else {
|
||||
if (off & 0x7) { // Anything but data high/low
|
||||
if (off & 0x7) {
|
||||
// Anything but data high/low
|
||||
this._debug('write soft switch', toHex(off), toHex(val));
|
||||
}
|
||||
|
||||
switch (off & 0x8f) {
|
||||
case LOC.ATADataHigh: // 0x00
|
||||
case LOC.ATADataHigh: // 0x00
|
||||
this._dataHigh = val;
|
||||
break;
|
||||
case LOC.SetCSMask: // 0x01
|
||||
case LOC.SetCSMask: // 0x01
|
||||
this._disableSignalling = true;
|
||||
break;
|
||||
case LOC.ClearCSMask: // 0x02
|
||||
case LOC.ClearCSMask: // 0x02
|
||||
this._disableSignalling = false;
|
||||
break;
|
||||
case LOC.WriteEEPROM: // 0x03
|
||||
case LOC.WriteEEPROM: // 0x03
|
||||
this._writeEEPROM = true;
|
||||
break;
|
||||
case LOC.NoWriteEEPROM: // 0x04
|
||||
this._writeEEPROM = false;
|
||||
break;
|
||||
case LOC.ATADevCtrl: // 0x06
|
||||
case LOC.ATADevCtrl: // 0x06
|
||||
this._debug('devCtrl:', toHex(val));
|
||||
this._interruptsEnabled = (val & 0x04) ? true : false;
|
||||
this._debug('Interrupts', this._interruptsEnabled ? 'enabled' : 'disabled');
|
||||
this._interruptsEnabled = val & 0x04 ? true : false;
|
||||
this._debug(
|
||||
'Interrupts',
|
||||
this._interruptsEnabled ? 'enabled' : 'disabled'
|
||||
);
|
||||
if (val & 0x02) {
|
||||
this._reset();
|
||||
}
|
||||
break;
|
||||
case LOC.ATADataLow: // 0x08
|
||||
this._curSector[this._curWord] = this._dataHigh << 8 | val;
|
||||
case LOC.ATADataLow: // 0x08
|
||||
this._curSector[this._curWord] =
|
||||
(this._dataHigh << 8) | val;
|
||||
this._curWord++;
|
||||
break;
|
||||
case LOC.ASectorCnt: // 0x0a
|
||||
case LOC.ASectorCnt: // 0x0a
|
||||
this._debug('setting sector count', val);
|
||||
this._sectorCnt = val;
|
||||
break;
|
||||
case LOC.ASector: // 0x0b
|
||||
case LOC.ASector: // 0x0b
|
||||
this._debug('setting sector', toHex(val));
|
||||
this._sector = val;
|
||||
break;
|
||||
case LOC.ATACylinder: // 0x0c
|
||||
case LOC.ATACylinder: // 0x0c
|
||||
this._debug('setting cylinder', toHex(val));
|
||||
this._cylinder = val;
|
||||
break;
|
||||
case LOC.ATACylinderH: // 0x0d
|
||||
case LOC.ATACylinderH: // 0x0d
|
||||
this._debug('setting cylinder high', toHex(val));
|
||||
this._cylinderH = val;
|
||||
break;
|
||||
@ -324,14 +342,23 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
||||
this._head = val & 0xf;
|
||||
this._lba = val & 0x40 ? true : false;
|
||||
this._drive = val & 0x10 ? 1 : 0;
|
||||
this._debug('setting head', toHex(val & 0xf), 'drive', this._drive);
|
||||
this._debug(
|
||||
'setting head',
|
||||
toHex(val & 0xf),
|
||||
'drive',
|
||||
this._drive
|
||||
);
|
||||
if (!this._lba) {
|
||||
console.error('CHS mode not supported');
|
||||
}
|
||||
break;
|
||||
case LOC.ATACommand: // 0x0f
|
||||
case LOC.ATACommand: // 0x0f
|
||||
this._debug('command:', toHex(val));
|
||||
sector = this._head << 24 | this._cylinderH << 16 | this._cylinder << 8 | this._sector;
|
||||
sector =
|
||||
(this._head << 24) |
|
||||
(this._cylinderH << 16) |
|
||||
(this._cylinder << 8) |
|
||||
this._sector;
|
||||
this._dumpSector(sector);
|
||||
|
||||
switch (val) {
|
||||
@ -341,13 +368,27 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
||||
this._curWord = 0;
|
||||
break;
|
||||
case COMMANDS.ATACRead:
|
||||
this._debug('ATA read sector', toHex(this._cylinderH), toHex(this._cylinder), toHex(this._sector), sector);
|
||||
this._curSector = this._sectors[this._drive][sector];
|
||||
this._debug(
|
||||
'ATA read sector',
|
||||
toHex(this._cylinderH),
|
||||
toHex(this._cylinder),
|
||||
toHex(this._sector),
|
||||
sector
|
||||
);
|
||||
this._curSector =
|
||||
this._sectors[this._drive][sector];
|
||||
this._curWord = 0;
|
||||
break;
|
||||
case COMMANDS.ATACWrite:
|
||||
this._debug('ATA write sector', toHex(this._cylinderH), toHex(this._cylinder), toHex(this._sector), sector);
|
||||
this._curSector = this._sectors[this._drive][sector];
|
||||
this._debug(
|
||||
'ATA write sector',
|
||||
toHex(this._cylinderH),
|
||||
toHex(this._cylinder),
|
||||
toHex(this._sector),
|
||||
sector
|
||||
);
|
||||
this._curSector =
|
||||
this._sectors[this._drive][sector];
|
||||
this._curWord = 0;
|
||||
break;
|
||||
default:
|
||||
@ -367,49 +408,45 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
||||
}
|
||||
|
||||
read(page: byte, off: byte) {
|
||||
return rom[(page - 0xc0) << 8 | off];
|
||||
return rom[((page - 0xc0) << 8) | off];
|
||||
}
|
||||
|
||||
write(page: byte, off: byte, val: byte) {
|
||||
if (this._writeEEPROM) {
|
||||
this._debug('writing', toHex(page << 8 | off), toHex(val));
|
||||
rom[(page - 0xc0) << 8 | off] - val;
|
||||
this._debug('writing', toHex((page << 8) | off), toHex(val));
|
||||
rom[((page - 0xc0) << 8) | off] - val;
|
||||
}
|
||||
}
|
||||
|
||||
getState() {
|
||||
return {
|
||||
disks: this._partitions.map(
|
||||
(partition) => {
|
||||
let result: BlockDisk | null = null;
|
||||
if (partition) {
|
||||
const disk: BlockDisk = partition.disk();
|
||||
result = {
|
||||
blocks: disk.blocks.map(
|
||||
(block) => new Uint8Array(block)
|
||||
),
|
||||
encoding: ENCODING_BLOCK,
|
||||
format: disk.format,
|
||||
readOnly: disk.readOnly,
|
||||
metadata: { ...disk.metadata },
|
||||
};
|
||||
}
|
||||
return result;
|
||||
disks: this._partitions.map((partition) => {
|
||||
let result: BlockDisk | null = null;
|
||||
if (partition) {
|
||||
const disk: BlockDisk = partition.disk();
|
||||
result = {
|
||||
blocks: disk.blocks.map(
|
||||
(block) => new Uint8Array(block)
|
||||
),
|
||||
encoding: ENCODING_BLOCK,
|
||||
format: disk.format,
|
||||
readOnly: disk.readOnly,
|
||||
metadata: { ...disk.metadata },
|
||||
};
|
||||
}
|
||||
)
|
||||
return result;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
setState(state: CFFAState) {
|
||||
state.disks.forEach(
|
||||
(disk, idx) => {
|
||||
if (disk) {
|
||||
this.setBlockVolume(idx + 1, disk);
|
||||
} else {
|
||||
this.resetBlockVolume(idx + 1);
|
||||
}
|
||||
state.disks.forEach((disk, idx) => {
|
||||
if (disk) {
|
||||
this.setBlockVolume(idx + 1, disk);
|
||||
} else {
|
||||
this.resetBlockVolume(idx + 1);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
resetBlockVolume(drive: number) {
|
||||
@ -433,12 +470,14 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
||||
drive = drive - 1;
|
||||
|
||||
// Convert 512 byte blocks into 256 word sectors
|
||||
this._sectors[drive] = disk.blocks.map(function(block) {
|
||||
this._sectors[drive] = disk.blocks.map(function (block) {
|
||||
return new Uint16Array(block.buffer);
|
||||
});
|
||||
|
||||
this._identity[drive][IDENTITY.SectorCountHigh] = this._sectors[0].length & 0xffff;
|
||||
this._identity[drive][IDENTITY.SectorCountLow] = this._sectors[0].length >> 16;
|
||||
this._identity[drive][IDENTITY.SectorCountHigh] =
|
||||
this._sectors[0].length & 0xffff;
|
||||
this._identity[drive][IDENTITY.SectorCountLow] =
|
||||
this._sectors[0].length >> 16;
|
||||
|
||||
const prodos = new ProDOSVolume(disk);
|
||||
|
||||
@ -455,7 +494,12 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
||||
|
||||
// Assign a raw disk image to a drive. Must be 2mg or raw PO image.
|
||||
|
||||
setBinary(drive: number, name: string, ext: BlockFormat, rawData: ArrayBuffer) {
|
||||
setBinary(
|
||||
drive: number,
|
||||
name: string,
|
||||
ext: BlockFormat,
|
||||
rawData: ArrayBuffer
|
||||
) {
|
||||
const volume = 254;
|
||||
const readOnly = false;
|
||||
|
||||
@ -471,7 +515,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
||||
rawData,
|
||||
name,
|
||||
volume,
|
||||
readOnly
|
||||
readOnly,
|
||||
};
|
||||
const disk = createBlockDisk(ext, options);
|
||||
|
||||
|
@ -1,23 +1,40 @@
|
||||
import { base64_encode } from '../base64';
|
||||
import type {
|
||||
byte,
|
||||
Card,
|
||||
ReadonlyUint8Array
|
||||
} from '../types';
|
||||
import type { byte, Card, ReadonlyUint8Array } from '../types';
|
||||
|
||||
import {
|
||||
DISK_PROCESSED, DriveNumber, DRIVE_NUMBERS, FloppyDisk,
|
||||
FloppyFormat, FormatWorkerMessage,
|
||||
FormatWorkerResponse, isNibbleDisk, isNoFloppyDisk, isWozDisk, JSONDisk, MassStorage,
|
||||
MassStorageData, NibbleDisk, NibbleFormat, NO_DISK, PROCESS_BINARY, PROCESS_JSON, PROCESS_JSON_DISK, SupportedSectors, WozDisk
|
||||
DISK_PROCESSED,
|
||||
DriveNumber,
|
||||
DRIVE_NUMBERS,
|
||||
FloppyDisk,
|
||||
FloppyFormat,
|
||||
FormatWorkerMessage,
|
||||
FormatWorkerResponse,
|
||||
isNibbleDisk,
|
||||
isNoFloppyDisk,
|
||||
isWozDisk,
|
||||
JSONDisk,
|
||||
MassStorage,
|
||||
MassStorageData,
|
||||
NibbleDisk,
|
||||
NibbleFormat,
|
||||
NO_DISK,
|
||||
PROCESS_BINARY,
|
||||
PROCESS_JSON,
|
||||
PROCESS_JSON_DISK,
|
||||
SupportedSectors,
|
||||
WozDisk,
|
||||
} from '../formats/types';
|
||||
|
||||
import {
|
||||
createDisk,
|
||||
createDiskFromJsonDisk
|
||||
} from '../formats/create_disk';
|
||||
import { createDisk, createDiskFromJsonDisk } from '../formats/create_disk';
|
||||
|
||||
import { jsonDecode, jsonEncode, readSector, _D13O, _DO, _PO } from '../formats/format_utils';
|
||||
import {
|
||||
jsonDecode,
|
||||
jsonEncode,
|
||||
readSector,
|
||||
_D13O,
|
||||
_DO,
|
||||
_PO,
|
||||
} from '../formats/format_utils';
|
||||
|
||||
import Apple2IO from '../apple2io';
|
||||
|
||||
@ -25,30 +42,36 @@ import { BOOTSTRAP_ROM_13, BOOTSTRAP_ROM_16 } from '../roms/cards/disk2';
|
||||
|
||||
import { EmptyDriver } from './drivers/EmptyDriver';
|
||||
import { NibbleDiskDriver } from './drivers/NibbleDiskDriver';
|
||||
import { ControllerState, DiskDriver, Drive, DriverState, Phase } from './drivers/types';
|
||||
import {
|
||||
ControllerState,
|
||||
DiskDriver,
|
||||
Drive,
|
||||
DriverState,
|
||||
Phase,
|
||||
} from './drivers/types';
|
||||
import { WozDiskDriver } from './drivers/WozDiskDriver';
|
||||
|
||||
/** Softswitch locations */
|
||||
const LOC = {
|
||||
// Disk II Controller Commands
|
||||
// See Understanding the Apple IIe, Table 9.1
|
||||
PHASE0OFF: 0x80, // Q0L: Phase 0 OFF
|
||||
PHASE0ON: 0x81, // Q0H: Phase 0 ON
|
||||
PHASE1OFF: 0x82, // Q1L: Phase 1 OFF
|
||||
PHASE1ON: 0x83, // Q1H: Phase 1 ON
|
||||
PHASE2OFF: 0x84, // Q2L: Phase 2 OFF
|
||||
PHASE2ON: 0x85, // Q2H: Phase 2 ON
|
||||
PHASE3OFF: 0x86, // Q3L: Phase 3 OFF
|
||||
PHASE3ON: 0x87, // Q3H: Phase 3 ON
|
||||
PHASE0OFF: 0x80, // Q0L: Phase 0 OFF
|
||||
PHASE0ON: 0x81, // Q0H: Phase 0 ON
|
||||
PHASE1OFF: 0x82, // Q1L: Phase 1 OFF
|
||||
PHASE1ON: 0x83, // Q1H: Phase 1 ON
|
||||
PHASE2OFF: 0x84, // Q2L: Phase 2 OFF
|
||||
PHASE2ON: 0x85, // Q2H: Phase 2 ON
|
||||
PHASE3OFF: 0x86, // Q3L: Phase 3 OFF
|
||||
PHASE3ON: 0x87, // Q3H: Phase 3 ON
|
||||
|
||||
DRIVEOFF: 0x88, // Q4L: Drives OFF
|
||||
DRIVEON: 0x89, // Q4H: Selected drive ON
|
||||
DRIVE1: 0x8A, // Q5L: Select drive 1
|
||||
DRIVE2: 0x8B, // Q5H: Select drive 2
|
||||
DRIVEREAD: 0x8C, // Q6L: Shift while writing; read data
|
||||
DRIVEWRITE: 0x8D, // Q6H: Load while writing; read write protect
|
||||
DRIVEREADMODE: 0x8E, // Q7L: Read
|
||||
DRIVEWRITEMODE: 0x8F // Q7H: Write
|
||||
DRIVEOFF: 0x88, // Q4L: Drives OFF
|
||||
DRIVEON: 0x89, // Q4H: Selected drive ON
|
||||
DRIVE1: 0x8a, // Q5L: Select drive 1
|
||||
DRIVE2: 0x8b, // Q5H: Select drive 2
|
||||
DRIVEREAD: 0x8c, // Q6L: Shift while writing; read data
|
||||
DRIVEWRITE: 0x8d, // Q6H: Load while writing; read write protect
|
||||
DRIVEREADMODE: 0x8e, // Q7L: Read
|
||||
DRIVEWRITEMODE: 0x8f, // Q7H: Write
|
||||
} as const;
|
||||
|
||||
/** Logic state sequencer ROM */
|
||||
@ -62,6 +85,7 @@ const LOC = {
|
||||
// B LOAD XXXXXXXX YYYYYYYY
|
||||
// D SL1 ABCDEFGH BCDEFGH1
|
||||
|
||||
// prettier-ignore
|
||||
const SEQUENCER_ROM_13 = [
|
||||
// See Understanding the Apple IIe, Figure 9.10 The DOS 3.2 Logic State Sequencer
|
||||
// Note that the column order here is NOT the same as in Figure 9.10 for Q7 H (Write).
|
||||
@ -88,6 +112,7 @@ const SEQUENCER_ROM_13 = [
|
||||
0xDD, 0x4D, 0xE0, 0xE0, 0x0A, 0x0A, 0x0A, 0x0A, 0x88, 0x88, 0x08, 0x08, 0x88, 0x88, 0x08, 0x08 // F
|
||||
] as const;
|
||||
|
||||
// prettier-ignore
|
||||
const SEQUENCER_ROM_16 = [
|
||||
// See Understanding the Apple IIe, Figure 9.11 The DOS 3.3 Logic State Sequencer
|
||||
// Note that the column order here is NOT the same as in Figure 9.11 for Q7 H (Write).
|
||||
@ -152,7 +177,7 @@ const PHASE_DELTA = [
|
||||
[0, 1, 2, -1],
|
||||
[-1, 0, 1, 2],
|
||||
[-2, -1, 0, 1],
|
||||
[1, -2, -1, 0]
|
||||
[1, -2, -1, 0],
|
||||
] as const;
|
||||
|
||||
/** Callbacks triggered by events of the drive or controller. */
|
||||
@ -211,7 +236,8 @@ function getDiskState(disk: FloppyDisk): FloppyDisk {
|
||||
}
|
||||
|
||||
if (isWozDisk(disk)) {
|
||||
const { format, encoding, metadata, readOnly, trackMap, rawTracks } = disk;
|
||||
const { format, encoding, metadata, readOnly, trackMap, rawTracks } =
|
||||
disk;
|
||||
const result: WozDisk = {
|
||||
format,
|
||||
encoding,
|
||||
@ -235,22 +261,23 @@ function getDiskState(disk: FloppyDisk): FloppyDisk {
|
||||
* Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller.
|
||||
*/
|
||||
export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
|
||||
private drives: Record<DriveNumber, Drive> = {
|
||||
1: { // Drive 1
|
||||
1: {
|
||||
// Drive 1
|
||||
track: 0,
|
||||
head: 0,
|
||||
phase: 0,
|
||||
readOnly: false,
|
||||
dirty: false,
|
||||
},
|
||||
2: { // Drive 2
|
||||
2: {
|
||||
// Drive 2
|
||||
track: 0,
|
||||
head: 0,
|
||||
phase: 0,
|
||||
readOnly: false,
|
||||
dirty: false,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
private disks: Record<DriveNumber, FloppyDisk> = {
|
||||
@ -263,7 +290,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
encoding: NO_DISK,
|
||||
readOnly: false,
|
||||
metadata: { name: 'Disk 2' },
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
private driver: Record<DriveNumber, DiskDriver> = {
|
||||
@ -283,7 +310,11 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
private worker: Worker;
|
||||
|
||||
/** Builds a new Disk ][ card. */
|
||||
constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors: SupportedSectors = 16) {
|
||||
constructor(
|
||||
private io: Apple2IO,
|
||||
private callbacks: Callbacks,
|
||||
private sectors: SupportedSectors = 16
|
||||
) {
|
||||
this.debug('Disk ][');
|
||||
|
||||
this.state = {
|
||||
@ -382,7 +413,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
case LOC.PHASE3OFF: // 0x06
|
||||
this.setPhase(3, false);
|
||||
break;
|
||||
case LOC.PHASE3ON: // 0x07
|
||||
case LOC.PHASE3ON: // 0x07
|
||||
this.setPhase(3, true);
|
||||
break;
|
||||
|
||||
@ -414,7 +445,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
}
|
||||
break;
|
||||
|
||||
case LOC.DRIVE1: // 0x0a
|
||||
case LOC.DRIVE1: // 0x0a
|
||||
this.debug('Disk 1');
|
||||
state.driveNo = 1;
|
||||
this.updateActiveDrive();
|
||||
@ -423,7 +454,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
this.callbacks.driveLight(1, true);
|
||||
}
|
||||
break;
|
||||
case LOC.DRIVE2: // 0x0b
|
||||
case LOC.DRIVE2: // 0x0b
|
||||
this.debug('Disk 2');
|
||||
state.driveNo = 2;
|
||||
this.updateActiveDrive();
|
||||
@ -443,7 +474,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
this.curDriver.onQ6High(readMode);
|
||||
break;
|
||||
|
||||
case LOC.DRIVEREADMODE: // 0x0e (Q7L)
|
||||
case LOC.DRIVEREADMODE: // 0x0e (Q7L)
|
||||
this.debug('Read Mode');
|
||||
state.q7 = false;
|
||||
break;
|
||||
@ -551,7 +582,6 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
this.driver[driveNo].setState(state.driver);
|
||||
}
|
||||
|
||||
|
||||
setState(state: State) {
|
||||
this.state = { ...state.controllerState };
|
||||
for (const d of DRIVE_NUMBERS) {
|
||||
@ -580,7 +610,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
rwts(driveNo: DriveNumber, track: byte, sector: byte) {
|
||||
const curDisk = this.disks[driveNo];
|
||||
if (!isNibbleDisk(curDisk)) {
|
||||
throw new Error('Can\'t read WOZ disks');
|
||||
throw new Error("Can't read WOZ disks");
|
||||
}
|
||||
return readSector(curDisk, track, sector);
|
||||
}
|
||||
@ -593,7 +623,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
type: PROCESS_JSON_DISK,
|
||||
payload: {
|
||||
driveNo: driveNo,
|
||||
jsonDisk
|
||||
jsonDisk,
|
||||
},
|
||||
};
|
||||
this.worker.postMessage(message);
|
||||
@ -611,7 +641,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
getJSON(driveNo: DriveNumber, pretty: boolean = false) {
|
||||
const curDisk = this.disks[driveNo];
|
||||
if (!isNibbleDisk(curDisk)) {
|
||||
throw new Error('Can\'t save WOZ disks to JSON');
|
||||
throw new Error("Can't save WOZ disks to JSON");
|
||||
}
|
||||
return jsonEncode(curDisk, pretty);
|
||||
}
|
||||
@ -622,7 +652,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
type: PROCESS_JSON,
|
||||
payload: {
|
||||
driveNo: driveNo,
|
||||
json
|
||||
json,
|
||||
},
|
||||
};
|
||||
this.worker.postMessage(message);
|
||||
@ -633,7 +663,12 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
return true;
|
||||
}
|
||||
|
||||
setBinary(driveNo: DriveNumber, name: string, fmt: FloppyFormat, rawData: ArrayBuffer) {
|
||||
setBinary(
|
||||
driveNo: DriveNumber,
|
||||
name: string,
|
||||
fmt: FloppyFormat,
|
||||
rawData: ArrayBuffer
|
||||
) {
|
||||
const readOnly = false;
|
||||
const volume = 254;
|
||||
const options = {
|
||||
@ -650,7 +685,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
driveNo: driveNo,
|
||||
fmt,
|
||||
options,
|
||||
}
|
||||
},
|
||||
};
|
||||
this.worker.postMessage(message);
|
||||
|
||||
@ -673,19 +708,22 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
try {
|
||||
this.worker = new Worker('dist/format_worker.bundle.js');
|
||||
|
||||
this.worker.addEventListener('message', (message: MessageEvent<FormatWorkerResponse>) => {
|
||||
const { data } = message;
|
||||
switch (data.type) {
|
||||
case DISK_PROCESSED:
|
||||
{
|
||||
const { driveNo: drive, disk } = data.payload;
|
||||
if (disk) {
|
||||
this.insertDisk(drive, disk);
|
||||
this.worker.addEventListener(
|
||||
'message',
|
||||
(message: MessageEvent<FormatWorkerResponse>) => {
|
||||
const { data } = message;
|
||||
switch (data.type) {
|
||||
case DISK_PROCESSED:
|
||||
{
|
||||
const { driveNo: drive, disk } = data.payload;
|
||||
if (disk) {
|
||||
this.insertDisk(drive, disk);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
console.error(e);
|
||||
}
|
||||
@ -696,22 +734,22 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
if (isNoFloppyDisk(disk)) {
|
||||
this.driver[driveNo] = new EmptyDriver(this.drives[driveNo]);
|
||||
} else if (isNibbleDisk(disk)) {
|
||||
this.driver[driveNo] =
|
||||
new NibbleDiskDriver(
|
||||
driveNo,
|
||||
this.drives[driveNo],
|
||||
disk,
|
||||
this.state,
|
||||
() => this.updateDirty(driveNo, true));
|
||||
this.driver[driveNo] = new NibbleDiskDriver(
|
||||
driveNo,
|
||||
this.drives[driveNo],
|
||||
disk,
|
||||
this.state,
|
||||
() => this.updateDirty(driveNo, true)
|
||||
);
|
||||
} else if (isWozDisk(disk)) {
|
||||
this.driver[driveNo] =
|
||||
new WozDiskDriver(
|
||||
driveNo,
|
||||
this.drives[driveNo],
|
||||
disk,
|
||||
this.state,
|
||||
() => this.updateDirty(driveNo, true),
|
||||
this.io);
|
||||
this.driver[driveNo] = new WozDiskDriver(
|
||||
driveNo,
|
||||
this.drives[driveNo],
|
||||
disk,
|
||||
this.state,
|
||||
() => this.updateDirty(driveNo, true),
|
||||
this.io
|
||||
);
|
||||
} else {
|
||||
throw new Error(`Unknown disk format ${disk.encoding}`);
|
||||
}
|
||||
@ -736,16 +774,20 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
* an error will be thrown. Using `ext == 'nib'` will always return
|
||||
* an image.
|
||||
*/
|
||||
getBinary(driveNo: DriveNumber, ext?: Exclude<NibbleFormat, 'woz'>): MassStorageData | null {
|
||||
getBinary(
|
||||
driveNo: DriveNumber,
|
||||
ext?: Exclude<NibbleFormat, 'woz'>
|
||||
): MassStorageData | null {
|
||||
const curDisk = this.disks[driveNo];
|
||||
if (!isNibbleDisk(curDisk)) {
|
||||
return null;
|
||||
}
|
||||
const { format, readOnly, tracks, volume } = curDisk;
|
||||
const { name } = curDisk.metadata;
|
||||
const len = format === 'nib' ?
|
||||
tracks.reduce((acc, track) => acc + track.length, 0) :
|
||||
this.sectors * tracks.length * 256;
|
||||
const len =
|
||||
format === 'nib'
|
||||
? tracks.reduce((acc, track) => acc + track.length, 0)
|
||||
: this.sectors * tracks.length * 256;
|
||||
const data = new Uint8Array(len);
|
||||
|
||||
ext = ext ?? format;
|
||||
@ -767,7 +809,11 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
|
||||
for (let s = 0; s < maxSector; s++) {
|
||||
const _s = sectorMap[s];
|
||||
const sector = readSector({ ...curDisk, format: ext }, t, _s);
|
||||
const sector = readSector(
|
||||
{ ...curDisk, format: ext },
|
||||
t,
|
||||
_s
|
||||
);
|
||||
data.set(sector, idx);
|
||||
idx += sector.length;
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { DriveNumber, NibbleDisk, WozDisk } from '../../formats/types';
|
||||
import { ControllerState, DiskDriver, Drive, DriverState } from './types';
|
||||
|
||||
|
||||
/**
|
||||
* Common logic for both `NibbleDiskDriver` and `WozDiskDriver`.
|
||||
*/
|
||||
@ -10,7 +9,8 @@ export abstract class BaseDiskDriver implements DiskDriver {
|
||||
protected readonly driveNo: DriveNumber,
|
||||
protected readonly drive: Drive,
|
||||
protected readonly disk: NibbleDisk | WozDisk,
|
||||
protected readonly controller: ControllerState) { }
|
||||
protected readonly controller: ControllerState
|
||||
) {}
|
||||
|
||||
/** Called frequently to ensure the disk is spinning. */
|
||||
abstract tick(): void;
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { DiskDriver, Drive, DriverState } from './types';
|
||||
|
||||
/** Returned state for an empty drive. */
|
||||
export interface EmptyDriverState extends DriverState { }
|
||||
export interface EmptyDriverState extends DriverState {}
|
||||
|
||||
/**
|
||||
* Driver for empty drives. This implementation does nothing except keep
|
||||
* the head clamped between tracks 0 and 34.
|
||||
*/
|
||||
export class EmptyDriver implements DiskDriver {
|
||||
constructor(private readonly drive: Drive) { }
|
||||
constructor(private readonly drive: Drive) {}
|
||||
|
||||
tick(): void {
|
||||
// do nothing
|
||||
|
@ -22,7 +22,8 @@ export class NibbleDiskDriver extends BaseDiskDriver {
|
||||
drive: Drive,
|
||||
readonly disk: NibbleDisk,
|
||||
controller: ControllerState,
|
||||
private readonly onDirty: () => void) {
|
||||
private readonly onDirty: () => void
|
||||
) {
|
||||
super(driveNo, drive, disk, controller);
|
||||
}
|
||||
|
||||
@ -57,7 +58,7 @@ export class NibbleDiskDriver extends BaseDiskDriver {
|
||||
} else {
|
||||
this.controller.latch = 0;
|
||||
}
|
||||
this.skip = (++this.skip % 2);
|
||||
this.skip = ++this.skip % 2;
|
||||
}
|
||||
|
||||
onQ6High(readMode: boolean): void {
|
||||
|
@ -3,7 +3,13 @@ import { DriveNumber, WozDisk } from '../../formats/types';
|
||||
import { toHex } from '../../util';
|
||||
import { SEQUENCER_ROM } from '../disk2';
|
||||
import { BaseDiskDriver } from './BaseDiskDriver';
|
||||
import { ControllerState, Drive, DriverState, LssClockCycle, LssState } from './types';
|
||||
import {
|
||||
ControllerState,
|
||||
Drive,
|
||||
DriverState,
|
||||
LssClockCycle,
|
||||
LssState,
|
||||
} from './types';
|
||||
|
||||
interface WozDiskDriverState extends DriverState {
|
||||
clock: LssClockCycle;
|
||||
@ -32,7 +38,8 @@ export class WozDiskDriver extends BaseDiskDriver {
|
||||
readonly disk: WozDisk,
|
||||
controller: ControllerState,
|
||||
private readonly onDirty: () => void,
|
||||
private readonly io: Apple2IO) {
|
||||
private readonly io: Apple2IO
|
||||
) {
|
||||
super(driveNo, drive, disk, controller);
|
||||
|
||||
// From the example in UtA2e, p. 9-29, col. 1, para. 1., this is
|
||||
@ -129,7 +136,13 @@ export class WozDiskDriver extends BaseDiskDriver {
|
||||
|
||||
const command = SEQUENCER_ROM[controller.sectors][idx];
|
||||
|
||||
this.debug(`clock: ${this.clock} state: ${toHex(this.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${controller.q6} latch: ${toHex(controller.latch)}`);
|
||||
this.debug(
|
||||
`clock: ${this.clock} state: ${toHex(
|
||||
this.state
|
||||
)} pulse: ${pulse} command: ${toHex(command)} q6: ${
|
||||
controller.q6
|
||||
} latch: ${toHex(controller.latch)}`
|
||||
);
|
||||
|
||||
switch (command & 0xf) {
|
||||
case 0x0: // CLR
|
||||
@ -140,23 +153,23 @@ export class WozDiskDriver extends BaseDiskDriver {
|
||||
case 0x9: // SL0
|
||||
controller.latch = (controller.latch << 1) & 0xff;
|
||||
break;
|
||||
case 0xA: // SR
|
||||
case 0xa: // SR
|
||||
controller.latch >>= 1;
|
||||
if (this.isWriteProtected()) {
|
||||
controller.latch |= 0x80;
|
||||
}
|
||||
break;
|
||||
case 0xB: // LD
|
||||
case 0xb: // LD
|
||||
controller.latch = controller.bus;
|
||||
this.debug('Loading', toHex(controller.latch), 'from bus');
|
||||
break;
|
||||
case 0xD: // SL1
|
||||
case 0xd: // SL1
|
||||
controller.latch = ((controller.latch << 1) | 0x01) & 0xff;
|
||||
break;
|
||||
default:
|
||||
this.debug(`unknown command: ${toHex(command & 0xf)}`);
|
||||
}
|
||||
this.state = (command >> 4 & 0xF) as LssState;
|
||||
this.state = ((command >> 4) & 0xf) as LssState;
|
||||
|
||||
if (this.clock === 4) {
|
||||
if (this.isOn()) {
|
||||
|
@ -13,7 +13,9 @@ export interface LanguageCardState {
|
||||
prewrite: boolean;
|
||||
}
|
||||
|
||||
export default class LanguageCard implements Card, Restorable<LanguageCardState> {
|
||||
export default class LanguageCard
|
||||
implements Card, Restorable<LanguageCardState>
|
||||
{
|
||||
private bank1: RAM;
|
||||
private bank2: RAM;
|
||||
private ram: RAM;
|
||||
@ -88,7 +90,8 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
|
||||
let bankStr;
|
||||
let rwStr;
|
||||
|
||||
if (writeSwitch) { // 0xC081, 0xC083
|
||||
if (writeSwitch) {
|
||||
// 0xC081, 0xC083
|
||||
if (readMode) {
|
||||
if (this._prewrite) {
|
||||
this._writebsr = true;
|
||||
@ -96,30 +99,37 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
|
||||
}
|
||||
this._prewrite = readMode;
|
||||
|
||||
if (offSwitch) { // $C083, $C08B
|
||||
if (offSwitch) {
|
||||
// $C083, $C08B
|
||||
this._readbsr = true;
|
||||
rwStr = 'Read/Write';
|
||||
} else { // $C081, $C089
|
||||
} else {
|
||||
// $C081, $C089
|
||||
this._readbsr = false;
|
||||
rwStr = 'Write';
|
||||
}
|
||||
} else { // $C080, $C082, $C088, $C08A
|
||||
} else {
|
||||
// $C080, $C082, $C088, $C08A
|
||||
this._writebsr = false;
|
||||
this._prewrite = false;
|
||||
|
||||
if (offSwitch) { // $C082, $C08A
|
||||
if (offSwitch) {
|
||||
// $C082, $C08A
|
||||
this._readbsr = false;
|
||||
rwStr = 'Off';
|
||||
} else { // $C080, $C088
|
||||
} else {
|
||||
// $C080, $C088
|
||||
this._readbsr = true;
|
||||
rwStr = 'Read';
|
||||
}
|
||||
}
|
||||
|
||||
if (bank1Switch) { // C08[8-C]
|
||||
if (bank1Switch) {
|
||||
// C08[8-C]
|
||||
this._bsr2 = false;
|
||||
bankStr = 'Bank 1';
|
||||
} else { // C08[0-3]
|
||||
} else {
|
||||
// C08[0-3]
|
||||
this._bsr2 = true;
|
||||
bankStr = 'Bank 2';
|
||||
}
|
||||
@ -180,7 +190,7 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
|
||||
prewrite: this._prewrite,
|
||||
ram: this.ram.getState(),
|
||||
bank1: this.bank1.getState(),
|
||||
bank2: this.bank2.getState()
|
||||
bank2: this.bank2.getState(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -4,16 +4,16 @@ import { debug } from '../util';
|
||||
import { rom } from '../roms/cards/mouse';
|
||||
|
||||
const CLAMP_MIN_LOW = 0x478;
|
||||
const CLAMP_MAX_LOW = 0x4F8;
|
||||
const CLAMP_MAX_LOW = 0x4f8;
|
||||
const CLAMP_MIN_HIGH = 0x578;
|
||||
const CLAMP_MAX_HIGH = 0x5F8;
|
||||
const CLAMP_MAX_HIGH = 0x5f8;
|
||||
|
||||
const X_LOW = 0x478;
|
||||
const Y_LOW = 0x4F8;
|
||||
const Y_LOW = 0x4f8;
|
||||
const X_HIGH = 0x578;
|
||||
const Y_HIGH = 0x5F8;
|
||||
const Y_HIGH = 0x5f8;
|
||||
const STATUS = 0x778;
|
||||
const MODE = 0x7F8;
|
||||
const MODE = 0x7f8;
|
||||
|
||||
const STATUS_DOWN = 0x80;
|
||||
const STATUS_LAST = 0x40;
|
||||
@ -38,7 +38,7 @@ const ENTRIES = {
|
||||
POS_MOUSE: 0x16,
|
||||
CLAMP_MOUSE: 0x17,
|
||||
HOME_MOUSE: 0x18,
|
||||
INIT_MOUSE: 0x19
|
||||
INIT_MOUSE: 0x19,
|
||||
};
|
||||
|
||||
interface MouseState {
|
||||
@ -65,9 +65,9 @@ export default class Mouse implements Card, Restorable<MouseState> {
|
||||
/** Lowest mouse Y */
|
||||
private clampYMin: word = 0;
|
||||
/** Highest mouse X */
|
||||
private clampXMax: word = 0x3FF;
|
||||
private clampXMax: word = 0x3ff;
|
||||
/** Highest mouse Y */
|
||||
private clampYMax: word = 0x3FF;
|
||||
private clampYMax: word = 0x3ff;
|
||||
/** Mouse X position */
|
||||
private x: word = 0;
|
||||
/** Mouse Y position */
|
||||
@ -117,7 +117,7 @@ export default class Mouse implements Card, Restorable<MouseState> {
|
||||
};
|
||||
|
||||
const clearCarry = (state: CpuState) => {
|
||||
state.s &= 0xFE;
|
||||
state.s &= 0xfe;
|
||||
return state;
|
||||
};
|
||||
|
||||
@ -145,7 +145,8 @@ export default class Mouse implements Card, Restorable<MouseState> {
|
||||
break;
|
||||
case rom[ENTRIES.READ_MOUSE]:
|
||||
{
|
||||
const moved = (this.lastX !== this.x) || (this.lastY !== this.y);
|
||||
const moved =
|
||||
this.lastX !== this.x || this.lastY !== this.y;
|
||||
const status =
|
||||
(this.down ? STATUS_DOWN : 0) |
|
||||
(this.lastDown ? STATUS_LAST : 0) |
|
||||
@ -183,13 +184,29 @@ export default class Mouse implements Card, Restorable<MouseState> {
|
||||
{
|
||||
const clampY = state.a;
|
||||
if (clampY) {
|
||||
this.clampYMin = holeRead(CLAMP_MIN_LOW) | (holeRead(CLAMP_MIN_HIGH) << 8);
|
||||
this.clampYMax = holeRead(CLAMP_MAX_LOW) | (holeRead(CLAMP_MAX_HIGH) << 8);
|
||||
debug('clampMouse Y', this.clampYMin, this.clampYMax);
|
||||
this.clampYMin =
|
||||
holeRead(CLAMP_MIN_LOW) |
|
||||
(holeRead(CLAMP_MIN_HIGH) << 8);
|
||||
this.clampYMax =
|
||||
holeRead(CLAMP_MAX_LOW) |
|
||||
(holeRead(CLAMP_MAX_HIGH) << 8);
|
||||
debug(
|
||||
'clampMouse Y',
|
||||
this.clampYMin,
|
||||
this.clampYMax
|
||||
);
|
||||
} else {
|
||||
this.clampXMin = holeRead(CLAMP_MIN_LOW) | (holeRead(CLAMP_MIN_HIGH) << 8);
|
||||
this.clampXMax = holeRead(CLAMP_MAX_LOW) | (holeRead(CLAMP_MAX_HIGH) << 8);
|
||||
debug('clampMouse X', this.clampXMin, this.clampXMax);
|
||||
this.clampXMin =
|
||||
holeRead(CLAMP_MIN_LOW) |
|
||||
(holeRead(CLAMP_MIN_HIGH) << 8);
|
||||
this.clampXMax =
|
||||
holeRead(CLAMP_MAX_LOW) |
|
||||
(holeRead(CLAMP_MAX_HIGH) << 8);
|
||||
debug(
|
||||
'clampMouse X',
|
||||
this.clampXMin,
|
||||
this.clampXMax
|
||||
);
|
||||
}
|
||||
state = clearCarry(state);
|
||||
}
|
||||
@ -229,10 +246,10 @@ export default class Mouse implements Card, Restorable<MouseState> {
|
||||
if (this.mode & MODE_INT_VBL) {
|
||||
this.serve |= INT_SCREEN;
|
||||
}
|
||||
if ((this.mode & MODE_INT_PRESS) && this.shouldIntPress) {
|
||||
if (this.mode & MODE_INT_PRESS && this.shouldIntPress) {
|
||||
this.serve |= INT_PRESS;
|
||||
}
|
||||
if ((this.mode & MODE_INT_MOVE) && this.shouldIntMove) {
|
||||
if (this.mode & MODE_INT_MOVE && this.shouldIntMove) {
|
||||
this.serve |= INT_MOVE;
|
||||
}
|
||||
if (this.serve) {
|
||||
@ -255,8 +272,8 @@ export default class Mouse implements Card, Restorable<MouseState> {
|
||||
setMouseXY(x: number, y: number, w: number, h: number) {
|
||||
const rangeX = this.clampXMax - this.clampXMin;
|
||||
const rangeY = this.clampYMax - this.clampYMin;
|
||||
this.x = (x * rangeX / w + this.clampXMin) & 0xffff;
|
||||
this.y = (y * rangeY / h + this.clampYMin) & 0xffff;
|
||||
this.x = ((x * rangeX) / w + this.clampXMin) & 0xffff;
|
||||
this.y = ((y * rangeY) / h + this.clampYMin) & 0xffff;
|
||||
this.shouldIntMove = true;
|
||||
}
|
||||
|
||||
@ -318,7 +335,7 @@ export default class Mouse implements Card, Restorable<MouseState> {
|
||||
serve: this.serve,
|
||||
shouldIntMove: this.shouldIntMove,
|
||||
shouldIntPress: this.shouldIntPress,
|
||||
slot: this.slot
|
||||
slot: this.slot,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,7 @@ import ROM from 'js/roms/rom';
|
||||
import { bit, byte } from 'js/types';
|
||||
import { debug } from '../util';
|
||||
|
||||
const PATTERN = [
|
||||
0xC5, 0x3A, 0xA3, 0x5C, 0xC5, 0x3A, 0xA3, 0x5C
|
||||
];
|
||||
const PATTERN = [0xc5, 0x3a, 0xa3, 0x5c, 0xc5, 0x3a, 0xa3, 0x5c];
|
||||
|
||||
const A0 = 0x01;
|
||||
const A2 = 0x04;
|
||||
@ -18,7 +16,6 @@ export default class NoSlotClock {
|
||||
debug('NoSlotClock');
|
||||
}
|
||||
|
||||
|
||||
private patternMatch() {
|
||||
for (let idx = 0; idx < 8; idx++) {
|
||||
let byte = 0;
|
||||
@ -53,7 +50,7 @@ export default class NoSlotClock {
|
||||
const hour = now.getHours();
|
||||
const minutes = now.getMinutes();
|
||||
const seconds = now.getSeconds();
|
||||
const hundredths = (now.getMilliseconds() / 10);
|
||||
const hundredths = now.getMilliseconds() / 10;
|
||||
|
||||
this.bits = [];
|
||||
|
||||
@ -113,4 +110,3 @@ export default class NoSlotClock {
|
||||
// Setting the state makes no sense.
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { Card, Restorable, byte } from '../types';
|
||||
import { rom } from '../roms/cards/parallel';
|
||||
|
||||
const LOC = {
|
||||
IOREG: 0x80
|
||||
IOREG: 0x80,
|
||||
} as const;
|
||||
|
||||
export interface ParallelState {}
|
||||
|
@ -12,7 +12,7 @@ const LOC = {
|
||||
_RAMMID: 0x85,
|
||||
_RAMHI: 0x86,
|
||||
_RAMDATA: 0x87,
|
||||
BANK: 0x8F
|
||||
BANK: 0x8f,
|
||||
} as const;
|
||||
|
||||
export class RAMFactorState {
|
||||
@ -41,21 +41,21 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
|
||||
}
|
||||
|
||||
private sethi(val: byte) {
|
||||
this.ramhi = (val & 0xff);
|
||||
this.ramhi = val & 0xff;
|
||||
}
|
||||
|
||||
private setmid(val: byte) {
|
||||
if (((this.rammid & 0x80) !== 0) && ((val & 0x80) === 0)) {
|
||||
if ((this.rammid & 0x80) !== 0 && (val & 0x80) === 0) {
|
||||
this.sethi(this.ramhi + 1);
|
||||
}
|
||||
this.rammid = (val & 0xff);
|
||||
this.rammid = val & 0xff;
|
||||
}
|
||||
|
||||
private setlo(val: byte) {
|
||||
if (((this.ramlo & 0x80) !== 0) && ((val & 0x80) === 0)) {
|
||||
if ((this.ramlo & 0x80) !== 0 && (val & 0x80) === 0) {
|
||||
this.setmid(this.rammid + 1);
|
||||
}
|
||||
this.ramlo = (val & 0xff);
|
||||
this.ramlo = val & 0xff;
|
||||
}
|
||||
|
||||
private access(off: byte, val: byte) {
|
||||
@ -105,7 +105,7 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this.loc = (this.ramhi << 16) | (this.rammid << 8) | (this.ramlo);
|
||||
this.loc = (this.ramhi << 16) | (this.rammid << 8) | this.ramlo;
|
||||
|
||||
/*
|
||||
if (val === undefined) {
|
||||
@ -123,7 +123,7 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
|
||||
}
|
||||
|
||||
read(page: byte, off: byte) {
|
||||
return rom[this.firmware << 12 | (page - 0xC0) << 8 | off];
|
||||
return rom[(this.firmware << 12) | ((page - 0xc0) << 8) | off];
|
||||
}
|
||||
|
||||
write() {
|
||||
@ -138,7 +138,7 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
|
||||
return {
|
||||
loc: this.loc,
|
||||
firmware: this.firmware,
|
||||
mem: new Uint8Array(this.mem)
|
||||
mem: new Uint8Array(this.mem),
|
||||
};
|
||||
}
|
||||
|
||||
@ -149,6 +149,6 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
|
||||
|
||||
this.ramhi = (this.loc >> 16) & 0xff;
|
||||
this.rammid = (this.loc >> 8) & 0xff;
|
||||
this.ramlo = (this.loc) & 0xff;
|
||||
this.ramlo = this.loc & 0xff;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,20 @@
|
||||
import { debug, toHex } from '../util';
|
||||
import { rom as smartPortRom } from '../roms/cards/smartport';
|
||||
import { Card, Restorable, byte, word, rom } from '../types';
|
||||
import { MassStorage, BlockDisk, ENCODING_BLOCK, BlockFormat, MassStorageData, DiskFormat } from '../formats/types';
|
||||
import {
|
||||
MassStorage,
|
||||
BlockDisk,
|
||||
ENCODING_BLOCK,
|
||||
BlockFormat,
|
||||
MassStorageData,
|
||||
DiskFormat,
|
||||
} from '../formats/types';
|
||||
import { CPU6502, CpuState, flags } from '@whscullin/cpu6502';
|
||||
import { create2MGFromBlockDisk, HeaderData, read2MGHeader } from '../formats/2mg';
|
||||
import {
|
||||
create2MGFromBlockDisk,
|
||||
HeaderData,
|
||||
read2MGHeader,
|
||||
} from '../formats/2mg';
|
||||
import createBlockDisk from '../formats/block';
|
||||
import { DriveNumber } from '../formats/types';
|
||||
|
||||
@ -27,7 +38,11 @@ class Address {
|
||||
lo: byte;
|
||||
hi: byte;
|
||||
|
||||
constructor(private cpu: CPU6502, a: byte | word, b?: byte) {
|
||||
constructor(
|
||||
private cpu: CPU6502,
|
||||
a: byte,
|
||||
b?: byte
|
||||
) {
|
||||
if (b === undefined) {
|
||||
this.lo = a & 0xff;
|
||||
this.hi = a >> 8;
|
||||
@ -46,7 +61,10 @@ class Address {
|
||||
}
|
||||
|
||||
inc(val: byte) {
|
||||
return new Address(this.cpu, ((this.hi << 8 | this.lo) + val) & 0xffff);
|
||||
return new Address(
|
||||
this.cpu,
|
||||
(((this.hi << 8) | this.lo) + val) & 0xffff
|
||||
);
|
||||
}
|
||||
|
||||
readByte() {
|
||||
@ -57,7 +75,7 @@ class Address {
|
||||
const readLo = this.readByte();
|
||||
const readHi = this.inc(1).readByte();
|
||||
|
||||
return readHi << 8 | readLo;
|
||||
return (readHi << 8) | readLo;
|
||||
}
|
||||
|
||||
readAddress() {
|
||||
@ -97,8 +115,8 @@ const BLOCK_LO = 0x46;
|
||||
|
||||
// const IO_ERROR = 0x27;
|
||||
const NO_DEVICE_CONNECTED = 0x28;
|
||||
const WRITE_PROTECTED = 0x2B;
|
||||
const DEVICE_OFFLINE = 0x2F;
|
||||
const WRITE_PROTECTED = 0x2b;
|
||||
const DEVICE_OFFLINE = 0x2f;
|
||||
// const VOLUME_DIRECTORY_NOT_FOUND = 0x45;
|
||||
// const NOT_A_PRODOS_DISK = 0x52;
|
||||
// const VOLUME_CONTROL_BLOCK_FULL = 0x55;
|
||||
@ -123,8 +141,9 @@ const DEVICE_TYPE_SCSI_HD = 0x07;
|
||||
// $0D: Printer
|
||||
// $0E: Clock
|
||||
// $0F: Modem
|
||||
export default class SmartPort implements Card, MassStorage<BlockFormat>, Restorable<SmartPortState> {
|
||||
|
||||
export default class SmartPort
|
||||
implements Card, MassStorage<BlockFormat>, Restorable<SmartPortState>
|
||||
{
|
||||
private rom: rom;
|
||||
private disks: BlockDisk[] = [];
|
||||
private busy: boolean[] = [];
|
||||
@ -139,7 +158,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
||||
) {
|
||||
if (options?.block) {
|
||||
const dumbPortRom = new Uint8Array(smartPortRom);
|
||||
dumbPortRom[0x07] = 0x3C;
|
||||
dumbPortRom[0x07] = 0x3c;
|
||||
this.rom = dumbPortRom;
|
||||
debug('DumbPort card');
|
||||
} else {
|
||||
@ -221,7 +240,12 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
||||
* readBlock
|
||||
*/
|
||||
|
||||
readBlock(state: CpuState, driveNo: DriveNumber, block: number, buffer: Address) {
|
||||
readBlock(
|
||||
state: CpuState,
|
||||
driveNo: DriveNumber,
|
||||
block: number,
|
||||
buffer: Address
|
||||
) {
|
||||
this.debug(`read drive=${driveNo}`);
|
||||
this.debug(`read buffer=${buffer.toString()}`);
|
||||
this.debug(`read block=$${toHex(block)}`);
|
||||
@ -249,7 +273,12 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
||||
* writeBlock
|
||||
*/
|
||||
|
||||
writeBlock(state: CpuState, driveNo: DriveNumber, block: number, buffer: Address) {
|
||||
writeBlock(
|
||||
state: CpuState,
|
||||
driveNo: DriveNumber,
|
||||
block: number,
|
||||
buffer: Address
|
||||
) {
|
||||
this.debug(`write drive=${driveNo}`);
|
||||
this.debug(`write buffer=${buffer.toString()}`);
|
||||
this.debug(`write block=$${toHex(block)}`);
|
||||
@ -347,13 +376,14 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
||||
const blockOff = this.rom[0xff];
|
||||
const smartOff = blockOff + 3;
|
||||
|
||||
if (off === blockOff && this.cpu.getSync()) { // Regular block device entry POINT
|
||||
if (off === blockOff && this.cpu.getSync()) {
|
||||
// Regular block device entry POINT
|
||||
this.debug('block device entry');
|
||||
cmd = this.cpu.read(0x00, COMMAND);
|
||||
unit = this.cpu.read(0x00, UNIT);
|
||||
const bufferAddr = new Address(this.cpu, ADDRESS_LO);
|
||||
const blockAddr = new Address(this.cpu, BLOCK_LO);
|
||||
const drive = (unit & 0x80) ? 2 : 1;
|
||||
const drive = unit & 0x80 ? 2 : 1;
|
||||
const driveSlot = (unit & 0x70) >> 4;
|
||||
|
||||
buffer = bufferAddr.readAddress();
|
||||
@ -435,27 +465,42 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
||||
default: // Unit 1
|
||||
switch (status) {
|
||||
case 0:
|
||||
blocks = this.disks[unit]?.blocks.length ?? 0;
|
||||
blocks =
|
||||
this.disks[unit]?.blocks.length ?? 0;
|
||||
buffer.writeByte(0xf0); // W/R Block device in drive
|
||||
buffer.inc(1).writeByte(blocks & 0xff); // 1600 blocks
|
||||
buffer.inc(2).writeByte((blocks & 0xff00) >> 8);
|
||||
buffer.inc(3).writeByte((blocks & 0xff0000) >> 16);
|
||||
buffer
|
||||
.inc(2)
|
||||
.writeByte((blocks & 0xff00) >> 8);
|
||||
buffer
|
||||
.inc(3)
|
||||
.writeByte((blocks & 0xff0000) >> 16);
|
||||
state.x = 4;
|
||||
state.y = 0;
|
||||
state.a = 0;
|
||||
state.s &= ~flags.C;
|
||||
break;
|
||||
case 3:
|
||||
blocks = this.disks[unit]?.blocks.length ?? 0;
|
||||
blocks =
|
||||
this.disks[unit]?.blocks.length ?? 0;
|
||||
buffer.writeByte(0xf0); // W/R Block device in drive
|
||||
buffer.inc(1).writeByte(blocks & 0xff); // Blocks low byte
|
||||
buffer.inc(2).writeByte((blocks & 0xff00) >> 8); // Blocks middle byte
|
||||
buffer.inc(3).writeByte((blocks & 0xff0000) >> 16); // Blocks high byte
|
||||
buffer
|
||||
.inc(2)
|
||||
.writeByte((blocks & 0xff00) >> 8); // Blocks middle byte
|
||||
buffer
|
||||
.inc(3)
|
||||
.writeByte((blocks & 0xff0000) >> 16); // Blocks high byte
|
||||
buffer.inc(4).writeByte(ID.length); // Vendor ID length
|
||||
for (let idx = 0; idx < ID.length; idx++) { // Vendor ID
|
||||
buffer.inc(5 + idx).writeByte(ID.charCodeAt(idx));
|
||||
for (let idx = 0; idx < ID.length; idx++) {
|
||||
// Vendor ID
|
||||
buffer
|
||||
.inc(5 + idx)
|
||||
.writeByte(ID.charCodeAt(idx));
|
||||
}
|
||||
buffer.inc(21).writeByte(DEVICE_TYPE_SCSI_HD); // Device Type
|
||||
buffer
|
||||
.inc(21)
|
||||
.writeByte(DEVICE_TYPE_SCSI_HD); // Device Type
|
||||
buffer.inc(22).writeByte(0x0); // Device Subtype
|
||||
buffer.inc(23).writeWord(0x0101); // Version
|
||||
state.x = 24;
|
||||
@ -515,41 +560,38 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
||||
|
||||
getState() {
|
||||
return {
|
||||
disks: this.disks.map(
|
||||
(disk) => {
|
||||
const result: BlockDisk = {
|
||||
blocks: disk.blocks.map(
|
||||
(block) => new Uint8Array(block)
|
||||
),
|
||||
encoding: ENCODING_BLOCK,
|
||||
format: disk.format,
|
||||
readOnly: disk.readOnly,
|
||||
metadata: { ...disk.metadata },
|
||||
};
|
||||
return result;
|
||||
}
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
setState(state: SmartPortState) {
|
||||
this.disks = state.disks.map(
|
||||
(disk) => {
|
||||
disks: this.disks.map((disk) => {
|
||||
const result: BlockDisk = {
|
||||
blocks: disk.blocks.map(
|
||||
(block) => new Uint8Array(block)
|
||||
),
|
||||
blocks: disk.blocks.map((block) => new Uint8Array(block)),
|
||||
encoding: ENCODING_BLOCK,
|
||||
format: disk.format,
|
||||
readOnly: disk.readOnly,
|
||||
metadata: { ...disk.metadata },
|
||||
};
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
setBinary(driveNo: DriveNumber, name: string, fmt: BlockFormat, rawData: ArrayBuffer) {
|
||||
setState(state: SmartPortState) {
|
||||
this.disks = state.disks.map((disk) => {
|
||||
const result: BlockDisk = {
|
||||
blocks: disk.blocks.map((block) => new Uint8Array(block)),
|
||||
encoding: ENCODING_BLOCK,
|
||||
format: disk.format,
|
||||
readOnly: disk.readOnly,
|
||||
metadata: { ...disk.metadata },
|
||||
};
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
setBinary(
|
||||
driveNo: DriveNumber,
|
||||
name: string,
|
||||
fmt: BlockFormat,
|
||||
rawData: ArrayBuffer
|
||||
) {
|
||||
let volume = 254;
|
||||
let readOnly = false;
|
||||
if (fmt === '2mg') {
|
||||
|
@ -4,25 +4,26 @@ import { rom } from '../roms/cards/thunderclock';
|
||||
|
||||
const LOC = {
|
||||
CONTROL: 0x80,
|
||||
AUX: 0x88
|
||||
AUX: 0x88,
|
||||
} as const;
|
||||
|
||||
const COMMANDS = {
|
||||
MASK: 0x18,
|
||||
REGHOLD: 0x00,
|
||||
REGSHIFT: 0x08,
|
||||
TIMED: 0x18
|
||||
TIMED: 0x18,
|
||||
} as const;
|
||||
|
||||
const FLAGS = {
|
||||
DATA: 0x01,
|
||||
CLOCK: 0x02,
|
||||
STROBE: 0x04
|
||||
STROBE: 0x04,
|
||||
} as const;
|
||||
|
||||
export interface ThunderclockState {}
|
||||
|
||||
export default class Thunderclock implements Card, Restorable<ThunderclockState>
|
||||
export default class Thunderclock
|
||||
implements Card, Restorable<ThunderclockState>
|
||||
{
|
||||
constructor() {
|
||||
debug('Thunderclock');
|
||||
@ -82,7 +83,7 @@ export default class Thunderclock implements Card, Restorable<ThunderclockState>
|
||||
}
|
||||
|
||||
private access(off: byte, val?: byte) {
|
||||
switch (off & 0x8F) {
|
||||
switch (off & 0x8f) {
|
||||
case LOC.CONTROL:
|
||||
if (val !== undefined) {
|
||||
const strobe = val & FLAGS.STROBE ? true : false;
|
||||
@ -105,7 +106,10 @@ export default class Thunderclock implements Card, Restorable<ThunderclockState>
|
||||
this.shiftMode = false;
|
||||
break;
|
||||
default:
|
||||
this.debug('Unknown command', toHex(this.command));
|
||||
this.debug(
|
||||
'Unknown command',
|
||||
toHex(this.command)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -132,7 +136,7 @@ export default class Thunderclock implements Card, Restorable<ThunderclockState>
|
||||
if (page < 0xc8) {
|
||||
result = rom[off];
|
||||
} else {
|
||||
result = rom[(page - 0xc8) << 8 | off];
|
||||
result = rom[((page - 0xc8) << 8) | off];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -13,25 +13,25 @@ interface VideotermState {
|
||||
|
||||
const LOC = {
|
||||
IOREG: 0x80,
|
||||
IOVAL: 0x81
|
||||
IOVAL: 0x81,
|
||||
} as const;
|
||||
|
||||
const REGS = {
|
||||
CURSOR_UPPER: 0x0A,
|
||||
CURSOR_LOWER: 0x0B,
|
||||
STARTPOS_HI: 0x0C,
|
||||
STARTPOS_LO: 0x0D,
|
||||
CURSOR_HI: 0x0E,
|
||||
CURSOR_LO: 0x0F,
|
||||
CURSOR_UPPER: 0x0a,
|
||||
CURSOR_LOWER: 0x0b,
|
||||
STARTPOS_HI: 0x0c,
|
||||
STARTPOS_LO: 0x0d,
|
||||
CURSOR_HI: 0x0e,
|
||||
CURSOR_LO: 0x0f,
|
||||
LIGHTPEN_HI: 0x10,
|
||||
LIGHTPEN_LO: 0x11
|
||||
LIGHTPEN_LO: 0x11,
|
||||
} as const;
|
||||
|
||||
const CURSOR_MODES = {
|
||||
SOLID: 0x00,
|
||||
HIDDEN: 0x01,
|
||||
BLINK: 0x10,
|
||||
FAST_BLINK: 0x11
|
||||
FAST_BLINK: 0x11,
|
||||
} as const;
|
||||
|
||||
const BLACK: Color = [0x00, 0x00, 0x00];
|
||||
@ -56,7 +56,7 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
|
||||
0x00, // 0E - Cursor Hi
|
||||
0x00, // 0F - Cursor Lo
|
||||
0x00, // 10 - Lightpen Hi
|
||||
0x00 // 11 - Lightpen Lo
|
||||
0x00, // 11 - Lightpen Lo
|
||||
];
|
||||
|
||||
private blink = false;
|
||||
@ -123,7 +123,8 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
|
||||
}
|
||||
|
||||
private refreshCursor(fromRegs: boolean) {
|
||||
const addr = this.regs[REGS.CURSOR_HI] << 8 | this.regs[REGS.CURSOR_LO];
|
||||
const addr =
|
||||
(this.regs[REGS.CURSOR_HI] << 8) | this.regs[REGS.CURSOR_LO];
|
||||
const saddr = (0x800 + addr - this.startPos) & 0x7ff;
|
||||
const data = this.imageData.data;
|
||||
const row = (saddr / 80) & 0xff;
|
||||
@ -144,16 +145,20 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
|
||||
if (blinkmode === CURSOR_MODES.HIDDEN) {
|
||||
return;
|
||||
}
|
||||
if (this.blink || (blinkmode === CURSOR_MODES.SOLID)) {
|
||||
if (this.blink || blinkmode === CURSOR_MODES.SOLID) {
|
||||
this.dirty = true;
|
||||
for (let idx = 0; idx < 8; idx++) {
|
||||
const color = WHITE;
|
||||
if (idx >= (this.regs[REGS.CURSOR_UPPER] & 0x1f) &&
|
||||
idx <= (this.regs[REGS.CURSOR_LOWER] & 0x1f)) {
|
||||
if (
|
||||
idx >= (this.regs[REGS.CURSOR_UPPER] & 0x1f) &&
|
||||
idx <= (this.regs[REGS.CURSOR_LOWER] & 0x1f)
|
||||
) {
|
||||
for (let jdx = 0; jdx < 7; jdx++) {
|
||||
data[(y + idx) * 560 * 4 + (x + jdx) * 4] = color[0];
|
||||
data[(y + idx) * 560 * 4 + (x + jdx) * 4 + 1] = color[1];
|
||||
data[(y + idx) * 560 * 4 + (x + jdx) * 4 + 2] = color[2];
|
||||
data[(y + idx) * 560 * 4 + (x + jdx) * 4 + 1] =
|
||||
color[1];
|
||||
data[(y + idx) * 560 * 4 + (x + jdx) * 4 + 2] =
|
||||
color[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -162,8 +167,7 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
|
||||
|
||||
private updateStartPos() {
|
||||
const startPos =
|
||||
this.regs[REGS.STARTPOS_HI] << 8 |
|
||||
this.regs[REGS.STARTPOS_LO];
|
||||
(this.regs[REGS.STARTPOS_HI] << 8) | this.regs[REGS.STARTPOS_LO];
|
||||
if (this.startPos !== startPos) {
|
||||
this.startPos = startPos;
|
||||
this.shouldRefresh = true;
|
||||
@ -208,7 +212,7 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
|
||||
}
|
||||
break;
|
||||
}
|
||||
this.bank = (off & 0x0C) >> 2;
|
||||
this.bank = (off & 0x0c) >> 2;
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -218,9 +222,9 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
|
||||
|
||||
read(page: byte, off: byte) {
|
||||
if (page < 0xcc) {
|
||||
return ROM[(page & 0x03) << 8 | off];
|
||||
} else if (page < 0xce){
|
||||
const addr = ((page & 0x01) + (this.bank << 1)) << 8 | off;
|
||||
return ROM[((page & 0x03) << 8) | off];
|
||||
} else if (page < 0xce) {
|
||||
const addr = (((page & 0x01) + (this.bank << 1)) << 8) | off;
|
||||
return this.buffer[addr];
|
||||
}
|
||||
return 0;
|
||||
@ -228,7 +232,7 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
|
||||
|
||||
write(page: byte, off: byte, val: byte) {
|
||||
if (page > 0xcb && page < 0xce) {
|
||||
const addr = ((page & 0x01) + (this.bank << 1)) << 8 | off;
|
||||
const addr = (((page & 0x01) + (this.bank << 1)) << 8) | off;
|
||||
this.updateBuffer(addr, val);
|
||||
}
|
||||
}
|
||||
|
@ -24,16 +24,13 @@ export const App = () => {
|
||||
|
||||
const system = {
|
||||
...defaultSystem,
|
||||
...(systemTypes[systemType] || {})
|
||||
...(systemTypes[systemType] || {}),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cs(styles.container, componentStyles.components)}>
|
||||
<Header e={system.e} />
|
||||
<Apple2
|
||||
gl={gl}
|
||||
{...system}
|
||||
/>
|
||||
<Apple2 gl={gl} {...system} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { h } from 'preact';
|
||||
import cs from 'classnames';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'preact/hooks';
|
||||
import { Apple2 as Apple2Impl } from '../apple2';
|
||||
import { ControlStrip } from './ControlStrip';
|
||||
import { Debugger } from './debugger/Debugger';
|
||||
@ -60,40 +66,50 @@ export const Apple2 = (props: Apple2Props) => {
|
||||
const vm = apple2?.getVideoModes();
|
||||
const rom = apple2?.getROM();
|
||||
|
||||
const doPaste = useCallback((event: Event) => {
|
||||
if (
|
||||
(document.activeElement !== screenRef.current) &&
|
||||
(document.activeElement !== document.body)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (io) {
|
||||
const paste = (event.clipboardData || window.clipboardData)?.getData('text');
|
||||
if (paste) {
|
||||
io.setKeyBuffer(paste);
|
||||
const doPaste = useCallback(
|
||||
(event: Event) => {
|
||||
if (
|
||||
document.activeElement !== screenRef.current &&
|
||||
document.activeElement !== document.body
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
}, [io]);
|
||||
if (io) {
|
||||
const paste = (
|
||||
event.clipboardData || window.clipboardData
|
||||
)?.getData('text');
|
||||
if (paste) {
|
||||
io.setKeyBuffer(paste);
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
},
|
||||
[io]
|
||||
);
|
||||
|
||||
const doCopy = useCallback((event: Event) => {
|
||||
if (
|
||||
(document.activeElement !== screenRef.current) &&
|
||||
(document.activeElement !== document.body)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (vm) {
|
||||
event.clipboardData?.setData('text/plain', vm.getText());
|
||||
}
|
||||
event.preventDefault();
|
||||
}, [vm]);
|
||||
const doCopy = useCallback(
|
||||
(event: Event) => {
|
||||
if (
|
||||
document.activeElement !== screenRef.current &&
|
||||
document.activeElement !== document.body
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (vm) {
|
||||
event.clipboardData?.setData('text/plain', vm.getText());
|
||||
}
|
||||
event.preventDefault();
|
||||
},
|
||||
[vm]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (screenRef.current) {
|
||||
const options = {
|
||||
canvas: screenRef.current,
|
||||
tick: () => { /* do nothing */ },
|
||||
tick: () => {
|
||||
/* do nothing */
|
||||
},
|
||||
...props,
|
||||
};
|
||||
const apple2 = new Apple2Impl(options);
|
||||
@ -148,18 +164,33 @@ export const Apple2 = (props: Apple2Props) => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div
|
||||
className={cs(styles.outer, { apple2e: e, [styles.ready]: ready })}
|
||||
className={cs(styles.outer, {
|
||||
apple2e: e,
|
||||
[styles.ready]: ready,
|
||||
})}
|
||||
>
|
||||
<Screen screenRef={screenRef} />
|
||||
{!e ? <LanguageCard cpu={cpu} io={io} rom={rom} slot={0} /> : null}
|
||||
{!e ? (
|
||||
<LanguageCard cpu={cpu} io={io} rom={rom} slot={0} />
|
||||
) : null}
|
||||
<Slinky io={io} slot={2} />
|
||||
{!e ? <Videoterm io={io} slot={3} /> : null}
|
||||
<Mouse cpu={cpu} screenRef={screenRef} io={io} slot={4} />
|
||||
<ThunderClock io={io} slot={5} />
|
||||
<Inset>
|
||||
<Drives cpu={cpu} io={io} sectors={sectors} enhanced={enhanced} ready={drivesReady} />
|
||||
<Drives
|
||||
cpu={cpu}
|
||||
io={io}
|
||||
sectors={sectors}
|
||||
enhanced={enhanced}
|
||||
ready={drivesReady}
|
||||
/>
|
||||
</Inset>
|
||||
<ControlStrip apple2={apple2} e={e} toggleDebugger={toggleDebugger} />
|
||||
<ControlStrip
|
||||
apple2={apple2}
|
||||
e={e}
|
||||
toggleDebugger={toggleDebugger}
|
||||
/>
|
||||
<Inset>
|
||||
<Keyboard apple2={apple2} e={e} />
|
||||
</Inset>
|
||||
|
@ -85,12 +85,17 @@ export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => {
|
||||
id={`disk${number}`}
|
||||
className={cs(styles.diskLight, { [styles.on]: on })}
|
||||
/>
|
||||
<ControlButton title="Load Disk" onClick={onOpenModal} icon="folder-open" />
|
||||
<ControlButton title="Save Disk" onClick={onOpenDownloadModal} icon="save" />
|
||||
<div
|
||||
id={`disk-label${number}`}
|
||||
className={styles.diskLabel}
|
||||
>
|
||||
<ControlButton
|
||||
title="Load Disk"
|
||||
onClick={onOpenModal}
|
||||
icon="folder-open"
|
||||
/>
|
||||
<ControlButton
|
||||
title="Save Disk"
|
||||
onClick={onOpenDownloadModal}
|
||||
icon="save"
|
||||
/>
|
||||
<div id={`disk-label${number}`} className={styles.diskLabel}>
|
||||
{name}
|
||||
</div>
|
||||
</DiskDragTarget>
|
||||
|
@ -14,8 +14,10 @@ import styles from './css/BlockFileModal.module.scss';
|
||||
const DISK_TYPES: FilePickerAcceptType[] = [
|
||||
{
|
||||
description: 'Disk Images',
|
||||
accept: { 'application/octet-stream': BLOCK_FORMATS.map(x => '.' + x) },
|
||||
}
|
||||
accept: {
|
||||
'application/octet-stream': BLOCK_FORMATS.map((x) => '.' + x),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
interface BlockFileModalProps {
|
||||
@ -25,7 +27,12 @@ interface BlockFileModalProps {
|
||||
onClose: (closeBox?: boolean) => void;
|
||||
}
|
||||
|
||||
export const BlockFileModal = ({ smartPort, driveNo: number, onClose, isOpen }: BlockFileModalProps) => {
|
||||
export const BlockFileModal = ({
|
||||
smartPort,
|
||||
driveNo: number,
|
||||
onClose,
|
||||
isOpen,
|
||||
}: BlockFileModalProps) => {
|
||||
const [handles, setHandles] = useState<FileSystemFileHandle[]>();
|
||||
const [busy, setBusy] = useState<boolean>(false);
|
||||
const [empty, setEmpty] = useState<boolean>(true);
|
||||
@ -41,7 +48,11 @@ export const BlockFileModal = ({ smartPort, driveNo: number, onClose, isOpen }:
|
||||
hashParts[number] = '';
|
||||
setBusy(true);
|
||||
try {
|
||||
await loadLocalBlockFile(smartPort, number, await handles[0].getFile());
|
||||
await loadLocalBlockFile(
|
||||
smartPort,
|
||||
number,
|
||||
await handles[0].getFile()
|
||||
);
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
} finally {
|
||||
@ -68,7 +79,9 @@ export const BlockFileModal = ({ smartPort, driveNo: number, onClose, isOpen }:
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<button onClick={doCancel}>Cancel</button>
|
||||
<button onClick={noAwait(doOpen)} disabled={busy || empty}>Open</button>
|
||||
<button onClick={noAwait(doOpen)} disabled={busy || empty}>
|
||||
Open
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
<ErrorModal error={error} setError={setError} />
|
||||
|
@ -38,19 +38,11 @@ export const CPUMeter = ({ apple2 }: CPUMeterProps) => {
|
||||
const time = Date.now();
|
||||
const delta = time - lastTime.current;
|
||||
if (stats) {
|
||||
setKhz(
|
||||
Math.floor(
|
||||
(stats.cycles - cycles) / delta
|
||||
)
|
||||
);
|
||||
setFps(
|
||||
Math.floor(
|
||||
(stats.frames - frames) / delta * 1000
|
||||
)
|
||||
);
|
||||
setKhz(Math.floor((stats.cycles - cycles) / delta));
|
||||
setFps(Math.floor(((stats.frames - frames) / delta) * 1000));
|
||||
setRps(
|
||||
Math.floor(
|
||||
(stats.renderedFrames - renderedFrames) / delta * 1000
|
||||
((stats.renderedFrames - renderedFrames) / delta) * 1000
|
||||
)
|
||||
);
|
||||
lastStats.current = { ...stats };
|
||||
|
@ -22,8 +22,19 @@ export interface ControlButtonProps {
|
||||
* @param onClick Click callback
|
||||
* @returns Control Button component
|
||||
*/
|
||||
export const ControlButton = ({ active, icon, title, onClick, ...props }: ControlButtonProps) => (
|
||||
<button className={styles.iconButton} onClick={onClick} title={title} {...props} >
|
||||
export const ControlButton = ({
|
||||
active,
|
||||
icon,
|
||||
title,
|
||||
onClick,
|
||||
...props
|
||||
}: ControlButtonProps) => (
|
||||
<button
|
||||
className={styles.iconButton}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
{...props}
|
||||
>
|
||||
<i className={cs('fas', `fa-${icon}`, { [styles.active]: active })}></i>
|
||||
</button>
|
||||
);
|
||||
|
@ -33,7 +33,11 @@ interface ControlStripProps {
|
||||
* @param e Whether or not this is a //e
|
||||
* @returns ControlStrip component
|
||||
*/
|
||||
export const ControlStrip = ({ apple2, e, toggleDebugger }: ControlStripProps) => {
|
||||
export const ControlStrip = ({
|
||||
apple2,
|
||||
e,
|
||||
toggleDebugger,
|
||||
}: ControlStripProps) => {
|
||||
const [showOptions, setShowOptions] = useState(false);
|
||||
const [io, setIO] = useState<Apple2IO>();
|
||||
const options = useContext(OptionsContext);
|
||||
@ -55,23 +59,22 @@ export const ControlStrip = ({ apple2, e, toggleDebugger }: ControlStripProps) =
|
||||
}
|
||||
}, [apple2, e, options]);
|
||||
|
||||
const doReset = useCallback(() =>
|
||||
apple2?.reset(), [apple2]);
|
||||
const doReset = useCallback(() => apple2?.reset(), [apple2]);
|
||||
|
||||
const doReadme = useCallback(() =>
|
||||
window.open(README, '_blank'), []);
|
||||
const doReadme = useCallback(() => window.open(README, '_blank'), []);
|
||||
|
||||
const doShowOptions = useCallback(() =>
|
||||
setShowOptions(true), []);
|
||||
const doShowOptions = useCallback(() => setShowOptions(true), []);
|
||||
|
||||
const doCloseOptions = useCallback(() =>
|
||||
setShowOptions(false), []);
|
||||
const doCloseOptions = useCallback(() => setShowOptions(false), []);
|
||||
|
||||
const doToggleFullPage = useCallback(() =>
|
||||
options.setOption(
|
||||
SCREEN_FULL_PAGE,
|
||||
!options.getOption(SCREEN_FULL_PAGE)
|
||||
), [options]);
|
||||
const doToggleFullPage = useCallback(
|
||||
() =>
|
||||
options.setOption(
|
||||
SCREEN_FULL_PAGE,
|
||||
!options.getOption(SCREEN_FULL_PAGE)
|
||||
),
|
||||
[options]
|
||||
);
|
||||
|
||||
useHotKey('F2', doToggleFullPage);
|
||||
useHotKey('F4', doShowOptions);
|
||||
@ -82,12 +85,20 @@ export const ControlStrip = ({ apple2, e, toggleDebugger }: ControlStripProps) =
|
||||
<OptionsModal isOpen={showOptions} onClose={doCloseOptions} />
|
||||
<Inset>
|
||||
<CPUMeter apple2={apple2} />
|
||||
<ControlButton onClick={toggleDebugger} title="Toggle Debugger" icon="bug" />
|
||||
<ControlButton
|
||||
onClick={toggleDebugger}
|
||||
title="Toggle Debugger"
|
||||
icon="bug"
|
||||
/>
|
||||
<AudioControl apple2={apple2} />
|
||||
<Printer io={io} slot={1} />
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
<ControlButton onClick={doReadme} title="About" icon="info" />
|
||||
<ControlButton onClick={doShowOptions} title="Options (F4)" icon="cog" />
|
||||
<ControlButton
|
||||
onClick={doShowOptions}
|
||||
title="Options (F4)"
|
||||
icon="cog"
|
||||
/>
|
||||
</Inset>
|
||||
{e && (
|
||||
<div className={styles.reset} onClick={doReset}>
|
||||
|
@ -1,15 +1,20 @@
|
||||
import { BLOCK_FORMATS, DISK_FORMATS, DriveNumber, FLOPPY_FORMATS, MassStorage } from 'js/formats/types';
|
||||
import {
|
||||
BLOCK_FORMATS,
|
||||
DISK_FORMATS,
|
||||
DriveNumber,
|
||||
FLOPPY_FORMATS,
|
||||
MassStorage,
|
||||
} from 'js/formats/types';
|
||||
import { h, JSX, RefObject } from 'preact';
|
||||
import { useEffect, useRef } from 'preact/hooks';
|
||||
import { loadLocalFile } from './util/files';
|
||||
import { loadLocalFile } from './util/files';
|
||||
import { spawn } from './util/promises';
|
||||
|
||||
export interface DiskDragTargetProps<T> extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
export interface DiskDragTargetProps<T>
|
||||
extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
storage: MassStorage<T> | undefined;
|
||||
driveNo?: DriveNumber;
|
||||
formats: typeof FLOPPY_FORMATS
|
||||
| typeof BLOCK_FORMATS
|
||||
| typeof DISK_FORMATS;
|
||||
formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS;
|
||||
dropRef?: RefObject<HTMLElement>;
|
||||
onError: (error: unknown) => void;
|
||||
}
|
||||
@ -32,7 +37,11 @@ export const DiskDragTarget = ({
|
||||
event.preventDefault();
|
||||
const dt = event.dataTransfer;
|
||||
if (dt) {
|
||||
if (Array.from(dt.items).every((item) => item.kind === 'file')) {
|
||||
if (
|
||||
Array.from(dt.items).every(
|
||||
(item) => item.kind === 'file'
|
||||
)
|
||||
) {
|
||||
dt.dropEffect = 'copy';
|
||||
} else {
|
||||
dt.dropEffect = 'none';
|
||||
@ -54,13 +63,18 @@ export const DiskDragTarget = ({
|
||||
const onDrop = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const targetDrive = driveNo ?? 1; //TODO(whscullin) Maybe pick available drive
|
||||
const targetDrive = driveNo ?? 1; //TODO(whscullin) Maybe pick available drive
|
||||
|
||||
const dt = event.dataTransfer;
|
||||
if (dt?.files.length === 1 && storage) {
|
||||
spawn(async () => {
|
||||
try {
|
||||
await loadLocalFile(storage, formats, targetDrive, dt.files[0]);
|
||||
await loadLocalFile(
|
||||
storage,
|
||||
formats,
|
||||
targetDrive,
|
||||
dt.files[0]
|
||||
);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
@ -68,8 +82,18 @@ export const DiskDragTarget = ({
|
||||
} else if (dt?.files.length === 2 && storage) {
|
||||
spawn(async () => {
|
||||
try {
|
||||
await loadLocalFile(storage, formats, 1, dt.files[0]);
|
||||
await loadLocalFile(storage, formats, 2, dt.files[1]);
|
||||
await loadLocalFile(
|
||||
storage,
|
||||
formats,
|
||||
1,
|
||||
dt.files[0]
|
||||
);
|
||||
await loadLocalFile(
|
||||
storage,
|
||||
formats,
|
||||
2,
|
||||
dt.files[1]
|
||||
);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
|
@ -70,7 +70,12 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
|
||||
formats={FLOPPY_FORMATS}
|
||||
onError={setError}
|
||||
>
|
||||
<FileModal disk2={disk2} driveNo={number} onClose={doClose} isOpen={modalOpen} />
|
||||
<FileModal
|
||||
disk2={disk2}
|
||||
driveNo={number}
|
||||
onClose={doClose}
|
||||
isOpen={modalOpen}
|
||||
/>
|
||||
<DownloadModal
|
||||
driveNo={number}
|
||||
massStorage={disk2}
|
||||
@ -79,11 +84,17 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
|
||||
/>
|
||||
<ErrorModal error={error} setError={setError} />
|
||||
<div className={cs(styles.diskLight, { [styles.on]: on })} />
|
||||
<ControlButton title="Load Disk" onClick={onOpenModal} icon="folder-open" />
|
||||
<ControlButton title="Save Disk" onClick={onOpenDownloadModal} icon="save" />
|
||||
<div className={styles.diskLabel}>
|
||||
{label}
|
||||
</div>
|
||||
<ControlButton
|
||||
title="Load Disk"
|
||||
onClick={onOpenModal}
|
||||
icon="folder-open"
|
||||
/>
|
||||
<ControlButton
|
||||
title="Save Disk"
|
||||
onClick={onOpenDownloadModal}
|
||||
icon="save"
|
||||
/>
|
||||
<div className={styles.diskLabel}>{label}</div>
|
||||
</DiskDragTarget>
|
||||
);
|
||||
};
|
||||
|
@ -12,7 +12,12 @@ interface DownloadModalProps {
|
||||
onClose: (closeBox?: boolean) => void;
|
||||
}
|
||||
|
||||
export const DownloadModal = ({ massStorage, driveNo, onClose, isOpen }: DownloadModalProps) => {
|
||||
export const DownloadModal = ({
|
||||
massStorage,
|
||||
driveNo,
|
||||
onClose,
|
||||
isOpen,
|
||||
}: DownloadModalProps) => {
|
||||
const [href, setHref] = useState('');
|
||||
const [downloadName, setDownloadName] = useState('');
|
||||
const doCancel = useCallback(() => onClose(true), [onClose]);
|
||||
@ -24,10 +29,9 @@ export const DownloadModal = ({ massStorage, driveNo, onClose, isOpen }: Downloa
|
||||
const { ext, data } = storageData;
|
||||
const { name } = storageData.metadata;
|
||||
if (data.byteLength) {
|
||||
const blob = new Blob(
|
||||
[data],
|
||||
{ type: 'application/octet-stream' }
|
||||
);
|
||||
const blob = new Blob([data], {
|
||||
type: 'application/octet-stream',
|
||||
});
|
||||
const href = window.URL.createObjectURL(blob);
|
||||
setHref(href);
|
||||
setDownloadName(`${name}.${ext}`);
|
||||
@ -44,22 +48,20 @@ export const DownloadModal = ({ massStorage, driveNo, onClose, isOpen }: Downloa
|
||||
<Modal title="Save File" isOpen={isOpen} onClose={onClose}>
|
||||
<ModalContent>
|
||||
<div className={styles.modalContent}>
|
||||
{href
|
||||
? (
|
||||
<>
|
||||
<span>Disk Name: {downloadName}</span>
|
||||
<a
|
||||
role="button"
|
||||
href={href}
|
||||
download={downloadName}
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<span>No Download Available</span>
|
||||
)
|
||||
}
|
||||
{href ? (
|
||||
<>
|
||||
<span>Disk Name: {downloadName}</span>
|
||||
<a
|
||||
role="button"
|
||||
href={href}
|
||||
download={downloadName}
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<span>No Download Available</span>
|
||||
)}
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
|
@ -8,9 +8,18 @@ import { CPU6502 } from '@whscullin/cpu6502';
|
||||
import { BlockDisk } from './BlockDisk';
|
||||
import { ErrorModal } from './ErrorModal';
|
||||
import { ProgressModal } from './ProgressModal';
|
||||
import { loadHttpUnknownFile, getHashParts, loadJSON, SmartStorageBroker } from './util/files';
|
||||
import {
|
||||
loadHttpUnknownFile,
|
||||
getHashParts,
|
||||
loadJSON,
|
||||
SmartStorageBroker,
|
||||
} from './util/files';
|
||||
import { useHash } from './hooks/useHash';
|
||||
import { DISK_FORMATS, DRIVE_NUMBERS, SupportedSectors } from 'js/formats/types';
|
||||
import {
|
||||
DISK_FORMATS,
|
||||
DRIVE_NUMBERS,
|
||||
SupportedSectors,
|
||||
} from 'js/formats/types';
|
||||
import { spawn, Ready } from './util/promises';
|
||||
|
||||
import styles from './css/Drives.module.scss';
|
||||
@ -74,12 +83,12 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
|
||||
const [smartData1, setSmartData1] = useState<DiskIIData>({
|
||||
on: false,
|
||||
number: 1,
|
||||
name: 'HD 1'
|
||||
name: 'HD 1',
|
||||
});
|
||||
const [smartData2, setSmartData2] = useState<DiskIIData>({
|
||||
on: false,
|
||||
number: 2,
|
||||
name: 'HD 2'
|
||||
name: 'HD 2',
|
||||
});
|
||||
|
||||
const hash = useHash();
|
||||
@ -97,25 +106,30 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
|
||||
const isJson = hashPart.match(/\.json$/i);
|
||||
if (isHttp && !isJson) {
|
||||
loading++;
|
||||
controllers.push(spawn(async (signal) => {
|
||||
try {
|
||||
await loadHttpUnknownFile(
|
||||
smartStorageBroker,
|
||||
driveNo,
|
||||
hashPart,
|
||||
signal,
|
||||
onProgress);
|
||||
} catch (e) {
|
||||
setError(e);
|
||||
}
|
||||
if (--loading === 0) {
|
||||
ready.onReady();
|
||||
}
|
||||
setCurrent(0);
|
||||
setTotal(0);
|
||||
}));
|
||||
controllers.push(
|
||||
spawn(async (signal) => {
|
||||
try {
|
||||
await loadHttpUnknownFile(
|
||||
smartStorageBroker,
|
||||
driveNo,
|
||||
hashPart,
|
||||
signal,
|
||||
onProgress
|
||||
);
|
||||
} catch (e) {
|
||||
setError(e);
|
||||
}
|
||||
if (--loading === 0) {
|
||||
ready.onReady();
|
||||
}
|
||||
setCurrent(0);
|
||||
setTotal(0);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const url = isHttp ? hashPart : `json/disks/${hashPart}.json`;
|
||||
const url = isHttp
|
||||
? hashPart
|
||||
: `json/disks/${hashPart}.json`;
|
||||
loadJSON(disk2, driveNo, url).catch((e) => setError(e));
|
||||
}
|
||||
}
|
||||
@ -123,7 +137,8 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
|
||||
if (!loading) {
|
||||
ready.onReady();
|
||||
}
|
||||
return () => controllers.forEach((controller) => controller.abort());
|
||||
return () =>
|
||||
controllers.forEach((controller) => controller.abort());
|
||||
}
|
||||
}, [hash, onProgress, ready, storageDevices]);
|
||||
|
||||
@ -132,10 +147,10 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
|
||||
const setSmartData = [setSmartData1, setSmartData2];
|
||||
const callbacks: Callbacks = {
|
||||
driveLight: (drive, on) => {
|
||||
setData[drive - 1]?.(data => ({ ...data, on }));
|
||||
setData[drive - 1]?.((data) => ({ ...data, on }));
|
||||
},
|
||||
label: (drive, name, side) => {
|
||||
setData[drive - 1]?.(data => ({
|
||||
setData[drive - 1]?.((data) => ({
|
||||
...data,
|
||||
name: name ?? `Disk ${drive}`,
|
||||
side,
|
||||
@ -143,27 +158,31 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
|
||||
},
|
||||
dirty: () => {
|
||||
// do nothing
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const smartPortCallbacks: Callbacks = {
|
||||
driveLight: (drive, on) => {
|
||||
setSmartData[drive - 1]?.(data => ({ ...data, on }));
|
||||
setSmartData[drive - 1]?.((data) => ({ ...data, on }));
|
||||
},
|
||||
label: (drive, name, side) => {
|
||||
setSmartData[drive - 1]?.(data => ({
|
||||
setSmartData[drive - 1]?.((data) => ({
|
||||
...data,
|
||||
name: name ?? `HD ${drive}`,
|
||||
side,
|
||||
}));
|
||||
},
|
||||
dirty: () => {/* Unused */ }
|
||||
dirty: () => {
|
||||
/* Unused */
|
||||
},
|
||||
};
|
||||
|
||||
if (cpu && io) {
|
||||
const disk2 = new Disk2(io, callbacks, sectors);
|
||||
io.setSlot(6, disk2);
|
||||
const smartPort = new SmartPort(cpu, smartPortCallbacks, { block: !enhanced });
|
||||
const smartPort = new SmartPort(cpu, smartPortCallbacks, {
|
||||
block: !enhanced,
|
||||
});
|
||||
io.setSlot(7, smartPort);
|
||||
|
||||
const smartStorageBroker = new SmartStorageBroker(disk2, smartPort);
|
||||
|
@ -5,7 +5,7 @@ import { Modal, ModalContent, ModalFooter } from './Modal';
|
||||
import styles from './css/ErrorModal.module.scss';
|
||||
|
||||
export interface ErrorProps {
|
||||
error: unknown | undefined;
|
||||
error: unknown;
|
||||
setError: (error: string | undefined) => void;
|
||||
}
|
||||
|
||||
@ -32,9 +32,7 @@ export const ErrorModal = ({ error, setError }: ErrorProps) => {
|
||||
onClose={onClose}
|
||||
>
|
||||
<ModalContent>
|
||||
<div className={styles.errorModal}>
|
||||
{errorStr}
|
||||
</div>
|
||||
<div className={styles.errorModal}>{errorStr}</div>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<button onClick={onClose}>OK</button>
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { h, Fragment } from 'preact';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'preact/hooks';
|
||||
import { noAwait } from './util/promises';
|
||||
|
||||
export interface FilePickerAcceptType {
|
||||
@ -34,7 +40,9 @@ interface ExtraProps {
|
||||
|
||||
const InputFileChooser = ({
|
||||
disabled = false,
|
||||
onChange = () => { /* do nothing */ },
|
||||
onChange = () => {
|
||||
/* do nothing */
|
||||
},
|
||||
accept = [],
|
||||
}: InputFileChooserProps) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@ -69,7 +77,9 @@ const InputFileChooser = ({
|
||||
// the moment, not adding the MIME type is sufficient.
|
||||
const newAccept: string[] = [];
|
||||
for (const type of accept) {
|
||||
for (let [/* typeString */, suffixes] of Object.entries(type.accept)) {
|
||||
for (let [, /* typeString */ suffixes] of Object.entries(
|
||||
type.accept
|
||||
)) {
|
||||
// newAccept.push(typeString);
|
||||
if (!Array.isArray(suffixes)) {
|
||||
suffixes = [suffixes];
|
||||
@ -91,11 +101,15 @@ const InputFileChooser = ({
|
||||
}, [accept]);
|
||||
|
||||
return (
|
||||
<input type="file" role='button' aria-label='Open file'
|
||||
<input
|
||||
type="file"
|
||||
role="button"
|
||||
aria-label="Open file"
|
||||
ref={inputRef}
|
||||
onChange={onChangeInternal}
|
||||
disabled={disabled}
|
||||
{...extraProps} />
|
||||
{...extraProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -107,8 +121,10 @@ interface FilePickerChooserProps {
|
||||
|
||||
const FilePickerChooser = ({
|
||||
disabled = false,
|
||||
onChange = () => { /* do nothing */ },
|
||||
accept = [ACCEPT_EVERYTHING_TYPE]
|
||||
onChange = () => {
|
||||
/* do nothing */
|
||||
},
|
||||
accept = [ACCEPT_EVERYTHING_TYPE],
|
||||
}: FilePickerChooserProps) => {
|
||||
const [busy, setBusy] = useState<boolean>(false);
|
||||
const [selectedFilename, setSelectedFilename] = useState<string>();
|
||||
@ -138,14 +154,16 @@ const FilePickerChooser = ({
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedFilename(
|
||||
fileHandles?.length
|
||||
? fileHandles[0].name
|
||||
: 'No file selected');
|
||||
fileHandles?.length ? fileHandles[0].name : 'No file selected'
|
||||
);
|
||||
}, [fileHandles]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={noAwait(onClickInternal)} disabled={disabled || busy}>
|
||||
<button
|
||||
onClick={noAwait(onClickInternal)}
|
||||
disabled={disabled || busy}
|
||||
>
|
||||
Choose File
|
||||
</button>
|
||||
|
||||
@ -164,7 +182,7 @@ const FilePickerChooser = ({
|
||||
* Using `window.showOpenFilePicker` has the advantage of allowing read/write
|
||||
* access to the file, whereas the regular input element only gives read
|
||||
* access.
|
||||
*
|
||||
*
|
||||
* The FileChooser takes an optional `accept` parameter that specifies which
|
||||
* types of files can be opened. The parameter is a map of MIME type to file
|
||||
* extension. If the MIME type is the empty string, t
|
||||
@ -174,38 +192,48 @@ export const FileChooser = ({
|
||||
control = controlDefault,
|
||||
...rest
|
||||
}: FileChooserProps) => {
|
||||
|
||||
const onChangeForInput = useCallback((files: FileList) => {
|
||||
const handles: FileSystemFileHandle[] = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files.item(i);
|
||||
if (file === null) {
|
||||
continue;
|
||||
const onChangeForInput = useCallback(
|
||||
(files: FileList) => {
|
||||
const handles: FileSystemFileHandle[] = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files.item(i);
|
||||
if (file === null) {
|
||||
continue;
|
||||
}
|
||||
handles.push({
|
||||
kind: 'file',
|
||||
name: file.name,
|
||||
getFile: () => Promise.resolve(file),
|
||||
createWritable: (_options) =>
|
||||
Promise.reject('File not writable.'),
|
||||
queryPermission: (descriptor) =>
|
||||
Promise.resolve(
|
||||
descriptor?.mode === 'read' ? 'granted' : 'denied'
|
||||
),
|
||||
requestPermission: (descriptor) =>
|
||||
Promise.resolve(
|
||||
descriptor?.mode === 'read' ? 'granted' : 'denied'
|
||||
),
|
||||
isSameEntry: (_unused) => Promise.resolve(false),
|
||||
isDirectory: false,
|
||||
isFile: true,
|
||||
});
|
||||
}
|
||||
handles.push({
|
||||
kind: 'file',
|
||||
name: file.name,
|
||||
getFile: () => Promise.resolve(file),
|
||||
createWritable: (_options) => Promise.reject('File not writable.'),
|
||||
queryPermission: (descriptor) => Promise.resolve(descriptor?.mode === 'read' ? 'granted' : 'denied'),
|
||||
requestPermission: (descriptor) => Promise.resolve(descriptor?.mode === 'read' ? 'granted' : 'denied'),
|
||||
isSameEntry: (_unused) => Promise.resolve(false),
|
||||
isDirectory: false,
|
||||
isFile: true,
|
||||
});
|
||||
}
|
||||
onChange(handles);
|
||||
}, [onChange]);
|
||||
onChange(handles);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const onChangeForPicker = useCallback((fileHandles: FileSystemFileHandle[]) => {
|
||||
onChange(fileHandles);
|
||||
}, [onChange]);
|
||||
const onChangeForPicker = useCallback(
|
||||
(fileHandles: FileSystemFileHandle[]) => {
|
||||
onChange(fileHandles);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return control === 'picker'
|
||||
? (
|
||||
<FilePickerChooser onChange={onChangeForPicker} {...rest} />
|
||||
)
|
||||
: (
|
||||
<InputFileChooser onChange={onChangeForInput} {...rest} />
|
||||
);
|
||||
return control === 'picker' ? (
|
||||
<FilePickerChooser onChange={onChangeForPicker} {...rest} />
|
||||
) : (
|
||||
<InputFileChooser onChange={onChangeForInput} {...rest} />
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +1,18 @@
|
||||
import { h, Fragment, JSX } from 'preact';
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||
import { DiskDescriptor, DriveNumber, FLOPPY_FORMATS, NibbleFormat } from '../formats/types';
|
||||
import {
|
||||
DiskDescriptor,
|
||||
DriveNumber,
|
||||
FLOPPY_FORMATS,
|
||||
NibbleFormat,
|
||||
} from '../formats/types';
|
||||
import { Modal, ModalContent, ModalFooter } from './Modal';
|
||||
import { loadLocalNibbleFile, loadJSON, getHashParts, setHashParts } from './util/files';
|
||||
import {
|
||||
loadLocalNibbleFile,
|
||||
loadJSON,
|
||||
getHashParts,
|
||||
setHashParts,
|
||||
} from './util/files';
|
||||
import DiskII from '../cards/disk2';
|
||||
import { ErrorModal } from './ErrorModal';
|
||||
|
||||
@ -15,8 +25,10 @@ import styles from './css/FileModal.module.scss';
|
||||
const DISK_TYPES: FilePickerAcceptType[] = [
|
||||
{
|
||||
description: 'Disk Images',
|
||||
accept: { 'application/octet-stream': FLOPPY_FORMATS.map(x => '.' + x) },
|
||||
}
|
||||
accept: {
|
||||
'application/octet-stream': FLOPPY_FORMATS.map((x) => '.' + x),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export type NibbleFileCallback = (
|
||||
@ -38,7 +50,12 @@ interface IndexEntry {
|
||||
category: string;
|
||||
}
|
||||
|
||||
export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) => {
|
||||
export const FileModal = ({
|
||||
disk2,
|
||||
driveNo,
|
||||
onClose,
|
||||
isOpen,
|
||||
}: FileModalProps) => {
|
||||
const [busy, setBusy] = useState<boolean>(false);
|
||||
const [empty, setEmpty] = useState<boolean>(true);
|
||||
const [category, setCategory] = useState<string>();
|
||||
@ -52,7 +69,7 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
|
||||
spawn(async () => {
|
||||
try {
|
||||
const indexResponse = await fetch('json/disks/index.json');
|
||||
const index = await indexResponse.json() as IndexEntry[];
|
||||
const index = (await indexResponse.json()) as IndexEntry[];
|
||||
setIndex(index);
|
||||
} catch (error) {
|
||||
setIndex([]);
|
||||
@ -70,7 +87,11 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
|
||||
try {
|
||||
if (handles?.length === 1) {
|
||||
hashParts[driveNo] = '';
|
||||
await loadLocalNibbleFile(disk2, driveNo, await handles[0].getFile());
|
||||
await loadLocalNibbleFile(
|
||||
disk2,
|
||||
driveNo,
|
||||
await handles[0].getFile()
|
||||
);
|
||||
}
|
||||
if (filename) {
|
||||
const name = filename.match(/\/([^/]+).json$/) || ['', ''];
|
||||
@ -95,15 +116,16 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
|
||||
|
||||
const doSelectCategory = useCallback(
|
||||
(event: JSX.TargetedMouseEvent<HTMLSelectElement>) =>
|
||||
setCategory(event.currentTarget.value)
|
||||
, []
|
||||
setCategory(event.currentTarget.value),
|
||||
[]
|
||||
);
|
||||
|
||||
const doSelectFilename = useCallback(
|
||||
(event: JSX.TargetedMouseEvent<HTMLSelectElement>) => {
|
||||
setEmpty(!event.currentTarget.value);
|
||||
setFilename(event.currentTarget.value);
|
||||
}, []
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
if (!index) {
|
||||
@ -111,10 +133,7 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
|
||||
}
|
||||
|
||||
const categories = index.reduce<Record<string, DiskDescriptor[]>>(
|
||||
(
|
||||
acc: Record<string, DiskDescriptor[]>,
|
||||
disk: DiskDescriptor
|
||||
) => {
|
||||
(acc: Record<string, DiskDescriptor[]>, disk: DiskDescriptor) => {
|
||||
const category = disk.category || 'Misc';
|
||||
acc[category] = [disk, ...(acc[category] || [])];
|
||||
|
||||
@ -138,7 +157,10 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
|
||||
</select>
|
||||
<select multiple onChange={doSelectFilename}>
|
||||
{disks.map((disk) => (
|
||||
<option key={disk.filename} value={disk.filename}>
|
||||
<option
|
||||
key={disk.filename}
|
||||
value={disk.filename}
|
||||
>
|
||||
{disk.name}
|
||||
{disk.disk ? ` - ${disk.disk}` : ''}
|
||||
</option>
|
||||
@ -149,7 +171,9 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<button onClick={doCancel}>Cancel</button>
|
||||
<button onClick={noAwait(doOpen)} disabled={busy || empty}>Open</button>
|
||||
<button onClick={noAwait(doOpen)} disabled={busy || empty}>
|
||||
Open
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
<ErrorModal error={error} setError={setError} />
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
keys2e,
|
||||
mapMouseEvent,
|
||||
keysAsTuples,
|
||||
mapKeyboardEvent
|
||||
mapKeyboardEvent,
|
||||
} from './util/keyboard';
|
||||
|
||||
import styles from './css/Keyboard.module.scss';
|
||||
@ -58,26 +58,21 @@ export const Key = ({
|
||||
active,
|
||||
pressed,
|
||||
onMouseDown,
|
||||
onMouseUp
|
||||
onMouseUp,
|
||||
}: KeyProps) => {
|
||||
const keyName = lower.replace(/[&#;]/g, '');
|
||||
const center =
|
||||
lower === 'LOCK'
|
||||
? styles.vCenter2
|
||||
: (upper === lower && upper.length > 1)
|
||||
? styles.vCenter
|
||||
: '';
|
||||
: upper === lower && upper.length > 1
|
||||
? styles.vCenter
|
||||
: '';
|
||||
return (
|
||||
<div
|
||||
className={cs(
|
||||
styles.key,
|
||||
styles[`key-${keyName}`],
|
||||
center,
|
||||
{
|
||||
[styles.pressed]: pressed,
|
||||
[styles.active]: active,
|
||||
},
|
||||
)}
|
||||
className={cs(styles.key, styles[`key-${keyName}`], center, {
|
||||
[styles.pressed]: pressed,
|
||||
[styles.active]: active,
|
||||
})}
|
||||
data-key1={lower}
|
||||
data-key2={upper}
|
||||
onMouseDown={onMouseDown}
|
||||
@ -85,7 +80,12 @@ export const Key = ({
|
||||
>
|
||||
<div>
|
||||
{buildLabel(upper)}
|
||||
{upper !== lower && <><br />{buildLabel(lower)}</>}
|
||||
{upper !== lower && (
|
||||
<>
|
||||
<br />
|
||||
{buildLabel(lower)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -119,15 +119,22 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.activeElement && document.activeElement !== document.body) {
|
||||
if (
|
||||
document.activeElement &&
|
||||
document.activeElement !== document.body
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const { key, keyCode, keyLabel } = mapKeyboardEvent(event, active.includes('LOCK'), active.includes('CTRL'));
|
||||
setPressed(pressed => pressed.concat([keyLabel]));
|
||||
setActive(active => active.concat([keyLabel]));
|
||||
const { key, keyCode, keyLabel } = mapKeyboardEvent(
|
||||
event,
|
||||
active.includes('LOCK'),
|
||||
active.includes('CTRL')
|
||||
);
|
||||
setPressed((pressed) => pressed.concat([keyLabel]));
|
||||
setActive((active) => active.concat([keyLabel]));
|
||||
|
||||
if (key === 'RESET') {
|
||||
apple2.reset();
|
||||
@ -153,8 +160,8 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
|
||||
return;
|
||||
}
|
||||
const { key, keyLabel } = mapKeyboardEvent(event);
|
||||
setPressed(pressed => pressed.filter(k => k !== keyLabel));
|
||||
setActive(active => active.filter(k => k !== keyLabel));
|
||||
setPressed((pressed) => pressed.filter((k) => k !== keyLabel));
|
||||
setActive((active) => active.filter((k) => k !== keyLabel));
|
||||
|
||||
const io = apple2.getIO();
|
||||
if (key === 'OPEN_APPLE') {
|
||||
@ -186,7 +193,7 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
|
||||
setActive([...active, key]);
|
||||
return true;
|
||||
}
|
||||
setActive(active.filter(x => x !== key));
|
||||
setActive(active.filter((x) => x !== key));
|
||||
return false;
|
||||
};
|
||||
|
||||
@ -237,12 +244,12 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
|
||||
true
|
||||
);
|
||||
apple2?.getIO().keyUp();
|
||||
setPressed(pressed.filter(x => x !== keyLabel));
|
||||
setPressed(pressed.filter((x) => x !== keyLabel));
|
||||
},
|
||||
[apple2, active, pressed]
|
||||
);
|
||||
|
||||
const bindKey = ([lower, upper]: [string, string]) =>
|
||||
const bindKey = ([lower, upper]: [string, string]) => (
|
||||
<Key
|
||||
lower={lower}
|
||||
upper={upper}
|
||||
@ -250,17 +257,14 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
|
||||
pressed={pressed.includes(lower)}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
/>;
|
||||
/>
|
||||
);
|
||||
|
||||
const rows = keys.map((row, idx) =>
|
||||
const rows = keys.map((row, idx) => (
|
||||
<div key={idx} className={cs(styles.row, styles[`row${idx}`])}>
|
||||
{row.map(bindKey)}
|
||||
</div>
|
||||
);
|
||||
));
|
||||
|
||||
return (
|
||||
<div className={styles.keyboard}>
|
||||
{rows}
|
||||
</div>
|
||||
);
|
||||
return <div className={styles.keyboard}>{rows}</div>;
|
||||
};
|
||||
|
@ -15,11 +15,7 @@ import { ControlButton } from './ControlButton';
|
||||
* @returns ModalOverlay component
|
||||
*/
|
||||
export const ModalOverlay = ({ children }: { children: ComponentChildren }) => {
|
||||
return (
|
||||
<div className={styles.modalOverlay}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div className={styles.modalOverlay}>{children}</div>;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -28,11 +24,7 @@ export const ModalOverlay = ({ children }: { children: ComponentChildren }) => {
|
||||
* @returns ModalContent component
|
||||
*/
|
||||
export const ModalContent = ({ children }: { children: ComponentChildren }) => {
|
||||
return (
|
||||
<div className={styles.modalContent}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div className={styles.modalContent}>{children}</div>;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -66,9 +58,7 @@ export const ModalCloseButton = ({ onClose }: ModalCloseButtonProp) => {
|
||||
const doClose = useCallback(() => onClose(true), [onClose]);
|
||||
useHotKey('Escape', doClose);
|
||||
|
||||
return (
|
||||
<ControlButton onClick={doClose} title="Close" icon="xmark" />
|
||||
);
|
||||
return <ControlButton onClick={doClose} title="Close" icon="xmark" />;
|
||||
};
|
||||
|
||||
type OnCloseCallback = (closeBox?: boolean) => void;
|
||||
@ -94,8 +84,7 @@ export const ModalHeader = ({ onClose, title, icon }: ModalHeaderProps) => {
|
||||
return (
|
||||
<div role="banner" className={styles.modalHeader}>
|
||||
<span className={styles.modalTitle}>
|
||||
{icon && <i className={`fa-solid fa-${icon}`} role="img" />}
|
||||
{' '}
|
||||
{icon && <i className={`fa-solid fa-${icon}`} role="img" />}{' '}
|
||||
{title}
|
||||
</span>
|
||||
{onClose && <ModalCloseButton onClose={onClose} />}
|
||||
@ -124,20 +113,19 @@ export interface ModalProps {
|
||||
* @param onClose Close callback
|
||||
* @returns Modal component
|
||||
*/
|
||||
export const Modal = ({
|
||||
isOpen,
|
||||
children,
|
||||
title,
|
||||
...props
|
||||
}: ModalProps) => {
|
||||
return (
|
||||
isOpen ? createPortal((
|
||||
<ModalOverlay>
|
||||
<div className={cs(styles.modal, componentStyles.components)} role="dialog">
|
||||
{title && <ModalHeader title={title} {...props} />}
|
||||
{children}
|
||||
</div>
|
||||
</ModalOverlay>
|
||||
), document.body) : null
|
||||
);
|
||||
export const Modal = ({ isOpen, children, title, ...props }: ModalProps) => {
|
||||
return isOpen
|
||||
? createPortal(
|
||||
<ModalOverlay>
|
||||
<div
|
||||
className={cs(styles.modal, componentStyles.components)}
|
||||
role="dialog"
|
||||
>
|
||||
{title && <ModalHeader title={title} {...props} />}
|
||||
{children}
|
||||
</div>
|
||||
</ModalOverlay>,
|
||||
document.body
|
||||
)
|
||||
: null;
|
||||
};
|
||||
|
@ -33,17 +33,13 @@ const Boolean = ({ option, value, setValue }: BooleanProps) => {
|
||||
const { label, name } = option;
|
||||
const onChange = useCallback(
|
||||
(event: JSX.TargetedMouseEvent<HTMLInputElement>) =>
|
||||
setValue(name, event.currentTarget.checked)
|
||||
, [name, setValue]
|
||||
setValue(name, event.currentTarget.checked),
|
||||
[name, setValue]
|
||||
);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<input type="checkbox" checked={value} onChange={onChange} />
|
||||
<label>{label}</label>
|
||||
</li>
|
||||
);
|
||||
@ -84,9 +80,7 @@ const Select = ({ option, value, setValue }: SelectProps) => {
|
||||
|
||||
return (
|
||||
<li>
|
||||
<select onChange={onChange}>
|
||||
{option.values.map(makeOption)}
|
||||
</select>
|
||||
<select onChange={onChange}>{option.values.map(makeOption)}</select>
|
||||
<label>{label}</label>
|
||||
</li>
|
||||
);
|
||||
@ -110,9 +104,12 @@ export interface OptionsModalProps {
|
||||
export const OptionsModal = ({ isOpen, onClose }: OptionsModalProps) => {
|
||||
const options = useContext(OptionsContext);
|
||||
const sections = options.getSections();
|
||||
const setValue = useCallback((name: string, value: string | boolean) => {
|
||||
options.setOption(name, value);
|
||||
}, [options]);
|
||||
const setValue = useCallback(
|
||||
(name: string, value: string | boolean) => {
|
||||
options.setOption(name, value);
|
||||
},
|
||||
[options]
|
||||
);
|
||||
|
||||
const makeOption = (option: Option) => {
|
||||
const { name, type } = option;
|
||||
@ -143,9 +140,7 @@ export const OptionsModal = ({ isOpen, onClose }: OptionsModalProps) => {
|
||||
return (
|
||||
<>
|
||||
<h3>{section.name}</h3>
|
||||
<ul>
|
||||
{section.options.map(makeOption)}
|
||||
</ul>
|
||||
<ul>{section.options.map(makeOption)}</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { h, Fragment } from 'preact';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'preact/hooks';
|
||||
import Apple2IO, { slot } from 'js/apple2io';
|
||||
import Parallel, { ParallelOptions } from 'js/cards/parallel';
|
||||
import { Modal, ModalContent, ModalFooter } from './Modal';
|
||||
@ -20,32 +26,35 @@ export const Printer = ({ io, slot }: PrinterProps) => {
|
||||
const rawLength = useRef(0);
|
||||
const [href, setHref] = useState('');
|
||||
|
||||
const cbs = useMemo<ParallelOptions>(() => ({
|
||||
putChar: (val: byte) => {
|
||||
const ascii = val & 0x7f;
|
||||
const visible = val >= 0x20;
|
||||
const char = String.fromCharCode(ascii);
|
||||
const cbs = useMemo<ParallelOptions>(
|
||||
() => ({
|
||||
putChar: (val: byte) => {
|
||||
const ascii = val & 0x7f;
|
||||
const visible = val >= 0x20;
|
||||
const char = String.fromCharCode(ascii);
|
||||
|
||||
if (char === '\r') {
|
||||
// Skip for once
|
||||
} else if (char === '\t') {
|
||||
// possibly not right due to tab stops
|
||||
setContent((content) => content += ' ');
|
||||
} else if (ascii === 0x04) {
|
||||
setContent((content) => content = content.slice(0, -1));
|
||||
return;
|
||||
} else if (visible) {
|
||||
setContent((content) => content += char);
|
||||
}
|
||||
if (char === '\r') {
|
||||
// Skip for once
|
||||
} else if (char === '\t') {
|
||||
// possibly not right due to tab stops
|
||||
setContent((content) => (content += ' '));
|
||||
} else if (ascii === 0x04) {
|
||||
setContent((content) => (content = content.slice(0, -1)));
|
||||
return;
|
||||
} else if (visible) {
|
||||
setContent((content) => (content += char));
|
||||
}
|
||||
|
||||
raw.current[rawLength.current++] = val;
|
||||
if (rawLength.current > raw.current.length) {
|
||||
const newRaw = new Uint8Array(raw.current.length * 2);
|
||||
newRaw.set(raw.current);
|
||||
raw.current = newRaw;
|
||||
}
|
||||
}
|
||||
}), [rawLength]);
|
||||
raw.current[rawLength.current++] = val;
|
||||
if (rawLength.current > raw.current.length) {
|
||||
const newRaw = new Uint8Array(raw.current.length * 2);
|
||||
newRaw.set(raw.current);
|
||||
raw.current = newRaw;
|
||||
}
|
||||
},
|
||||
}),
|
||||
[rawLength]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (io) {
|
||||
@ -56,10 +65,9 @@ export const Printer = ({ io, slot }: PrinterProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const blob = new Blob(
|
||||
[raw.current.slice(0, rawLength.current)],
|
||||
{ type: 'application/octet-stream' }
|
||||
);
|
||||
const blob = new Blob([raw.current.slice(0, rawLength.current)], {
|
||||
type: 'application/octet-stream',
|
||||
});
|
||||
const href = window.URL.createObjectURL(blob);
|
||||
setHref(href);
|
||||
}
|
||||
@ -80,7 +88,7 @@ export const Printer = ({ io, slot }: PrinterProps) => {
|
||||
<>
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Printer">
|
||||
<ModalContent>
|
||||
<pre className={styles.printer} tabIndex={-1} >
|
||||
<pre className={styles.printer} tabIndex={-1}>
|
||||
{content}
|
||||
</pre>
|
||||
</ModalContent>
|
||||
|
@ -17,7 +17,9 @@ export const ProgressModal = ({ title, current, total }: ErrorProps) => {
|
||||
<div className={styles.progressContainer}>
|
||||
<div
|
||||
className={styles.progressBar}
|
||||
style={{ width: Math.floor(320 * (current / total)) }}
|
||||
style={{
|
||||
width: Math.floor(320 * (current / total)),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ModalContent>
|
||||
|
@ -9,11 +9,7 @@ export interface TabProps {
|
||||
}
|
||||
|
||||
export const Tab = ({ children }: TabProps) => {
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div>{children}</div>;
|
||||
};
|
||||
|
||||
interface TabWrapperProps {
|
||||
@ -24,7 +20,10 @@ interface TabWrapperProps {
|
||||
|
||||
const TabWrapper = ({ children, onClick, selected }: TabWrapperProps) => {
|
||||
return (
|
||||
<div onClick={onClick} className={cs(styles.tab, { [styles.selected]: selected })}>
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={cs(styles.tab, { [styles.selected]: selected })}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@ -38,10 +37,13 @@ export interface TabsProps {
|
||||
export const Tabs = ({ children, setSelected }: TabsProps) => {
|
||||
const [innerSelected, setInnerSelected] = useState(0);
|
||||
|
||||
const innerSetSelected = useCallback((idx: number) => {
|
||||
setSelected(idx);
|
||||
setInnerSelected(idx);
|
||||
}, [setSelected]);
|
||||
const innerSetSelected = useCallback(
|
||||
(idx: number) => {
|
||||
setSelected(idx);
|
||||
setInnerSelected(idx);
|
||||
},
|
||||
[setSelected]
|
||||
);
|
||||
|
||||
if (!Array.isArray(children)) {
|
||||
return null;
|
||||
@ -49,7 +51,7 @@ export const Tabs = ({ children, setSelected }: TabsProps) => {
|
||||
|
||||
return (
|
||||
<div className={styles.tabs}>
|
||||
{children.map((child, idx) =>
|
||||
{children.map((child, idx) => (
|
||||
<TabWrapper
|
||||
key={idx}
|
||||
onClick={() => innerSetSelected(idx)}
|
||||
@ -57,7 +59,7 @@ export const Tabs = ({ children, setSelected }: TabsProps) => {
|
||||
>
|
||||
{child}
|
||||
</TabWrapper>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -9,7 +9,7 @@ export interface VideotermProps {
|
||||
slot: slot;
|
||||
}
|
||||
|
||||
export const Videoterm = ({ io, slot }: VideotermProps ) => {
|
||||
export const Videoterm = ({ io, slot }: VideotermProps) => {
|
||||
useEffect(() => {
|
||||
if (io) {
|
||||
const videoterm = new VideotermImpl();
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
.diskLight {
|
||||
margin: 5px;
|
||||
background-image: url("../../../css/green-off-16.png");
|
||||
background-image: url('../../../css/green-off-16.png');
|
||||
background-size: 16px 16px;
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
@ -14,7 +14,7 @@
|
||||
}
|
||||
|
||||
.diskLight.on {
|
||||
background-image: url("../../../css/green-on-16.png");
|
||||
background-image: url('../../../css/green-on-16.png');
|
||||
}
|
||||
|
||||
.diskLabel {
|
||||
@ -29,12 +29,12 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media only screen and (min-resolution: 1.25dppx) {
|
||||
@media only screen and (resolution >= 1.25dppx) {
|
||||
.diskLight {
|
||||
background-image: url("../../../css/green-off-32.png");
|
||||
background-image: url('../../../css/green-off-32.png');
|
||||
}
|
||||
|
||||
.diskLight.on {
|
||||
background-image: url("../../../css/green-on-32.png");
|
||||
background-image: url('../../../css/green-on-32.png');
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,13 @@
|
||||
font-size: 14px;
|
||||
font-family: sans-serif;
|
||||
|
||||
a[role="button"] {
|
||||
a[role='button'] {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button,
|
||||
a[role="button"],
|
||||
input[type="file"]::file-selector-button {
|
||||
a[role='button'],
|
||||
input[type='file']::file-selector-button {
|
||||
background: #44372c;
|
||||
color: #fff;
|
||||
padding: 2px 8px;
|
||||
@ -24,26 +24,26 @@
|
||||
}
|
||||
|
||||
button:hover,
|
||||
a[role="button"]:hover,
|
||||
input[type="file"]::file-selector-button {
|
||||
a[role='button']:hover,
|
||||
input[type='file']::file-selector-button {
|
||||
background-color: #55473d;
|
||||
border: 1px outset #66594e;
|
||||
}
|
||||
|
||||
button:active,
|
||||
a[role="button"]:active,
|
||||
input[type="file"]::file-selector-button {
|
||||
a[role='button']:active,
|
||||
input[type='file']::file-selector-button {
|
||||
background-color: #22150a;
|
||||
border: 1px outset #44372c;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
a[role="button"]:focus,
|
||||
input[type="file"]::file-selector-button {
|
||||
a[role='button']:focus,
|
||||
input[type='file']::file-selector-button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
input[type='checkbox'] {
|
||||
appearance: none;
|
||||
background-color: #65594d;
|
||||
border: 1px inset #65594d;
|
||||
@ -54,14 +54,14 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked {
|
||||
input[type='checkbox']:checked {
|
||||
background-color: #65594d;
|
||||
border: 1px inset #65594d;
|
||||
color: #0d0;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked::after {
|
||||
content: "\2716";
|
||||
input[type='checkbox']:checked::after {
|
||||
content: '\2716';
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -8,7 +8,9 @@
|
||||
/* border: 5px outset #66594E; */
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
font: 9px Helvetica, sans-serif;
|
||||
font:
|
||||
9px Helvetica,
|
||||
sans-serif;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
margin-left: 10px;
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
.diskLight {
|
||||
margin: 5px;
|
||||
background-image: url("../../../css/red-off-16.png");
|
||||
background-image: url('../../../css/red-off-16.png');
|
||||
background-size: 16px 16px;
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
@ -14,7 +14,7 @@
|
||||
}
|
||||
|
||||
.diskLight.on {
|
||||
background-image: url("../../../css/red-on-16.png");
|
||||
background-image: url('../../../css/red-on-16.png');
|
||||
}
|
||||
|
||||
.diskLabel {
|
||||
@ -29,12 +29,12 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media only screen and (min-resolution: 1.25dppx) {
|
||||
@media only screen and (resolution >= 1.25dppx) {
|
||||
.diskLight {
|
||||
background-image: url("../../../css/red-off-32.png");
|
||||
background-image: url('../../../css/red-off-32.png');
|
||||
}
|
||||
|
||||
.diskLight.on {
|
||||
background-image: url("../../../css/red-on-32.png");
|
||||
background-image: url('../../../css/red-on-32.png');
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@
|
||||
margin: 0;
|
||||
padding: 3px 0 0 10;
|
||||
color: black;
|
||||
font-family: "Adobe Garamond Pro", Garamond, Times, serif;
|
||||
font-family: 'Adobe Garamond Pro', Garamond, Times, serif;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
user-select: none;
|
||||
|
@ -177,7 +177,7 @@
|
||||
}
|
||||
|
||||
.key-OPEN_APPLE div {
|
||||
background-image: url("../../../img/open-apple24.png");
|
||||
background-image: url('../../../img/open-apple24.png');
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
bottom: 1px;
|
||||
@ -185,7 +185,7 @@
|
||||
}
|
||||
|
||||
.key-CLOSED_APPLE div {
|
||||
background-image: url("../../../img/closed-apple24.png");
|
||||
background-image: url('../../../img/closed-apple24.png');
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
bottom: 1px;
|
||||
@ -201,9 +201,9 @@
|
||||
}
|
||||
|
||||
.key-OPEN_APPLE.active div {
|
||||
background-image: url("../../../img/open-apple24-green.png");
|
||||
background-image: url('../../../img/open-apple24-green.png');
|
||||
}
|
||||
|
||||
.key-CLOSED_APPLE.active div {
|
||||
background-image: url("../../../img/closed-apple24-green.png");
|
||||
background-image: url('../../../img/closed-apple24-green.png');
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
.modalOverlay {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -63,7 +60,7 @@
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
|
||||
a[role="button"],
|
||||
a[role='button'],
|
||||
button {
|
||||
margin: 0 0 0 5px;
|
||||
min-width: 75px;
|
||||
|
@ -1,5 +1,5 @@
|
||||
:global(.mono) {
|
||||
filter: url("#green");
|
||||
filter: url('#green');
|
||||
}
|
||||
|
||||
.display {
|
||||
@ -14,10 +14,7 @@
|
||||
padding: 0;
|
||||
border: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
inset: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: auto !important;
|
||||
@ -48,31 +45,26 @@
|
||||
:global(.scanlines)::after {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
transparent 1px,
|
||||
rgb(0 0 0 / 50%) 1px,
|
||||
rgb(0 0 0 / 50%) 2px
|
||||
);
|
||||
content: "";
|
||||
background-image: repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
transparent 1px,
|
||||
rgb(0 0 0 / 50%) 1px,
|
||||
rgb(0 0 0 / 50%) 2px
|
||||
);
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
:global(.full-page) :global(.scanlines)::after {
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
transparent 0.25vh,
|
||||
rgb(0 0 0 / 50%) 0.25vh,
|
||||
rgb(0 0 0 / 50%) 0.5vh
|
||||
);
|
||||
background-image: repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
transparent 0.25vh,
|
||||
rgb(0 0 0 / 50%) 0.25vh,
|
||||
rgb(0 0 0 / 50%) 0.5vh
|
||||
);
|
||||
}
|
||||
|
||||
.screen {
|
||||
|
@ -42,12 +42,23 @@ const formatArray = (value: unknown): string => {
|
||||
const Variable = ({ variable }: { variable: ApplesoftVariable }) => {
|
||||
const { name, type, sizes, value } = variable;
|
||||
const isArray = !!sizes;
|
||||
const arrayStr = isArray ? `(${sizes.map((size) => size - 1).join(',')})` : '';
|
||||
const arrayStr = isArray
|
||||
? `(${sizes.map((size) => size - 1).join(',')})`
|
||||
: '';
|
||||
return (
|
||||
<tr>
|
||||
<td>{name}{TYPE_SYMBOL[type]}{arrayStr}</td>
|
||||
<td>{TYPE_NAME[type]}{isArray ? ' Array' : ''}</td>
|
||||
<td><pre tabIndex={-1}>{isArray ? formatArray(value) : value}</pre></td>
|
||||
<td>
|
||||
{name}
|
||||
{TYPE_SYMBOL[type]}
|
||||
{arrayStr}
|
||||
</td>
|
||||
<td>
|
||||
{TYPE_NAME[type]}
|
||||
{isArray ? ' Array' : ''}
|
||||
</td>
|
||||
<td>
|
||||
<pre tabIndex={-1}>{isArray ? formatArray(value) : value}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
@ -57,7 +68,7 @@ export const Applesoft = ({ apple2 }: ApplesoftProps) => {
|
||||
const [data, setData] = useState<ApplesoftData>({
|
||||
listing: '',
|
||||
variables: [],
|
||||
internals: {}
|
||||
internals: {},
|
||||
});
|
||||
const [heap, setHeap] = useState<ApplesoftHeap>();
|
||||
const cpu = apple2?.getCPU();
|
||||
@ -72,18 +83,19 @@ export const Applesoft = ({ apple2 }: ApplesoftProps) => {
|
||||
const animate = useCallback(() => {
|
||||
if (cpu && heap) {
|
||||
try {
|
||||
const decompiler = ApplesoftDecompiler.decompilerFromMemory(cpu);
|
||||
const decompiler =
|
||||
ApplesoftDecompiler.decompilerFromMemory(cpu);
|
||||
setData({
|
||||
variables: heap.dumpVariables(),
|
||||
internals: heap.dumpInternals(),
|
||||
listing: decompiler.decompile()
|
||||
listing: decompiler.decompile(),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
setData({
|
||||
variables: [],
|
||||
internals: {},
|
||||
listing: error.message
|
||||
listing: error.message,
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
@ -103,7 +115,9 @@ export const Applesoft = ({ apple2 }: ApplesoftProps) => {
|
||||
return (
|
||||
<div className={styles.column}>
|
||||
<span className={debuggerStyles.subHeading}>Listing</span>
|
||||
<pre className={styles.listing} tabIndex={-1}>{listing}</pre>
|
||||
<pre className={styles.listing} tabIndex={-1}>
|
||||
{listing}
|
||||
</pre>
|
||||
<span className={debuggerStyles.subHeading}>Variables</span>
|
||||
<div className={styles.variables}>
|
||||
<table>
|
||||
@ -112,7 +126,9 @@ export const Applesoft = ({ apple2 }: ApplesoftProps) => {
|
||||
<th>Type</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
{variables.map((variable, idx) => <Variable key={idx} variable={variable} />)}
|
||||
{variables.map((variable, idx) => (
|
||||
<Variable key={idx} variable={variable} />
|
||||
))}
|
||||
</table>
|
||||
</div>
|
||||
<span className={debuggerStyles.subHeading}>Internals</span>
|
||||
|
@ -60,7 +60,7 @@ export const CPU = ({ apple2 }: CPUProps) => {
|
||||
stack: debug.getStack(38),
|
||||
trace: debug.getTrace(16),
|
||||
zeroPage: debug.dumpPage(0),
|
||||
memory: debug.dumpPage(parseInt(memoryPage, 16) || 0)
|
||||
memory: debug.dumpPage(parseInt(memoryPage, 16) || 0),
|
||||
});
|
||||
}
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
@ -83,46 +83,51 @@ export const CPU = ({ apple2 }: CPUProps) => {
|
||||
debug?.step();
|
||||
}, [debug]);
|
||||
|
||||
const doLoadAddress = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
|
||||
setLoadAddress(event.currentTarget.value);
|
||||
}, []);
|
||||
const doRunCheck = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
|
||||
setRun(event.currentTarget.checked);
|
||||
}, []);
|
||||
const doLoadAddress = useCallback(
|
||||
(event: JSX.TargetedEvent<HTMLInputElement>) => {
|
||||
setLoadAddress(event.currentTarget.value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const doRunCheck = useCallback(
|
||||
(event: JSX.TargetedEvent<HTMLInputElement>) => {
|
||||
setRun(event.currentTarget.checked);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const doMemoryPage = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
|
||||
setMemoryPage(event.currentTarget.value);
|
||||
}, []);
|
||||
const doMemoryPage = useCallback(
|
||||
(event: JSX.TargetedEvent<HTMLInputElement>) => {
|
||||
setMemoryPage(event.currentTarget.value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const doChooseFile = useCallback((handles: FileSystemFileHandle[]) => {
|
||||
if (debug && handles.length === 1) {
|
||||
spawn(async () => {
|
||||
const file = await handles[0].getFile();
|
||||
let atAddress = parseInt(loadAddress, 16) || 0x800;
|
||||
const doChooseFile = useCallback(
|
||||
(handles: FileSystemFileHandle[]) => {
|
||||
if (debug && handles.length === 1) {
|
||||
spawn(async () => {
|
||||
const file = await handles[0].getFile();
|
||||
let atAddress = parseInt(loadAddress, 16) || 0x800;
|
||||
|
||||
const matches = file.name.match(CIDERPRESS_EXTENSION);
|
||||
if (matches && matches.length === 3) {
|
||||
const [, , aux] = matches;
|
||||
atAddress = parseInt(aux, 16);
|
||||
}
|
||||
const matches = file.name.match(CIDERPRESS_EXTENSION);
|
||||
if (matches && matches.length === 3) {
|
||||
const [, , aux] = matches;
|
||||
atAddress = parseInt(aux, 16);
|
||||
}
|
||||
|
||||
await loadLocalBinaryFile(file, atAddress, debug);
|
||||
setLoadAddress(toHex(atAddress, 4));
|
||||
if (run) {
|
||||
debug?.runAt(atAddress);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [debug, loadAddress, run]);
|
||||
await loadLocalBinaryFile(file, atAddress, debug);
|
||||
setLoadAddress(toHex(atAddress, 4));
|
||||
if (run) {
|
||||
debug?.runAt(atAddress);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
[debug, loadAddress, run]
|
||||
);
|
||||
|
||||
const {
|
||||
memory,
|
||||
registers,
|
||||
running,
|
||||
stack,
|
||||
trace,
|
||||
zeroPage
|
||||
} = data;
|
||||
const { memory, registers, running, stack, trace, zeroPage } = data;
|
||||
|
||||
const memoryPageValid = VALID_PAGE.test(memoryPage);
|
||||
const loadAddressValid = VALID_ADDRESS.test(loadAddress);
|
||||
@ -156,9 +161,7 @@ export const CPU = ({ apple2 }: CPUProps) => {
|
||||
<div className={debuggerStyles.row}>
|
||||
<div className={debuggerStyles.column}>
|
||||
<span className={debuggerStyles.subHeading}>Registers</span>
|
||||
<pre tabIndex={-1}>
|
||||
{registers}
|
||||
</pre>
|
||||
<pre tabIndex={-1}>{registers}</pre>
|
||||
<span className={debuggerStyles.subHeading}>Trace</span>
|
||||
<pre className={styles.trace} tabIndex={-1}>
|
||||
{trace}
|
||||
@ -177,7 +180,9 @@ export const CPU = ({ apple2 }: CPUProps) => {
|
||||
</div>
|
||||
<div>
|
||||
<hr />
|
||||
<span className={debuggerStyles.subHeading}>Memory Page: $ </span>
|
||||
<span className={debuggerStyles.subHeading}>
|
||||
Memory Page: ${' '}
|
||||
</span>
|
||||
<input
|
||||
value={memoryPage}
|
||||
onChange={doMemoryPage}
|
||||
@ -199,9 +204,9 @@ export const CPU = ({ apple2 }: CPUProps) => {
|
||||
onChange={doLoadAddress}
|
||||
className={cs({ [styles.invalid]: !loadAddressValid })}
|
||||
/>
|
||||
{loadAddressValid ? null : ERROR_ICON}
|
||||
{' '}
|
||||
<input type="checkbox" checked={run} onChange={doRunCheck} />Run
|
||||
{loadAddressValid ? null : ERROR_ICON}{' '}
|
||||
<input type="checkbox" checked={run} onChange={doRunCheck} />
|
||||
Run
|
||||
<div className={styles.fileChooser}>
|
||||
<FileChooser onChange={doChooseFile} />
|
||||
</div>
|
||||
|
@ -2,7 +2,15 @@ import { h, Fragment } from 'preact';
|
||||
import { useMemo } from 'preact/hooks';
|
||||
import cs from 'classnames';
|
||||
import { Apple2 as Apple2Impl } from 'js/apple2';
|
||||
import { BlockDisk, DiskFormat, DriveNumber, FloppyDisk, isBlockDiskFormat, isNibbleDisk, MassStorage } from 'js/formats/types';
|
||||
import {
|
||||
BlockDisk,
|
||||
DiskFormat,
|
||||
DriveNumber,
|
||||
FloppyDisk,
|
||||
isBlockDiskFormat,
|
||||
isNibbleDisk,
|
||||
MassStorage,
|
||||
} from 'js/formats/types';
|
||||
import { slot } from 'js/apple2io';
|
||||
import DiskII from 'js/cards/disk2';
|
||||
import SmartPort from 'js/cards/smartport';
|
||||
@ -18,7 +26,11 @@ import { toHex } from 'js/util';
|
||||
import styles from './css/Disks.module.scss';
|
||||
import debuggerStyles from './css/Debugger.module.scss';
|
||||
import { useCallback, useState } from 'preact/hooks';
|
||||
import { DOS33, FileEntry as DOSEntry, isMaybeDOS33 } from 'js/formats/dos/dos33';
|
||||
import {
|
||||
DOS33,
|
||||
FileEntry as DOSEntry,
|
||||
isMaybeDOS33,
|
||||
} from 'js/formats/dos/dos33';
|
||||
import createDiskFromDOS from 'js/formats/do';
|
||||
import { FileData, FileViewer } from './FileViewer';
|
||||
|
||||
@ -29,7 +41,10 @@ import { FileData, FileViewer } from './FileViewer';
|
||||
* @returns Short string date
|
||||
*/
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'short' });
|
||||
return date.toLocaleString(undefined, {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'short',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -39,7 +54,7 @@ const formatDate = (date: Date) => {
|
||||
* @returns true if is BlockDisk
|
||||
*/
|
||||
function isBlockDisk(disk: FloppyDisk | BlockDisk): disk is BlockDisk {
|
||||
return !!((disk as BlockDisk).blocks);
|
||||
return !!(disk as BlockDisk).blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -80,14 +95,17 @@ const FileListing = ({ depth, fileEntry, setFileData }: FileListingProps) => {
|
||||
onClick={doSetFileData}
|
||||
>
|
||||
{'| '.repeat(depth)}
|
||||
{deleted ?
|
||||
<i className="fas fa-file-circle-xmark" /> :
|
||||
{deleted ? (
|
||||
<i className="fas fa-file-circle-xmark" />
|
||||
) : (
|
||||
<i className="fas fa-file" />
|
||||
}
|
||||
{' '}
|
||||
)}{' '}
|
||||
{fileEntry.name}
|
||||
</td>
|
||||
<td>{FILE_TYPES[fileEntry.fileType] ?? `$${toHex(fileEntry.fileType)}`}</td>
|
||||
<td>
|
||||
{FILE_TYPES[fileEntry.fileType] ??
|
||||
`$${toHex(fileEntry.fileType)}`}
|
||||
</td>
|
||||
<td>{`$${toHex(fileEntry.auxType, 4)}`}</td>
|
||||
<td>{fileEntry.blocksUsed}</td>
|
||||
<td>{formatDate(fileEntry.creation)}</td>
|
||||
@ -115,7 +133,12 @@ interface DirectoryListingProps {
|
||||
* @param dirEntry Current directory entry to display
|
||||
* @returns DirectoryListing component
|
||||
*/
|
||||
const DirectoryListing = ({ volume, depth, dirEntry, setFileData }: DirectoryListingProps) => {
|
||||
const DirectoryListing = ({
|
||||
volume,
|
||||
depth,
|
||||
dirEntry,
|
||||
setFileData,
|
||||
}: DirectoryListingProps) => {
|
||||
const [open, setOpen] = useState(depth === 0);
|
||||
return (
|
||||
<>
|
||||
@ -126,8 +149,12 @@ const DirectoryListing = ({ volume, depth, dirEntry, setFileData }: DirectoryLis
|
||||
title={dirEntry.name}
|
||||
>
|
||||
{'| '.repeat(depth)}
|
||||
<i className={cs('fas', { 'fa-folder-open': open, 'fa-folder-closed': !open })} />
|
||||
{' '}
|
||||
<i
|
||||
className={cs('fas', {
|
||||
'fa-folder-open': open,
|
||||
'fa-folder-closed': !open,
|
||||
})}
|
||||
/>{' '}
|
||||
{dirEntry.name}
|
||||
</td>
|
||||
<td></td>
|
||||
@ -136,26 +163,31 @@ const DirectoryListing = ({ volume, depth, dirEntry, setFileData }: DirectoryLis
|
||||
<td>{formatDate(dirEntry.creation)}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{open && dirEntry.entries.map((fileEntry, idx) => {
|
||||
if (fileEntry.storageType === STORAGE_TYPES.DIRECTORY) {
|
||||
const dirEntry = new Directory(volume, fileEntry);
|
||||
return <DirectoryListing
|
||||
key={idx}
|
||||
depth={depth + 1}
|
||||
volume={volume}
|
||||
dirEntry={dirEntry}
|
||||
setFileData={setFileData}
|
||||
/>;
|
||||
} else {
|
||||
return <FileListing
|
||||
key={idx}
|
||||
depth={depth + 1}
|
||||
volume={volume}
|
||||
fileEntry={fileEntry}
|
||||
setFileData={setFileData}
|
||||
/>;
|
||||
}
|
||||
})}
|
||||
{open &&
|
||||
dirEntry.entries.map((fileEntry, idx) => {
|
||||
if (fileEntry.storageType === STORAGE_TYPES.DIRECTORY) {
|
||||
const dirEntry = new Directory(volume, fileEntry);
|
||||
return (
|
||||
<DirectoryListing
|
||||
key={idx}
|
||||
depth={depth + 1}
|
||||
volume={volume}
|
||||
dirEntry={dirEntry}
|
||||
setFileData={setFileData}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FileListing
|
||||
key={idx}
|
||||
depth={depth + 1}
|
||||
volume={volume}
|
||||
fileEntry={fileEntry}
|
||||
setFileData={setFileData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -187,9 +219,12 @@ const CatalogEntry = ({ dos, fileEntry, setFileData }: CatalogEntryProps) => {
|
||||
|
||||
return (
|
||||
<tr onClick={doSetFileData}>
|
||||
<td className={cs(styles.filename, { [styles.deleted]: fileEntry.deleted })}>
|
||||
{fileEntry.locked && <i className="fas fa-lock" />}
|
||||
{' '}
|
||||
<td
|
||||
className={cs(styles.filename, {
|
||||
[styles.deleted]: fileEntry.deleted,
|
||||
})}
|
||||
>
|
||||
{fileEntry.locked && <i className="fas fa-lock" />}{' '}
|
||||
{fileEntry.name}
|
||||
</td>
|
||||
<td>{fileEntry.type}</td>
|
||||
@ -303,12 +338,20 @@ const DiskInfo = ({ massStorage, driveNo, setFileData }: DiskInfoProps) => {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={styles.filename}>Filename</th>
|
||||
<th className={styles.filename}>
|
||||
Filename
|
||||
</th>
|
||||
<th className={styles.type}>Type</th>
|
||||
<th className={styles.aux}>Aux</th>
|
||||
<th className={styles.blocks}>Blocks</th>
|
||||
<th className={styles.created}>Created</th>
|
||||
<th className={styles.modified}>Modified</th>
|
||||
<th className={styles.blocks}>
|
||||
Blocks
|
||||
</th>
|
||||
<th className={styles.created}>
|
||||
Created
|
||||
</th>
|
||||
<th className={styles.modified}>
|
||||
Modified
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -321,9 +364,13 @@ const DiskInfo = ({ massStorage, driveNo, setFileData }: DiskInfoProps) => {
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={1}>Blocks Free: {freeCount}</td>
|
||||
<td colSpan={1}>
|
||||
Blocks Free: {freeCount}
|
||||
</td>
|
||||
<td colSpan={3}>Used: {usedCount}</td>
|
||||
<td colSpan={2}>Total: {totalBlocks}</td>
|
||||
<td colSpan={2}>
|
||||
Total: {totalBlocks}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
@ -337,7 +384,9 @@ const DiskInfo = ({ massStorage, driveNo, setFileData }: DiskInfoProps) => {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={styles.filename}>Filename</th>
|
||||
<th className={styles.filename}>
|
||||
Filename
|
||||
</th>
|
||||
<th className={styles.type}>Type</th>
|
||||
<th className={styles.sectors}>Sectors</th>
|
||||
<th></th>
|
||||
@ -409,11 +458,19 @@ export const Disks = ({ apple2 }: DisksProps) => {
|
||||
<div className={debuggerStyles.subHeading}>
|
||||
{card.constructor.name} - 1
|
||||
</div>
|
||||
<DiskInfo massStorage={card} driveNo={1} setFileData={setFileData} />
|
||||
<DiskInfo
|
||||
massStorage={card}
|
||||
driveNo={1}
|
||||
setFileData={setFileData}
|
||||
/>
|
||||
<div className={debuggerStyles.subHeading}>
|
||||
{card.constructor.name} - 2
|
||||
</div>
|
||||
<DiskInfo massStorage={card} driveNo={2} setFileData={setFileData} />
|
||||
<DiskInfo
|
||||
massStorage={card}
|
||||
driveNo={2}
|
||||
setFileData={setFileData}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<FileViewer fileData={fileData} onClose={onClose} />
|
||||
|
@ -56,7 +56,14 @@ const HiresPreview = ({ binary }: { binary: Uint8Array }) => {
|
||||
vm.blit();
|
||||
}
|
||||
|
||||
return <canvas ref={canvasRef} width={560} height={384} className={styles.hiresPreview} />;
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={560}
|
||||
height={384}
|
||||
className={styles.hiresPreview}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -96,7 +103,14 @@ const DoubleHiresPreview = ({ binary }: { binary: Uint8Array }) => {
|
||||
vm.blit();
|
||||
}
|
||||
|
||||
return <canvas ref={canvasRef} width={560} height={384} className={styles.hiresPreview} />;
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={560}
|
||||
height={384}
|
||||
className={styles.hiresPreview}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -114,16 +128,14 @@ export const FileViewer = ({ fileData, onClose }: FileViewerProps) => {
|
||||
useEffect(() => {
|
||||
if (fileData) {
|
||||
const { binary, text } = fileData;
|
||||
const binaryBlob = new Blob(
|
||||
[binary],
|
||||
{ type: 'application/octet-stream' }
|
||||
);
|
||||
const binaryBlob = new Blob([binary], {
|
||||
type: 'application/octet-stream',
|
||||
});
|
||||
const binaryHref = window.URL.createObjectURL(binaryBlob);
|
||||
setBinaryHref(binaryHref);
|
||||
const textBlob = new Blob(
|
||||
[text],
|
||||
{ type: 'application/octet-stream' }
|
||||
);
|
||||
const textBlob = new Blob([text], {
|
||||
type: 'application/octet-stream',
|
||||
});
|
||||
const textHref = window.URL.createObjectURL(textBlob);
|
||||
setTextHref(textHref);
|
||||
}
|
||||
@ -142,7 +154,7 @@ export const FileViewer = ({ fileData, onClose }: FileViewerProps) => {
|
||||
<div className={styles.fileViewer}>
|
||||
<HiresPreview binary={binary} />
|
||||
<DoubleHiresPreview binary={binary} />
|
||||
<pre className={styles.textViewer} tabIndex={-1} >
|
||||
<pre className={styles.textViewer} tabIndex={-1}>
|
||||
{text}
|
||||
</pre>
|
||||
</div>
|
||||
|
@ -54,8 +54,8 @@ interface Banks {
|
||||
* @returns LC read/write state
|
||||
*/
|
||||
const calcLC = (mmu: MMU, altzp: boolean) => {
|
||||
const read = mmu.readbsr && (mmu.altzp === altzp);
|
||||
const write = mmu.writebsr && (mmu.altzp === altzp);
|
||||
const read = mmu.readbsr && mmu.altzp === altzp;
|
||||
const write = mmu.writebsr && mmu.altzp === altzp;
|
||||
return {
|
||||
read,
|
||||
write,
|
||||
@ -170,7 +170,7 @@ const calcLanguageCard = (card: LanguageCard): LC => {
|
||||
rom: {
|
||||
read: !card.readbsr,
|
||||
write: !card.writebsr,
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -207,16 +207,14 @@ interface LanguageCardMapProps {
|
||||
const LanguageCardMap = ({ lc, children }: LanguageCardMapProps) => {
|
||||
return (
|
||||
<div className={cs(styles.bank)}>
|
||||
<div className={cs(styles.lc, rw(lc))}>
|
||||
{children} LC
|
||||
</div>
|
||||
<div className={cs(styles.lc, rw(lc))}>{children} LC</div>
|
||||
<div className={styles.lcbanks}>
|
||||
<div className={cs(styles.lcbank, styles.lcbank0, rw(lc.bank0))}>
|
||||
<div
|
||||
className={cs(styles.lcbank, styles.lcbank0, rw(lc.bank0))}
|
||||
>
|
||||
Bank 0
|
||||
</div>
|
||||
<div className={cs(styles.lcbank, rw(lc.bank1))}>
|
||||
Bank 1
|
||||
</div>
|
||||
<div className={cs(styles.lcbank, rw(lc.bank1))}>Bank 1</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -238,10 +236,14 @@ const Legend = () => {
|
||||
<div className={cs(styles.write, styles.legend)}> </div> Write
|
||||
</div>
|
||||
<div>
|
||||
<div className={cs(styles.write, styles.read, styles.legend)}> </div> Read/Write
|
||||
<div className={cs(styles.write, styles.read, styles.legend)}>
|
||||