added Gamepad API support

This commit is contained in:
Steven Hugg 2019-06-07 13:03:30 -04:00
parent 271c2ea020
commit 9324b23def
8 changed files with 131 additions and 26 deletions

View File

@ -86,7 +86,7 @@ TODO:
- upload binary files doesn't do what's expected, changing pulldown and whatnot
- chrome autostart audio: https://github.com/processing/p5.js-sound/issues/249
- firefox autostart audio: https://support.mozilla.org/en-US/kb/block-autoplay
- show player controls for each platform, allow touch support, navigator.getGamepads
- show player controls for each platform, allow touch support
- better undo/diff for mistakes?
- ide bug/feature visualizer for sponsors
- global undo/redo at checkpoints (when rom changes)

View File

@ -1,5 +1,5 @@
import { RAM, RasterVideo, dumpRAM, AnimationTimer, setKeyboardFromMap, padBytes } from "./emu";
import { RAM, RasterVideo, dumpRAM, AnimationTimer, setKeyboardFromMap, padBytes, ControllerPoller } from "./emu";
import { hex, printFlags, invertMap } from "./util";
import { CodeAnalyzer } from "./analysis";
import { disassemble6502 } from "./cpu/disasm6502";
@ -265,11 +265,14 @@ export abstract class BaseDebugPlatform extends BasePlatform {
this.resume();
}
preFrame() {
this.updateRecorder();
}
postFrame() {
}
pollControls() {
}
nextFrame(novideo : boolean) {
this.pollControls();
this.updateRecorder();
this.preFrame();
this.advance(novideo);
this.postFrame();
@ -1101,6 +1104,7 @@ export abstract class BasicZ80ScanlinePlatform extends BaseZ80Platform {
pixels : Uint32Array;
inputs = new Uint8Array(16);
mainElement : HTMLElement;
poller : ControllerPoller;
abstract newRAM() : Uint8Array;
abstract newMembus() : MemoryBus;
@ -1126,13 +1130,15 @@ export abstract class BasicZ80ScanlinePlatform extends BaseZ80Platform {
this.video = new RasterVideo(this.mainElement, this.canvasWidth, this.numVisibleScanlines, this.getVideoOptions());
this.video.create();
this.pixels = this.video.getFrameData();
setKeyboardFromMap(this.video, this.inputs, this.getKeyboardMap(), this.getKeyboardFunction());
this.poller = setKeyboardFromMap(this.video, this.inputs, this.getKeyboardMap(), this.getKeyboardFunction());
this.timer = new AnimationTimer(60, this.nextFrame.bind(this));
}
readAddress(addr) {
return this.membus.read(addr);
}
pollControls() { this.poller.poll(); }
advance(novideo : boolean) {
var extraCycles = 0;

View File

@ -51,13 +51,13 @@ export enum KeyFlags {
}
function _setKeyboardEvents(canvas:HTMLElement, callback:KeyboardCallback) {
canvas.onkeydown = function(e) {
canvas.onkeydown = (e) => {
callback(e.which, 0, KeyFlags.KeyDown|_metakeyflags(e));
};
canvas.onkeyup = function(e) {
canvas.onkeyup = (e) => {
callback(e.which, 0, KeyFlags.KeyUp|_metakeyflags(e));
};
canvas.onkeypress = function(e) {
canvas.onkeypress = (e) => {
callback(e.which, e.charCode, KeyFlags.KeyPress|_metakeyflags(e));
};
};
@ -327,16 +327,24 @@ interface KeyDef {
button?:number
};
interface KeyMapEntry {
index:number;
mask:number;
def:KeyDef;
}
type KeyCodeMap = Map<number,KeyMapEntry>;
export const Keys : {[keycode:string]:KeyDef} = {
// gamepad and keyboard (player 0)
UP: {c: 38, n: "Up", plyr:0, yaxis:-1},
DOWN: {c: 40, n: "Down", plyr:0, yaxis:1},
LEFT: {c: 37, n: "Left", plyr:0, xaxis:-1},
RIGHT: {c: 39, n: "Right", plyr:0, xaxis:1},
A: {c: 32, n: "Space", plyr:0, button:0},
B: {c: 17, n: "Ctrl", plyr:0, button:1},
GP_A: {c: 88, n: "X", plyr:0, button:0},
GP_B: {c: 90, n: "Z", plyr:0, button:1},
A: {c: 32, n: "Space", plyr:0, button:0},
B: {c: 17, n: "Ctrl", plyr:0, button:1},
GP_A: {c: 88, n: "X", plyr:0, button:0},
GP_B: {c: 90, n: "Z", plyr:0, button:1},
SELECT: {c: 220, n: "\\", plyr:0, button:8},
START: {c: 13, n: "Enter", plyr:0, button:9},
// gamepad and keyboard (player 1)
@ -344,8 +352,8 @@ export const Keys : {[keycode:string]:KeyDef} = {
P2_DOWN: {c: 83, n: "S", plyr:1, yaxis:1},
P2_LEFT: {c: 65, n: "A", plyr:1, xaxis:-1},
P2_RIGHT: {c: 68, n: "D", plyr:1, xaxis:1},
P2_A: {c: 84, n: "T", plyr:1, button:0},
P2_B: {c: 82, n: "R", plyr:1, button:1},
P2_A: {c: 84, n: "T", plyr:1, button:0},
P2_B: {c: 82, n: "R", plyr:1, button:1},
P2_SELECT: {c: 70, n: "F", plyr:1, button:8},
P2_START: {c: 71, n: "G", plyr:1, button:9},
// keyboard only
@ -459,9 +467,11 @@ function _metakeyflags(e) {
(e.metaKey?KeyFlags.Meta:0);
}
export function setKeyboardFromMap(video, switches, map, func?) {
video.setKeyboardEvents(function(key,code,flags) {
var o = map[key];
type KeyMapFunction = (o:KeyMapEntry, key:number, code:number, flags:number) => void;
export function setKeyboardFromMap(video:RasterVideo, switches:number[]|Uint8Array, map:KeyCodeMap, func?:KeyMapFunction) {
var handler = (key,code,flags) => {
var o : KeyMapEntry = map[key];
if (o && func) {
func(o, key, code, flags);
}
@ -479,18 +489,92 @@ export function setKeyboardFromMap(video, switches, map, func?) {
switches[o.index] &= ~mask;
}
}
});
};
video.setKeyboardEvents(handler);
return new ControllerPoller(map, handler);
}
export function makeKeycodeMap(table : [KeyDef,number,number][]) {
var map = new Map();
export function makeKeycodeMap(table : [KeyDef,number,number][]) : KeyCodeMap {
var map = new Map<number,KeyMapEntry>();
for (var i=0; i<table.length; i++) {
var entry = table[i];
map[entry[0].c] = {index:entry[1], mask:entry[2]};
var val : KeyMapEntry = {index:entry[1], mask:entry[2], def:entry[0]};
map[entry[0].c] = val;
}
return map;
}
export class ControllerPoller {
active = false;
map : KeyCodeMap;
handler;
state = new Int8Array(32);
lastState = new Int8Array(32);
AXIS0 = 24; // first joystick axis index
constructor(map:KeyCodeMap, handler:(key,code,flags) => void) {
this.map = map;
this.handler = handler;
window.addEventListener("gamepadconnected", (event) => {
console.log("Gamepad connected:", event);
this.active = typeof navigator.getGamepads === 'function';
});
window.addEventListener("gamepaddisconnected", (event) => {
console.log("Gamepad disconnected:", event);
});
}
poll() {
if (!this.active) return;
var gamepads = navigator.getGamepads();
for (var gpi=0; gpi<gamepads.length; gpi++) {
var gp = gamepads[gpi];
if (gp) {
for (var i=0; i<gp.axes.length; i++) {
var k = i + this.AXIS0;
this.state[k] = Math.round(gp.axes[i]);
if (this.state[k] != this.lastState[k]) {
this.handleStateChange(gpi,k);
}
}
for (var i=0; i<gp.buttons.length; i++) {
this.state[i] = gp.buttons[i].pressed ? 1 : 0;
if (this.state[i] != this.lastState[i]) {
this.handleStateChange(gpi,i);
}
}
this.lastState.set(this.state);
}
}
}
handleStateChange(gpi:number, k:number) {
var axis = k - this.AXIS0;
for (var code in this.map) {
var entry = this.map[code];
var def = entry.def;
// is this a gamepad entry? same player #?
if (def && def.plyr == gpi) {
var state = this.state[k];
var lastState = this.lastState[k];
// check for button/axis match
if (k == def.button || (axis == 0 && def.xaxis == state) || (axis == 1 && def.yaxis == state)) {
//console.log(gpi,k,state,entry);
if (state != 0) {
this.handler(code, 0, KeyFlags.KeyDown);
} else {
this.handler(code, 0, KeyFlags.KeyUp);
}
break;
}
// joystick released?
else if (state == 0 && (axis == 0 && def.xaxis == lastState) || (axis == 1 && def.yaxis == lastState)) {
this.handler(code, 0, KeyFlags.KeyUp);
break;
}
}
}
}
}
export function padBytes(data:Uint8Array|number[], len:number) : Uint8Array {
if (data.length > len) {
throw Error("Data too long, " + data.length + " > " + len);

View File

@ -201,6 +201,7 @@ const _BallyAstrocadePlatform = function(mainElement, arcade) {
class BallyAstrocadePlatform extends BaseZ80Platform implements Platform {
scanline : number;
poller;
getPresets() {
return ASTROCADE_PRESETS;
@ -328,7 +329,7 @@ const _BallyAstrocadePlatform = function(mainElement, arcade) {
video.create();
video.setupMouseEvents();
var idata = video.getFrameData();
setKeyboardFromMap(video, inputs, ASTROCADE_KEYCODE_MAP);
this.poller = setKeyboardFromMap(video, inputs, ASTROCADE_KEYCODE_MAP);
pixels = video.getFrameData();
timer = new AnimationTimer(60, this.nextFrame.bind(this));
// default palette
@ -345,6 +346,8 @@ const _BallyAstrocadePlatform = function(mainElement, arcade) {
inputs[0x1d] = video.paddle_y & 0xff;
}
pollControls() { this.poller.poll(); }
advance(novideo : boolean) {
this.scanline = 0;
var extra = 0; // keep track of spare cycles

View File

@ -72,6 +72,7 @@ const _ColecoVisionPlatform = function(mainElement) {
var audio, psg;
var inputs = new Uint8Array(4);
var keypadMode = false;
var poller;
class ColecoVisionPlatform extends BaseZ80Platform implements Platform {
@ -137,9 +138,11 @@ const _ColecoVisionPlatform = function(mainElement) {
}
};
vdp = new TMS9918A(video.getFrameData(), cru, true); // true = 4 sprites/line
setKeyboardFromMap(video, inputs, COLECOVISION_KEYCODE_MAP);
poller = setKeyboardFromMap(video, inputs, COLECOVISION_KEYCODE_MAP);
timer = new AnimationTimer(60, this.nextFrame.bind(this));
}
pollControls() { poller.poll(); }
readAddress(addr) {
return membus.read(addr);

View File

@ -203,6 +203,7 @@ const _GalaxianPlatform = function(mainElement, options) {
class GalaxianPlatform extends BaseZ80Platform implements Platform {
scanline : number;
poller;
getPresets() {
return GALAXIAN_PRESETS;
@ -295,10 +296,12 @@ const _GalaxianPlatform = function(mainElement, options) {
video = new RasterVideo(mainElement,264,264,{rotate:90});
video.create();
var idata = video.getFrameData();
setKeyboardFromMap(video, inputs, keyMap);
this.poller = setKeyboardFromMap(video, inputs, keyMap);
pixels = video.getFrameData();
timer = new AnimationTimer(60, this.nextFrame.bind(this));
}
pollControls() { this.poller.poll(); }
readAddress(a) {
return (a == 0x7000 || a == 0x7800) ? null : membus.read(a); // ignore watchdog

View File

@ -1,7 +1,7 @@
"use strict";
import { Platform, Base6502Platform, BaseMAMEPlatform, getOpcodeMetadata_6502, cpuStateToLongString_6502, getToolForFilename_6502, dumpStackToString, ProfilerOutput } from "../baseplatform";
import { PLATFORMS, RAM, newAddressDecoder, padBytes, noise, setKeyboardFromMap, AnimationTimer, RasterVideo, Keys, makeKeycodeMap, dumpRAM, KeyFlags, EmuHalt } from "../emu";
import { PLATFORMS, RAM, newAddressDecoder, padBytes, noise, setKeyboardFromMap, AnimationTimer, RasterVideo, Keys, makeKeycodeMap, dumpRAM, KeyFlags, EmuHalt, ControllerPoller } from "../emu";
import { hex, lpad, lzgmini, byteArrayToString } from "../util";
import { CodeAnalyzer_nes } from "../analysis";
import { SampleAudio } from "../audio";
@ -71,6 +71,7 @@ class JSNESPlatform extends Base6502Platform implements Platform {
video;
audio;
timer;
poller : ControllerPoller;
audioFrequency = 44030; //44100
frameindex = 0;
ntvideo;
@ -136,7 +137,7 @@ class JSNESPlatform extends Base6502Platform implements Platform {
}
this.timer = new AnimationTimer(60, this.nextFrame.bind(this));
// set keyboard map
setKeyboardFromMap(this.video, [], JSNES_KEYCODE_MAP, (o,key,code,flags) => {
this.poller = setKeyboardFromMap(this.video, [], JSNES_KEYCODE_MAP, (o,key,code,flags) => {
if (flags & KeyFlags.KeyDown)
this.nes.buttonDown(o.index+1, o.mask); // controller, button
else if (flags & KeyFlags.KeyUp)
@ -144,6 +145,8 @@ class JSNESPlatform extends Base6502Platform implements Platform {
});
//var s = ''; nes.ppu.palTable.curTable.forEach((rgb) => { s += "0x"+hex(rgb,6)+", "; }); console.log(s);
}
pollControls() { this.poller.poll(); }
advance(novideo : boolean) {
this.nes.frame();

View File

@ -223,6 +223,7 @@ var VerilogPlatform = function(mainElement, options) {
this.__proto__ = new (BasePlatform as any)();
var video, audio;
var poller;
var useAudio = false;
var videoWidth = 292;
var videoHeight = 256;
@ -327,7 +328,7 @@ var VerilogPlatform = function(mainElement, options) {
ctx.font = "8px TinyFont";
ctx.fillStyle = "white";
ctx.textAlign = "left";
setKeyboardFromMap(video, switches, VERILOG_KEYCODE_MAP);
poller = setKeyboardFromMap(video, switches, VERILOG_KEYCODE_MAP);
var vcanvas = $(video.canvas);
idata = video.getFrameData();
timerCallback = () => {
@ -376,6 +377,8 @@ var VerilogPlatform = function(mainElement, options) {
});
}
// TODO: pollControls() { poller.poll(); }
resize() {
if (this.waveview) this.waveview.recreate();
}