From aba829d8af9f4d6ec2b20742a119df4d0f1ba1c2 Mon Sep 17 00:00:00 2001 From: "Justin D. Harris" Date: Tue, 9 Jun 2020 00:18:16 -0400 Subject: [PATCH] client: Support recording + running macros (#21) Resolves #10 --- README.md | 13 ++- .../src/components/Macros/MacroRecorder.ts | 31 +++++++ .../src/components/Macros/Macros.tsx | 89 +++++++++++++++++++ website-client/src/components/PlayGame.tsx | 10 +++ .../src/key-binding/KeyboardBinding.ts | 26 +++--- 5 files changed, 152 insertions(+), 17 deletions(-) create mode 100644 website-client/src/components/Macros/MacroRecorder.ts create mode 100644 website-client/src/components/Macros/Macros.tsx diff --git a/README.md b/README.md index 249de9c..58aa87d 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,21 @@ You <===> Website <=====> Server <--Bluetooth--> Switch '------------------------------ Streaming Server ``` -Example [video](https://youtu.be/EIofCEfQA1E) of someone playing my Switch from another city. +Example [video](https://youtu.be/EIofCEfQA1E) of someone playing my Switch **from another city**. -Example [video](https://youtu.be/TJlWK2HU8Do) of me using an Xbox controller (that does not have Bluetooth) to play my Switch. +Example [video](https://youtu.be/TJlWK2HU8Do) of me using an **Xbox controller (that does not have Bluetooth)** to play my Switch. + +Example [video](https://youtu.be/viv-B_A-A2o) of recording and running a **macro**. # Status One keyboard layout or gaming controller layout is supported to map input to the control sticks and the buttons on a Nintendo Switch controller. I've mainly tested this with Animal Crossing and Mixer - FTL low latency streaming. This is very much a work in progress right now but you can indeed play your Switch remotely using a keyboard/controller. +You can record and run **macros**. +You do not need your Switch's video going through your PC to record and run macros. +Just setting up the server to send commands via Bluetooth is enough. + # Requirements The host (person setting this up) needs: * A Nintendo Switch @@ -38,9 +44,8 @@ The client (your friend) needs: * A keyboard or gaming controller # Plans -* Support gaming controllers. * Support custom key bindings. -* **Support recording and running macros**. +* Improve macro support: naming, sharing, selecting, editing, etc. * Default layout options for common controllers. * Default key binding options for keyboard/mouse for certain games. * Loadable and exportable key binding configurations. diff --git a/website-client/src/components/Macros/MacroRecorder.ts b/website-client/src/components/Macros/MacroRecorder.ts new file mode 100644 index 0000000..a79bcbe --- /dev/null +++ b/website-client/src/components/Macros/MacroRecorder.ts @@ -0,0 +1,31 @@ +import { ControllerState } from "../Controller/ControllerState" + +export default class MacroRecorder { + isRecording = false + lastCommandTime = Date.now() + currentRecording: { command: string, controllerState: any }[] = [] + + start(): void { + this.currentRecording = [] + this.lastCommandTime = Date.now() + this.isRecording = true + } + + stop(): void { + this.isRecording = false + console.debug("Recorded macro:") + console.debug(this.currentRecording) + } + + add(command: string, controllerState: ControllerState): void { + if (this.isRecording) { + const commandTime = Date.now() + this.currentRecording.push({ + command: `wait ${commandTime - this.lastCommandTime}`, + controllerState + }) + this.currentRecording.push({ command, controllerState }) + this.lastCommandTime = commandTime + } + } +} diff --git a/website-client/src/components/Macros/Macros.tsx b/website-client/src/components/Macros/Macros.tsx new file mode 100644 index 0000000..9c119b4 --- /dev/null +++ b/website-client/src/components/Macros/Macros.tsx @@ -0,0 +1,89 @@ +import { createStyles, withStyles } from '@material-ui/core' +import Button from '@material-ui/core/Button' +import Grid from '@material-ui/core/Grid' +import Tooltip from '@material-ui/core/Tooltip' +import Typography from '@material-ui/core/Typography' +import React from 'react' +import { SendCommand } from '../../key-binding/KeyBinding' +import MacroRecorder from './MacroRecorder' + +const styles = () => createStyles({ +}) + +class Macros extends React.Component<{ + macroRecorder: MacroRecorder, + sendCommand: SendCommand, +}, any> { + constructor(props: any) { + super(props) + + this.state = { + isRecording: false, + macroExists: false, + } + + this.playLastRecordedMacro = this.playLastRecordedMacro.bind(this) + this.startRecording = this.startRecording.bind(this) + this.stopRecording = this.stopRecording.bind(this) + } + + startRecording(): void { + this.setState({ isRecording: true }) + this.props.macroRecorder.start() + } + + stopRecording(): void { + this.props.macroRecorder.stop() + this.setState({ isRecording: false, macroExists: true, }) + } + + 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 + const m = /wait (\d+)/.exec(command) + if (m) { + const sleepMillis = parseInt(m[1]) + if (sleepMillis > 5) { + await this.sleep(sleepMillis) + } + } else { + this.props.sendCommand(command, controllerState) + } + } + } + + render(): React.ReactNode { + return
+ Macros + + + + + +
+ } +} + +export default withStyles(styles)(Macros) diff --git a/website-client/src/components/PlayGame.tsx b/website-client/src/components/PlayGame.tsx index dd6f5a5..14436a3 100644 --- a/website-client/src/components/PlayGame.tsx +++ b/website-client/src/components/PlayGame.tsx @@ -13,6 +13,8 @@ import GamepadBinding from '../key-binding/GamepadBinding' import KeyboardBinding from '../key-binding/KeyboardBinding' import Controller from './Controller/Controller' import { ControllerState } from './Controller/ControllerState' +import MacroRecorder from './Macros/MacroRecorder' +import Macros from './Macros/Macros' // Can take a Theme as input. const styles = () => createStyles({ @@ -45,6 +47,8 @@ const styles = () => createStyles({ const setupMixedContent = " You may have to enable \"mixed content\" or \"insecure content\" for this connection in your browser's settings if the server your friend is hosting does not have SSL (a link that starts with https). Warning! This is insecure." class PlayGame extends React.Component { + macroRecorder = new MacroRecorder() + constructor(props: Readonly) { super(props) @@ -215,6 +219,11 @@ class PlayGame extends React.Component { 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))) } private toggleSendMode() { @@ -311,6 +320,7 @@ class PlayGame extends React.Component { mixerChannel: this.state.mixerChannel, }} /> +
URL Parameters for this page diff --git a/website-client/src/key-binding/KeyboardBinding.ts b/website-client/src/key-binding/KeyboardBinding.ts index 4f3788f..727327e 100644 --- a/website-client/src/key-binding/KeyboardBinding.ts +++ b/website-client/src/key-binding/KeyboardBinding.ts @@ -134,24 +134,24 @@ export default class KeyboardBinding extends KeyBinding { const action = keyMapping[keyName] if (action) { - const controllerState = this.controllerState as any; + const controllerState = this.controllerState as any if (action.dirName) { 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 {