mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
04ae0327c2
This adds both the recommended TypeScript checks, plus the recommended TypeScript checks that require type checking. This latter addition means that eslint essentially has to compile all of the TypeScript in the project, causing it to be slower. This isn't much of a problem in VS Code because there's a lot of caching being done, but it's clearly slower when run on the commandline. All of the errors are either fixed or suppressed. Some errors are suppressed because fixing them would be too laborious for the little value gained. The eslint config is also slightly refactored to separate the strictly TypeScript checks from the JavaScript checks.
325 lines
9.8 KiB
TypeScript
325 lines
9.8 KiB
TypeScript
import { Card, byte, word, Restorable } from '../types';
|
|
import CPU6502, { CpuState } from '../cpu6502';
|
|
import { debug } from '../util';
|
|
import { rom } from '../roms/cards/mouse';
|
|
|
|
const CLAMP_MIN_LOW = 0x478;
|
|
const CLAMP_MAX_LOW = 0x4F8;
|
|
const CLAMP_MIN_HIGH = 0x578;
|
|
const CLAMP_MAX_HIGH = 0x5F8;
|
|
|
|
const X_LOW = 0x478;
|
|
const Y_LOW = 0x4F8;
|
|
const X_HIGH = 0x578;
|
|
const Y_HIGH = 0x5F8;
|
|
const STATUS = 0x778;
|
|
const MODE = 0x7F8;
|
|
|
|
const STATUS_DOWN = 0x80;
|
|
const STATUS_LAST = 0x40;
|
|
const STATUS_MOVED = 0x20;
|
|
const INT_SCREEN = 0x08;
|
|
const INT_PRESS = 0x04;
|
|
const INT_MOVE = 0x02;
|
|
|
|
const MODE_ON = 0x01;
|
|
const MODE_INT_MOVE = 0x02;
|
|
const MODE_INT_PRESS = 0x04;
|
|
const MODE_INT_VBL = 0x08;
|
|
|
|
/**
|
|
* Firmware routine offset pointers
|
|
*/
|
|
const ENTRIES = {
|
|
SET_MOUSE: 0x12,
|
|
SERVE_MOUSE: 0x13,
|
|
READ_MOUSE: 0x14,
|
|
CLEAR_MOUSE: 0x15,
|
|
POS_MOUSE: 0x16,
|
|
CLAMP_MOUSE: 0x17,
|
|
HOME_MOUSE: 0x18,
|
|
INIT_MOUSE: 0x19
|
|
};
|
|
|
|
interface MouseState {
|
|
clampXMin: word;
|
|
clampYMin: word;
|
|
clampXMax: word;
|
|
clampYMax: word;
|
|
x: word;
|
|
y: word;
|
|
mode: byte;
|
|
down: boolean;
|
|
lastDown: boolean;
|
|
lastX: word;
|
|
lastY: word;
|
|
serve: byte;
|
|
shouldIntMove: boolean;
|
|
shouldIntPress: boolean;
|
|
slot: byte;
|
|
}
|
|
|
|
export default class Mouse implements Card, Restorable<MouseState> {
|
|
/** Lowest mouse X */
|
|
private clampXMin: word = 0;
|
|
/** Lowest mouse Y */
|
|
private clampYMin: word = 0;
|
|
/** Highest mouse X */
|
|
private clampXMax: word = 0x3FF;
|
|
/** Highest mouse Y */
|
|
private clampYMax: word = 0x3FF;
|
|
/** Mouse X position */
|
|
private x: word = 0;
|
|
/** Mouse Y position */
|
|
private y: word = 0;
|
|
/** Mouse mode */
|
|
private mode: byte = 0;
|
|
/** Mouse button down state */
|
|
private down = false;
|
|
/** Last mouse button down state */
|
|
private lastDown = false;
|
|
/** Last mouse Y Position */
|
|
private lastX: word = 0;
|
|
/** Last mouse X position */
|
|
private lastY: word = 0;
|
|
/** Interrupt service flags */
|
|
private serve: byte = 0;
|
|
/** Move happened since last refresh */
|
|
private shouldIntMove = false;
|
|
/** Button press happened since last refresh */
|
|
private shouldIntPress = false;
|
|
/** Slot for screen hole indexing */
|
|
private slot = 0;
|
|
|
|
constructor(
|
|
private cpu: CPU6502,
|
|
private cbs: {
|
|
setMouse: (mouse: Mouse) => void;
|
|
mouseMode: (on: boolean) => void;
|
|
}
|
|
) {
|
|
this.cbs.setMouse(this);
|
|
}
|
|
|
|
ioSwitch(_off: byte, _val?: byte) {
|
|
return undefined;
|
|
}
|
|
|
|
read(_page: byte, off: byte) {
|
|
let state = this.cpu.getState();
|
|
|
|
const holeWrite = (addr: word, val: byte) => {
|
|
this.cpu.write(addr >> 8, (addr & 0xff) + this.slot, val);
|
|
};
|
|
|
|
const holeRead = (addr: word) => {
|
|
return this.cpu.read(addr >> 8, addr & 0xff);
|
|
};
|
|
|
|
const clearCarry = (state: CpuState) => {
|
|
state.s &= 0xFE;
|
|
return state;
|
|
};
|
|
|
|
if (this.cpu.getSync()) {
|
|
switch (off) {
|
|
case rom[ENTRIES.SET_MOUSE]:
|
|
{
|
|
this.mode = state.a;
|
|
this.cbs.mouseMode(!!(this.mode & MODE_ON));
|
|
state = clearCarry(state);
|
|
// debug(
|
|
// 'setMouse ',
|
|
// (_mode & MODE_ON ? 'Mouse on ' : 'Mouse off '),
|
|
// (_mode & MODE_INT_MOVE ? 'Move interrupt ' : '') +
|
|
// (_mode & MODE_INT_PRESS ? 'Move press ' : '') +
|
|
// (_mode & MODE_INT_VBL ? 'Move VBL ' : '')
|
|
// );
|
|
}
|
|
break;
|
|
case rom[ENTRIES.SERVE_MOUSE]:
|
|
// debug('serveMouse');
|
|
holeWrite(STATUS, this.serve);
|
|
state = clearCarry(state);
|
|
this.serve = 0;
|
|
break;
|
|
case rom[ENTRIES.READ_MOUSE]:
|
|
{
|
|
const moved = (this.lastX !== this.x) || (this.lastY !== this.y);
|
|
const status =
|
|
(this.down ? STATUS_DOWN : 0) |
|
|
(this.lastDown ? STATUS_LAST : 0) |
|
|
(moved ? STATUS_MOVED : 0);
|
|
const mouseXLow = this.x & 0xff;
|
|
const mouseYLow = this.y & 0xff;
|
|
const mouseXHigh = this.x >> 8;
|
|
const mouseYHigh = this.y >> 8;
|
|
|
|
// debug({ mouseXLow, mouseYLow, mouseXHigh, mouseYHigh });
|
|
|
|
holeWrite(X_LOW, mouseXLow);
|
|
holeWrite(Y_LOW, mouseYLow);
|
|
holeWrite(X_HIGH, mouseXHigh);
|
|
holeWrite(Y_HIGH, mouseYHigh);
|
|
holeWrite(STATUS, status);
|
|
holeWrite(MODE, this.mode);
|
|
|
|
this.lastDown = this.down;
|
|
this.lastX = this.x;
|
|
this.lastY = this.y;
|
|
|
|
state = clearCarry(state);
|
|
}
|
|
break;
|
|
case rom[ENTRIES.CLEAR_MOUSE]:
|
|
debug('clearMouse');
|
|
state = clearCarry(state);
|
|
break;
|
|
case rom[ENTRIES.POS_MOUSE]:
|
|
debug('posMouse');
|
|
state = clearCarry(state);
|
|
break;
|
|
case rom[ENTRIES.CLAMP_MOUSE]:
|
|
{
|
|
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);
|
|
} 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);
|
|
}
|
|
state = clearCarry(state);
|
|
}
|
|
break;
|
|
case rom[ENTRIES.HOME_MOUSE]:
|
|
{
|
|
debug('homeMouse');
|
|
this.x = this.clampXMin;
|
|
this.y = this.clampYMin;
|
|
state = clearCarry(state);
|
|
}
|
|
break;
|
|
case rom[ENTRIES.INIT_MOUSE]:
|
|
{
|
|
this.slot = state.y >> 4;
|
|
debug('initMouse slot', this.slot);
|
|
state = clearCarry(state);
|
|
}
|
|
break;
|
|
}
|
|
|
|
this.cpu.setState(state);
|
|
}
|
|
|
|
return rom[off];
|
|
}
|
|
|
|
write() {
|
|
// not writable
|
|
}
|
|
|
|
/**
|
|
* Triggers interrupts based on activity since the last tick
|
|
*/
|
|
|
|
tick() {
|
|
if (this.mode & MODE_INT_VBL) {
|
|
this.serve |= INT_SCREEN;
|
|
}
|
|
if ((this.mode & MODE_INT_PRESS) && this.shouldIntPress) {
|
|
this.serve |= INT_PRESS;
|
|
}
|
|
if ((this.mode & MODE_INT_MOVE) && this.shouldIntMove) {
|
|
this.serve |= INT_MOVE;
|
|
}
|
|
if (this.serve) {
|
|
this.cpu.irq();
|
|
}
|
|
this.shouldIntMove = false;
|
|
this.shouldIntPress = false;
|
|
}
|
|
|
|
/**
|
|
* Scales mouse position and clamps to min and max,and flags
|
|
* potential mouse state change interrupt
|
|
*
|
|
* @param x Client mouse X position
|
|
* @param y Client mouse Y position
|
|
* @param w Client width
|
|
* @param h Client height
|
|
*/
|
|
|
|
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.shouldIntMove = true;
|
|
}
|
|
|
|
/**
|
|
* Tracks mouse button state and flags potential
|
|
* mouse state change interrupt
|
|
*
|
|
* @param down Mouse button down state
|
|
*/
|
|
|
|
setMouseDown(down: boolean) {
|
|
this.shouldIntPress = this.down !== down;
|
|
this.down = down;
|
|
}
|
|
|
|
/**
|
|
* Restores saved state
|
|
*
|
|
* @param state stored state
|
|
*/
|
|
|
|
setState(state: MouseState) {
|
|
this.clampXMin = state.clampXMin;
|
|
this.clampYMin = state.clampYMin;
|
|
this.clampXMax = state.clampXMax;
|
|
this.clampYMax = state.clampYMax;
|
|
this.x = state.x;
|
|
this.y = state.y;
|
|
this.mode = state.mode;
|
|
this.down = state.down;
|
|
this.lastDown = state.lastDown;
|
|
this.lastX = state.lastX;
|
|
this.lastY = state.lastY;
|
|
this.serve = state.serve;
|
|
this.shouldIntMove = state.shouldIntMove;
|
|
this.shouldIntPress = state.shouldIntPress;
|
|
this.slot = state.slot;
|
|
}
|
|
|
|
/**
|
|
* Saves state for restoration
|
|
*
|
|
* @returns restorable state
|
|
*/
|
|
|
|
getState(): MouseState {
|
|
return {
|
|
clampXMin: this.clampXMin,
|
|
clampYMin: this.clampYMin,
|
|
clampXMax: this.clampXMax,
|
|
clampYMax: this.clampYMax,
|
|
x: this.x,
|
|
y: this.y,
|
|
mode: this.mode,
|
|
down: this.down,
|
|
lastDown: this.lastDown,
|
|
lastX: this.lastX,
|
|
lastY: this.lastY,
|
|
serve: this.serve,
|
|
shouldIntMove: this.shouldIntMove,
|
|
shouldIntPress: this.shouldIntPress,
|
|
slot: this.slot
|
|
};
|
|
}
|
|
}
|