From b83300b2b2411429c550b7812aea163eaf0fef50 Mon Sep 17 00:00:00 2001 From: "Justin D. Harris" Date: Fri, 19 Jun 2020 00:13:19 -0400 Subject: [PATCH] [client] MACROS (#23) Macros: save, edit, and delete. They're stored in your browser. Relates to #8 --- server/setup.py | 2 +- website-client/.eslintrc.js | 2 +- website-client/package.json | 2 +- website-client/src/App.tsx | 2 + .../src/components/Controller/Controller.tsx | 46 +-- .../__tests__/parse-command.test.ts | 220 ++++++++++ .../components/Controller/parse-command.ts | 132 ++++++ .../src/components/Macros/MacroRecorder.ts | 15 +- .../src/components/Macros/Macros.tsx | 381 +++++++++++++++++- website-client/src/components/PlayGame.tsx | 29 +- website-client/src/key-binding/KeyBinding.ts | 2 +- .../src/key-binding/KeyboardBinding.ts | 30 +- 12 files changed, 797 insertions(+), 66 deletions(-) create mode 100644 website-client/src/components/Controller/__tests__/parse-command.test.ts create mode 100644 website-client/src/components/Controller/parse-command.ts diff --git a/server/setup.py b/server/setup.py index 0aa45ca..ec66aab 100644 --- a/server/setup.py +++ b/server/setup.py @@ -14,7 +14,7 @@ setup( name='switch-remoteplay-server', - version='0.4.0', + version='0.5.0', packages=find_packages(), url='https://github.com/juharris/switch-remoteplay', license='MIT', diff --git a/website-client/.eslintrc.js b/website-client/.eslintrc.js index bf05e36..bbbf7ba 100644 --- a/website-client/.eslintrc.js +++ b/website-client/.eslintrc.js @@ -15,7 +15,7 @@ module.exports = { rules: { 'comma-dangle': ['off', 'ignore'], 'comma-spacing': ["error", { "before": false, "after": true }], - indent: ['error', 'tab'], + indent: ['error', 'tab', { SwitchCase: 1 }], 'no-tabs': 0, 'operator-linebreak': ['off'], quotes: ['off'], diff --git a/website-client/package.json b/website-client/package.json index 0e20147..172d1ab 100644 --- a/website-client/package.json +++ b/website-client/package.json @@ -1,6 +1,6 @@ { "name": "switch-rp-client", - "version": "0.4.0", + "version": "0.5.0", "private": true, "dependencies": { "@material-ui/core": "^4.10.0", diff --git a/website-client/src/App.tsx b/website-client/src/App.tsx index 3645513..d4b26cd 100644 --- a/website-client/src/App.tsx +++ b/website-client/src/App.tsx @@ -1,3 +1,4 @@ +import blue from '@material-ui/core/colors/blue' import CssBaseline from '@material-ui/core/CssBaseline' import { createMuiTheme, createStyles, ThemeProvider, withStyles, WithStyles } from '@material-ui/core/styles' import React from 'react' @@ -10,6 +11,7 @@ import PlayGame from './components/PlayGame' const theme = createMuiTheme({ palette: { type: 'dark', + primary: blue, }, }) diff --git a/website-client/src/components/Controller/Controller.tsx b/website-client/src/components/Controller/Controller.tsx index 3fa713c..94a43fb 100644 --- a/website-client/src/components/Controller/Controller.tsx +++ b/website-client/src/components/Controller/Controller.tsx @@ -17,7 +17,7 @@ const styles = () => createStyles({ class Controller extends React.Component { render(): React.ReactNode { const { classes } = this.props - const controllerState: ControllerState = this.props.controllerState || new ControllerState() + const controllerState: ControllerState | undefined = this.props.controllerState return (
@@ -26,7 +26,7 @@ class Controller extends React.Component {

L

@@ -35,7 +35,7 @@ class Controller extends React.Component {

ZL

@@ -43,7 +43,7 @@ class Controller extends React.Component {
{/* Slightly wider than a typical minus. */} @@ -52,22 +52,22 @@ class Controller extends React.Component {

@@ -81,7 +81,7 @@ class Controller extends React.Component {

R

@@ -90,7 +90,7 @@ class Controller extends React.Component {

+

@@ -98,7 +98,7 @@ class Controller extends React.Component {

ZR

@@ -107,21 +107,21 @@ class Controller extends React.Component {

diff --git a/website-client/src/components/Controller/__tests__/parse-command.test.ts b/website-client/src/components/Controller/__tests__/parse-command.test.ts new file mode 100644 index 0000000..d446342 --- /dev/null +++ b/website-client/src/components/Controller/__tests__/parse-command.test.ts @@ -0,0 +1,220 @@ +import { ControllerState } from '../ControllerState' +import { parseCommand } from '../parse-command' + +describe('parseCommand', () => { + it('tap button', () => { + let c1, c2 + for (const buttonName of [ + 'l', 'zl', + 'r', 'zr', + 'a', 'b', 'x', 'y', + 'minus', 'plus', + 'home', 'capture', + + ]) { + c1 = new ControllerState(); + (c1 as any)[buttonName].isPressed = true + c2 = new ControllerState() + expect(parseCommand(`${buttonName}`)).toStrictEqual([c1, c2]) + } + + c1 = new ControllerState() + c1.leftStick.isPressed = true + c2 = new ControllerState() + expect(parseCommand(`l_stick`)).toStrictEqual([c1, c2]) + + c1 = new ControllerState() + c1.rightStick.isPressed = true + c2 = new ControllerState() + expect(parseCommand(`r_stick`)).toStrictEqual([c1, c2]) + + c1 = new ControllerState() + c1.arrowDown.isPressed = true + c2 = new ControllerState() + expect(parseCommand(`down`)).toStrictEqual([c1, c2]) + + c1 = new ControllerState() + c1.arrowUp.isPressed = true + c2 = new ControllerState() + expect(parseCommand(`up`)).toStrictEqual([c1, c2]) + + c1 = new ControllerState() + c1.arrowRight.isPressed = true + c2 = new ControllerState() + expect(parseCommand(`right`)).toStrictEqual([c1, c2]) + + c1 = new ControllerState() + c1.arrowLeft.isPressed = true + c2 = new ControllerState() + expect(parseCommand(`left`)).toStrictEqual([c1, c2]) + }) + + it('push button', () => { + let c = new ControllerState() + for (const buttonName of [ + 'l', 'zl', + 'r', 'zr', + 'a', 'b', 'x', 'y', + 'minus', 'plus', + 'home', 'capture', + + ]) { + (c as any)[buttonName].isPressed = true + expect(parseCommand(`${buttonName} d`)).toStrictEqual([c]) + + c = new ControllerState() + expect(parseCommand(`${buttonName} u`)).toStrictEqual([c]) + } + + c = new ControllerState() + c.leftStick.isPressed = true + expect(parseCommand(`l_stick d`)).toStrictEqual([c]) + c = new ControllerState() + expect(parseCommand(`l_stick u`)).toStrictEqual([c]) + + c.rightStick.isPressed = true + expect(parseCommand(`r_stick d`)).toStrictEqual([c]) + c = new ControllerState() + expect(parseCommand(`r_stick u`)).toStrictEqual([c]) + + c = new ControllerState() + c.arrowDown.isPressed = true + expect(parseCommand(`down d`)).toStrictEqual([c]) + c = new ControllerState() + expect(parseCommand(`down u`)).toStrictEqual([c]) + + c.arrowUp.isPressed = true + expect(parseCommand(`up d`)).toStrictEqual([c]) + c = new ControllerState() + expect(parseCommand(`up u`)).toStrictEqual([c]) + + c.arrowRight.isPressed = true + expect(parseCommand(`right d`)).toStrictEqual([c]) + c = new ControllerState() + expect(parseCommand(`right u`)).toStrictEqual([c]) + + c.arrowLeft.isPressed = true + expect(parseCommand(`left d`)).toStrictEqual([c]) + c = new ControllerState() + expect(parseCommand(`left u`)).toStrictEqual([c]) + }) + + it('move sticks', () => { + let c = new ControllerState() + + c.leftStick.verticalValue = -1 + expect(parseCommand(`s l up`)).toStrictEqual([c]) + c.leftStick.verticalValue = 1 + expect(parseCommand(`s l down`)).toStrictEqual([c]) + c.leftStick.verticalValue = 0 + c.leftStick.horizontalValue = -1 + expect(parseCommand(`s l left`)).toStrictEqual([c]) + c.leftStick.horizontalValue = 1 + expect(parseCommand(`s l right`)).toStrictEqual([c]) + c.leftStick.horizontalValue = 0 + expect(parseCommand(`s l center`)).toStrictEqual([c]) + + c.leftStick.horizontalValue = -1 + expect(parseCommand(`s l h min`)).toStrictEqual([c]) + c.leftStick.horizontalValue = 1 + expect(parseCommand(`s l h max`)).toStrictEqual([c]) + c.leftStick.horizontalValue = 0 + expect(parseCommand(`s l h center`)).toStrictEqual([c]) + c.leftStick.verticalValue = 1 + expect(parseCommand(`s l v min`)).toStrictEqual([c]) + c.leftStick.verticalValue = -1 + expect(parseCommand(`s l v max`)).toStrictEqual([c]) + c.leftStick.verticalValue = 0 + expect(parseCommand(`s l v center`)).toStrictEqual([c]) + + c.leftStick.horizontalValue = 0.5 + expect(parseCommand(`s l h 0.5`)).toStrictEqual([c]) + c.leftStick.horizontalValue = -0.5 + expect(parseCommand(`s l h -0.5`)).toStrictEqual([c]) + c.leftStick.horizontalValue = -1 + expect(parseCommand(`s l h -1`)).toStrictEqual([c]) + c.leftStick.horizontalValue = 1 + expect(parseCommand(`s l h +1`)).toStrictEqual([c]) + c.leftStick.horizontalValue = 1 + expect(parseCommand(`s l h 1`)).toStrictEqual([c]) + c.leftStick.horizontalValue = 0 + expect(parseCommand(`s l h 0`)).toStrictEqual([c]) + c.leftStick.verticalValue = 0.5 + expect(parseCommand(`s l v 0.5`)).toStrictEqual([c]) + c.leftStick.verticalValue = -0.5 + expect(parseCommand(`s l v -0.5`)).toStrictEqual([c]) + c.leftStick.verticalValue = -1 + expect(parseCommand(`s l v -1`)).toStrictEqual([c]) + c.leftStick.verticalValue = 1 + expect(parseCommand(`s l v +1`)).toStrictEqual([c]) + c.leftStick.verticalValue = 1 + expect(parseCommand(`s l v 1`)).toStrictEqual([c]) + c.leftStick.verticalValue = 0 + expect(parseCommand(`s l v 0`)).toStrictEqual([c]) + c.leftStick.horizontalValue = 0.5 + c.leftStick.verticalValue = -1 + expect(parseCommand(`s l hv 0.5 -1`)).toStrictEqual([c]) + c.leftStick.horizontalValue = -0.5 + c.leftStick.verticalValue = 1 + expect(parseCommand(`s l hv -0.5 1`)).toStrictEqual([c]) + c.leftStick.horizontalValue = 0 + c.leftStick.verticalValue = 0.5 + expect(parseCommand(`s l hv 0 .5`)).toStrictEqual([c]) + + + c = new ControllerState() + c.rightStick.verticalValue = -1 + expect(parseCommand(`s r up`)).toStrictEqual([c]) + c.rightStick.verticalValue = 1 + expect(parseCommand(`s r down`)).toStrictEqual([c]) + c.rightStick.verticalValue = 0 + c.rightStick.horizontalValue = -1 + expect(parseCommand(`s r left`)).toStrictEqual([c]) + c.rightStick.horizontalValue = 1 + expect(parseCommand(`s r right`)).toStrictEqual([c]) + c.rightStick.horizontalValue = 0 + expect(parseCommand(`s r center`)).toStrictEqual([c]) + + c.rightStick.horizontalValue = -1 + expect(parseCommand(`s r h min`)).toStrictEqual([c]) + c.rightStick.horizontalValue = 1 + expect(parseCommand(`s r h max`)).toStrictEqual([c]) + c.rightStick.horizontalValue = 0 + expect(parseCommand(`s r h center`)).toStrictEqual([c]) + c.rightStick.verticalValue = 1 + expect(parseCommand(`s r v min`)).toStrictEqual([c]) + c.rightStick.verticalValue = -1 + expect(parseCommand(`s r v max`)).toStrictEqual([c]) + c.rightStick.verticalValue = 0 + expect(parseCommand(`s r v center`)).toStrictEqual([c]) + + c.rightStick.horizontalValue = 0.5 + expect(parseCommand(`s r h 0.5`)).toStrictEqual([c]) + c.rightStick.horizontalValue = 0 + c.rightStick.verticalValue = 0.5 + expect(parseCommand(`s r v 0.5`)).toStrictEqual([c]) + c.rightStick.verticalValue = -0.5 + expect(parseCommand(`s r v -0.5`)).toStrictEqual([c]) + c.rightStick.verticalValue = -1 + expect(parseCommand(`s r v -1`)).toStrictEqual([c]) + c.rightStick.verticalValue = 0 + expect(parseCommand(`s r v 0`)).toStrictEqual([c]) + c.rightStick.horizontalValue = 0.5 + c.rightStick.verticalValue = -1 + expect(parseCommand(`s r hv 0.5 -1`)).toStrictEqual([c]) + c.rightStick.horizontalValue = -0.5 + c.rightStick.verticalValue = 1 + expect(parseCommand(`s r hv -0.5 1`)).toStrictEqual([c]) + }) + + + it('unrecognized', () => { + expect(parseCommand('junk')).toStrictEqual([new ControllerState()]) + }) + + it('combined', () => { + const c = new ControllerState() + c.a.isPressed = c.b.isPressed = true + expect(parseCommand('a d&b d')).toStrictEqual([c]) + }) +}) diff --git a/website-client/src/components/Controller/parse-command.ts b/website-client/src/components/Controller/parse-command.ts new file mode 100644 index 0000000..71a4473 --- /dev/null +++ b/website-client/src/components/Controller/parse-command.ts @@ -0,0 +1,132 @@ +import { ControllerState } from './ControllerState' + +const buttonNames = new Set([ + 'l', 'zl', + 'r', 'zr', + 'a', 'b', 'x', 'y', + 'minus', 'plus', + 'l_stick', 'r_stick', + 'home', 'capture', + 'down', 'up', 'left', 'right']) + +const buttonNameToStateMember: { [buttonName: string]: string } = { + l_stick: 'leftStick', + r_stick: 'rightStick', + down: 'arrowDown', + up: 'arrowUp', + left: 'arrowLeft', + right: 'arrowRight', +} + +/** + * This should mainly be used for macros. + * + * @param command The command to run. + * @returns The controller states for running the command. + */ +function parseCommand(command: string): ControllerState[] { + const result = [] + + const controllerState = new ControllerState() + let hasTap = false + for (let singleCommand of command.split('&')) { + singleCommand = singleCommand.trim() + if (buttonNames.has(singleCommand)) { + const member: string = buttonNameToStateMember[singleCommand] || singleCommand; + (controllerState as any)[member].isPressed = true + hasTap = true + } else { + const commandParts = singleCommand.split(/\s+/) + if (commandParts.length < 2) { + console.warn("Ignoring unrecognized part of command: \"%s\" from \"%s\"", singleCommand, command) + } else { + const button = commandParts[0] + if (button === 's') { + const stick = commandParts[1] + let stickState = undefined + if (stick === 'l') { + stickState = controllerState.leftStick + } else if (stick === 'r') { + stickState = controllerState.rightStick + } + if (stickState) { + if (commandParts.length === 3) { + const direction = commandParts[2] + switch (direction) { + case 'up': + stickState.verticalValue = -1 + break + case 'down': + stickState.verticalValue = 1 + break + case 'left': + stickState.horizontalValue = -1 + break + case 'right': + stickState.horizontalValue = 1 + break + case 'center': + stickState.horizontalValue = stickState.verticalValue = 0 + break + default: + console.warn("Ignoring unrecognized direction in: \"%s\" from \"%s\"", singleCommand, command) + } + } else if (commandParts.length === 4) { + const direction = commandParts[2] + const amount = commandParts[3] + let stickAmount + switch (amount) { + case 'min': + stickAmount = direction === 'h' ? -1 : +1 + break + case 'max': + stickAmount = direction === 'h' ? +1 : -1 + break + case 'center': + stickAmount = 0 + break + default: + stickAmount = parseFloat(amount) + } + if (direction === 'h') { + stickState.horizontalValue = stickAmount + } else if (direction === 'v') { + stickState.verticalValue = stickAmount + } else { + console.warn("Ignoring unrecognized direction in: \"%s\" from \"%s\"", singleCommand, command) + } + } else if (commandParts.length === 5 && commandParts[2] === 'hv') { + const horizontalAmount = commandParts[3] + const verticalAmount = commandParts[4] + stickState.horizontalValue = parseFloat(horizontalAmount) + stickState.verticalValue = parseFloat(verticalAmount) + } else { + console.warn("Ignoring unrecognized stick command in: \"%s\" from \"%s\"", singleCommand, command) + } + } else { + console.warn("Ignoring unrecognized stick in: \"%s\" from \"%s\"", singleCommand, command) + } + } else if (buttonNames.has(button)) { + const isPressed = commandParts[1] === 'd' + if (isPressed) { + (controllerState as any)[buttonNameToStateMember[button] || button].isPressed = true + } + } else { + console.warn("Ignoring unrecognized part of command: \"%s\" from \"%s\"", singleCommand, command) + } + } + } + + } + result.push(controllerState) + if (hasTap) { + result.push(new ControllerState()) + } + + return result +} + + + +export { parseCommand } + diff --git a/website-client/src/components/Macros/MacroRecorder.ts b/website-client/src/components/Macros/MacroRecorder.ts index a79bcbe..8a7aa55 100644 --- a/website-client/src/components/Macros/MacroRecorder.ts +++ b/website-client/src/components/Macros/MacroRecorder.ts @@ -1,9 +1,7 @@ -import { ControllerState } from "../Controller/ControllerState" - export default class MacroRecorder { isRecording = false lastCommandTime = Date.now() - currentRecording: { command: string, controllerState: any }[] = [] + currentRecording: string[] = [] start(): void { this.currentRecording = [] @@ -17,14 +15,13 @@ export default class MacroRecorder { console.debug(this.currentRecording) } - add(command: string, controllerState: ControllerState): void { + add(command: string): void { if (this.isRecording) { const commandTime = Date.now() - this.currentRecording.push({ - command: `wait ${commandTime - this.lastCommandTime}`, - controllerState - }) - this.currentRecording.push({ command, controllerState }) + this.currentRecording.push( + `wait ${commandTime - this.lastCommandTime}`, + ) + this.currentRecording.push(command) this.lastCommandTime = commandTime } } diff --git a/website-client/src/components/Macros/Macros.tsx b/website-client/src/components/Macros/Macros.tsx index 5dc1a77..656ae32 100644 --- a/website-client/src/components/Macros/Macros.tsx +++ b/website-client/src/components/Macros/Macros.tsx @@ -1,49 +1,309 @@ import { createStyles, withStyles } from '@material-ui/core' import Button from '@material-ui/core/Button' +import Card from '@material-ui/core/Card' +import CardActions from '@material-ui/core/CardActions' +import CardContent from '@material-ui/core/CardContent' +import green from '@material-ui/core/colors/green' +import red from '@material-ui/core/colors/red' import Grid from '@material-ui/core/Grid' +import Link from '@material-ui/core/Link' +import Snackbar from '@material-ui/core/Snackbar' +import { Theme } from '@material-ui/core/styles' +import TextareaAutosize from '@material-ui/core/TextareaAutosize' +import TextField from '@material-ui/core/TextField' import Tooltip from '@material-ui/core/Tooltip' import Typography from '@material-ui/core/Typography' +import AddIcon from '@material-ui/icons/Add' +import CancelIcon from '@material-ui/icons/Cancel' +import DeleteForeverIcon from '@material-ui/icons/DeleteForever' +import EditIcon from '@material-ui/icons/Edit' +import PlayArrowIcon from '@material-ui/icons/PlayArrow' +import SaveIcon from '@material-ui/icons/Save' +import MuiAlert, { AlertProps, Color as ToastColor } from '@material-ui/lab/Alert' import React from 'react' import { SendCommand } from '../../key-binding/KeyBinding' import MacroRecorder from './MacroRecorder' -const styles = () => createStyles({ + +const styles = (theme: Theme) => createStyles({ + macrosContainer: { + alignItems: 'stretch', + }, + macroName: { + marginLeft: '20px', + marginBottom: '1em', + }, + macroCode: { + whiteSpace: 'pre-wrap', + wordWrap: 'break-word', + }, + macroText: { + backgroundColor: '#222', + color: '#ddd', + width: '90%', + marginLeft: '20px', + }, + macroItem: { + }, + macroCard: { + display: 'flex', + flexDirection: 'column', + height: '100%', + }, + cardContent: { + // To keep the card buttons at the bottom of the card. + display: 'flex', + flex: '1 0 auto', + flexDirection: 'column', + }, + macroCardActions: { + display: 'flex', + }, + toast: { + width: '100%', + '& > * + *': { + marginTop: theme.spacing(2), + }, + }, }) +function Alert(props: AlertProps) { + return +} + +class SavedMacro { + constructor( + public id: string, + public name: string, + public macro: string[]) { } +} + class Macros extends React.Component<{ macroRecorder: MacroRecorder, sendCommand: SendCommand, + classes: any, }, any> { + private db?: IDBDatabase + constructor(props: any) { super(props) this.state = { isRecording: false, macroExists: false, + + isToastOpen: false, + toastMessage: "", + toastSeverity: "", + + macroName: "", + editMacro: "", + + savedMacros: [], } + this.addMacro = this.addMacro.bind(this) + this.cancelEditMacro = this.cancelEditMacro.bind(this) + this.closeToast = this.closeToast.bind(this) + this.deleteMacro = this.deleteMacro.bind(this) + this.handleChange = this.handleChange.bind(this) + this.openToast = this.openToast.bind(this) + this.playMacro = this.playMacro.bind(this) this.playLastRecordedMacro = this.playLastRecordedMacro.bind(this) + this.saveMacro = this.saveMacro.bind(this) this.startRecording = this.startRecording.bind(this) this.stopRecording = this.stopRecording.bind(this) } - startRecording(): void { + componentDidMount() { + const indexedDB = window.indexedDB + const request = indexedDB.open('macros', 1) + request.onerror = (event: Event) => { + this.openToast("Could not open the macro database. See the Console for more details.", 'error') + console.error("Could not open the macro database.") + console.error(event) + } + + request.onupgradeneeded = function(event: Event) { + const db: IDBDatabase = (event?.target as any).result + db.createObjectStore('macro', { keyPath: 'id', autoIncrement: true, }) + } + + request.onsuccess = (event: Event) => { + this.db = (event?.target as any).result + + this.loadSavedMacros() + } + } + + private openToast(toastMessage: string, toastSeverity: ToastColor): void { + this.setState({ + isToastOpen: true, + toastMessage, + toastSeverity, + }) + } + + private closeToast(event?: React.SyntheticEvent, reason?: string) { + if (reason === 'clickaway') { + return + } + + this.setState({ + isToastOpen: false, + }) + } + + private loadSavedMacros() { + if (this.db === undefined) { + // Shouldn't happen. + this.openToast("Error loading macros. See the Console for more details.", 'error') + console.error("The macro database has not been loaded yet. Please try again.") + return + } + const transaction = this.db.transaction('macro', 'readonly') + transaction.onerror = (event: Event) => { + this.openToast("Error loading macros. See the Console for more details.", 'error') + console.error(event) + } + const dataStore = transaction.objectStore('macro') + const getRequest = dataStore.getAll() + getRequest.onerror = (event: Event) => { + this.openToast("Error loading macros. See the Console for more details.", 'error') + console.error(event) + } + + getRequest.onsuccess = () => { + this.setState({ + savedMacros: getRequest.result, + }) + } + } + + private handleChange(event: React.ChangeEvent) { + this.setState({ [event.target.name]: event.target.value }) + } + + private startEditingMacro(id: string | undefined, name: string, macro: string[]) { + this.setState({ + editMacroId: id, + macroName: name, + editMacro: JSON.stringify(macro, null, 4), + }) + } + + private addMacro(): void { + this.startEditingMacro(undefined, "", ["a d", "wait 350", "a u"]) + } + + private parseMacro(macro: string): string[] { + // More validation and cleaning can be done here. + macro = macro.replace(/'/g, "\"") + return JSON.parse(macro) + } + + private saveMacro(): void { + let macro: string[] + try { + macro = this.parseMacro(this.state.editMacro) + } catch (err) { + this.openToast("The macro is invalid. See the Console for more details.", 'error') + console.error("Invalid macro:") + console.error(err) + return + } + const name = this.state.macroName + + if (this.db === undefined) { + this.openToast("The macro database has not been loaded yet. Please try again.", 'error') + console.error("The macro database has not been loaded yet. Please try again.") + return + } + + const transaction = this.db.transaction('macro', 'readwrite') + transaction.onerror = (event: Event) => { + this.openToast("Error saving the macro. See the Console for more details.", 'error') + console.error(event) + } + const dataStore = transaction.objectStore('macro') + let request + if (this.state.editMacroId !== undefined) { + request = dataStore.put({ + id: this.state.editMacroId, + name, + macro, + }) + } else { + request = dataStore.add({ + name, + macro, + }) + } + request.onerror = (event: Event) => { + this.openToast("Error saving the macro. See the Console for more details.", 'error') + console.error(event) + } + request.onsuccess = () => { + this.loadSavedMacros() + this.setState({ + macroName: "", + editMacro: "", + }) + this.openToast("Saved", 'success') + } + } + + private deleteMacro(macroId: string): void { + if (this.db === undefined) { + this.openToast("The macro database has not been loaded yet. Please try again.", 'error') + console.error("The macro database has not been loaded yet. Please try again.") + return + } + + const transaction = this.db.transaction('macro', 'readwrite') + transaction.onerror = (event: Event) => { + this.openToast("Error deleting the macro. See the Console for more details.", 'error') + console.error(event) + } + const dataStore = transaction.objectStore('macro') + const deleteRequest = dataStore.delete(macroId) + deleteRequest.onerror = (event: Event) => { + this.openToast("Error deleting the macro. See the Console for more details.", 'error') + console.error(event) + } + deleteRequest.onsuccess = () => { + this.loadSavedMacros() + this.cancelEditMacro() + this.openToast("Deleted the macro", 'info') + } + } + + private cancelEditMacro(): void { + this.setState({ + editMacro: "" + }) + } + + private startRecording(): void { this.setState({ isRecording: true }) this.props.macroRecorder.start() + this.openToast("Recording macro", 'info') } - stopRecording(): void { + private stopRecording(): void { this.props.macroRecorder.stop() + this.startEditingMacro(undefined, "", this.props.macroRecorder.currentRecording) this.setState({ isRecording: false, macroExists: true, }) + this.openToast("Stopped recording macro", 'info') } async sleep(sleepMillis: number) { return new Promise(resolve => setTimeout(resolve, sleepMillis)) } - async playLastRecordedMacro(): Promise { - for (const c of this.props.macroRecorder.currentRecording) { - const { command, controllerState } = c + async playMacro(macro: string[]) { + this.openToast("Playing macro", 'info') + for (const command of macro) { const m = /wait (\d+)/.exec(command) if (m) { const sleepMillis = parseInt(m[1]) @@ -51,15 +311,35 @@ class Macros extends React.Component<{ await this.sleep(sleepMillis) } } else { - this.props.sendCommand(command, controllerState) + this.props.sendCommand(command) } } + this.openToast("Done playing macro", 'info') + } + + async playLastRecordedMacro(): Promise { + return this.playMacro(this.props.macroRecorder.currentRecording) } render(): React.ReactNode { + const { classes } = this.props + return
+ + + {this.state.toastMessage} + + Macros + + + + + + + + {this.state.savedMacros.map((savedMacro: SavedMacro) => { + const maxLength = 90 + let macroText = JSON.stringify(savedMacro.macro) + if (macroText.length > maxLength) { + macroText = macroText.slice(0, maxLength - 10) + "..." + } + return + + + + {savedMacro.name} + +
{macroText}
+ {/* + */} +
+ + + + + + + + +
+
+ } + )} +
} } diff --git a/website-client/src/components/PlayGame.tsx b/website-client/src/components/PlayGame.tsx index 38ce9d5..8cf1820 100644 --- a/website-client/src/components/PlayGame.tsx +++ b/website-client/src/components/PlayGame.tsx @@ -13,6 +13,7 @@ import GamepadBinding from '../key-binding/GamepadBinding' import KeyboardBinding from '../key-binding/KeyboardBinding' import Controller from './Controller/Controller' import { ControllerState } from './Controller/ControllerState' +import { parseCommand } from './Controller/parse-command' import MacroRecorder from './Macros/MacroRecorder' import Macros from './Macros/Macros' @@ -213,18 +214,28 @@ class PlayGame extends React.Component { }) } - private sendCommand(command: string, controllerState: ControllerState) { + private sendCommand(command: string, controllerState?: ControllerState) { if (command && this.state.socket && this.state.isInSendMode) { this.state.socket.emit('p', command) } - this.setState({ - controllerState, - }) - // TODO Find a more compact way to store controller state changes. - // Maybe they shouldn't be stored at all and we can just re-parse the command. - // That would save weird logic in other places but still keep redundancies trying to make a compact command but they rebuilding it. - // Although the rebuilding can be limited to be doing just when saving a macro. - this.macroRecorder.add(command, JSON.parse(JSON.stringify(controllerState))) + + // Controller and key bindings should send the state since it should be easy for them to compute it. + // Running commands from a macro might not send the state. + if (controllerState !== undefined) { + this.setState({ + controllerState, + }) + } else { + const controllerStates = parseCommand(command) + for (let i = 0; i < controllerStates.length; ++i) { + setTimeout(() => { + this.setState({ + controllerState: controllerStates[i], + }) + }, i * 100) + } + } + this.macroRecorder.add(command) } private toggleSendMode() { diff --git a/website-client/src/key-binding/KeyBinding.ts b/website-client/src/key-binding/KeyBinding.ts index 0205b6b..e4580c8 100644 --- a/website-client/src/key-binding/KeyBinding.ts +++ b/website-client/src/key-binding/KeyBinding.ts @@ -1,7 +1,7 @@ import { ControllerState } from '../components/Controller/ControllerState' export interface SendCommand { - (command: string, controllerState: ControllerState): void + (command: string, controllerState?: ControllerState): void } export abstract class KeyBinding { diff --git a/website-client/src/key-binding/KeyboardBinding.ts b/website-client/src/key-binding/KeyboardBinding.ts index 727327e..666d6e4 100644 --- a/website-client/src/key-binding/KeyboardBinding.ts +++ b/website-client/src/key-binding/KeyboardBinding.ts @@ -65,7 +65,7 @@ export default class KeyboardBinding extends KeyBinding { // It would be tricky to do in general and should probably be specific to the game. // document.addEventListener('mousemove', mouseMoveHandler) // document.addEventListener('mousedown', (e: MouseEvent) => { - // // TODO Allow if clicking on 'send-mode-toggle' + // // Allow if clicking on 'send-mode-toggle' // // if (e!.target!.name === 'send-mode-toggle') { // // return // // } @@ -122,6 +122,10 @@ export default class KeyboardBinding extends KeyBinding { // } private handleKey(e: KeyboardEvent | MouseEvent, keyName: string, keyDirection: string) { + const targetName = (e.target as any).name + if (targetName === 'editMacro' || targetName === 'macroName') { + return + } let keyMapping = this.keyMap if (keyName in keyMapping) { @@ -140,18 +144,18 @@ export default class KeyboardBinding extends KeyBinding { const stick = controllerState[action.name] if (stick) { switch (action.dirName) { - case 'left': - stick.horizontalValue = keyDirection === 'down' ? -1 : 0 - break - case 'right': - stick.horizontalValue = keyDirection === 'down' ? +1 : 0 - break - case 'up': - stick.verticalValue = keyDirection === 'down' ? -1 : 0 - break - case 'down': - stick.verticalValue = keyDirection === 'down' ? +1 : 0 - break + case 'left': + stick.horizontalValue = keyDirection === 'down' ? -1 : 0 + break + case 'right': + stick.horizontalValue = keyDirection === 'down' ? +1 : 0 + break + case 'up': + stick.verticalValue = keyDirection === 'down' ? -1 : 0 + break + case 'down': + stick.verticalValue = keyDirection === 'down' ? +1 : 0 + break } } } else {