Skip to content

Commit

Permalink
client: Support recording + running macros (#21)
Browse files Browse the repository at this point in the history
Resolves #10
  • Loading branch information
juharris authored Jun 9, 2020
1 parent 923091e commit aba829d
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 17 deletions.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
31 changes: 31 additions & 0 deletions website-client/src/components/Macros/MacroRecorder.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
}
89 changes: 89 additions & 0 deletions website-client/src/components/Macros/Macros.tsx
Original file line number Diff line number Diff line change
@@ -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<void> {
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 <div>
<Typography variant="h3">Macros</Typography>
<Grid container>
<Grid item hidden={this.state.isRecording}>
<Tooltip title="Start recording a macro" placement="top">
<Button onClick={this.startRecording}>
<span role='img' aria-label="Start recording a macro">🔴</span>
</Button>
</Tooltip>
</Grid>
<Grid item hidden={!this.state.isRecording}>
<Tooltip title="Stop recording the macro" placement="top" >
<Button onClick={this.stopRecording}>
<span role='img' aria-label="Stop recording the macro"></span>
</Button>
</Tooltip>
</Grid>
<Grid item hidden={!this.state.macroExists}>
<Tooltip title="Play the last macro recorded" placement="top" >
<Button onClick={this.playLastRecordedMacro}>
<span role='img' aria-label="Play the last macro recorded"></span>
</Button>
</Tooltip>
</Grid>
</Grid>
</div>
}
}

export default withStyles(styles)(Macros)
10 changes: 10 additions & 0 deletions website-client/src/components/PlayGame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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<any, any> {
macroRecorder = new MacroRecorder()

constructor(props: Readonly<any>) {
super(props)

Expand Down Expand Up @@ -215,6 +219,11 @@ class PlayGame extends React.Component<any, any> {
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() {
Expand Down Expand Up @@ -311,6 +320,7 @@ class PlayGame extends React.Component<any, any> {
mixerChannel: this.state.mixerChannel,
}} />
</div>
<Macros macroRecorder={this.macroRecorder} sendCommand={this.sendCommand} />
<div className={classes.urlParamsInfo}>
<Typography variant="h3" >URL Parameters for this page</Typography>
<Typography component="p">
Expand Down
26 changes: 13 additions & 13 deletions website-client/src/key-binding/KeyboardBinding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit aba829d

Please sign in to comment.