├── XArcade XInput Interface.url
├── webapp
├── .eslintrc
├── src
│ ├── config.js
│ ├── redux
│ │ ├── rootReducer.js
│ │ ├── createReducer.js
│ │ └── createStore.js
│ ├── App.js
│ ├── index.js
│ ├── index.css
│ ├── status
│ │ ├── reducer.js
│ │ └── actions.js
│ ├── mappings
│ │ ├── reducer.js
│ │ └── actions.js
│ ├── common.js
│ ├── Status.js
│ ├── UnmanagedTextField.js
│ ├── IconMenu.js
│ ├── MappingEntry.js
│ └── MappingList.js
├── .gitignore
├── package.json
└── public
│ └── index.html
├── Install Driver
├── App.config
├── Properties
│ └── AssemblyInfo.cs
├── Install Driver.csproj
├── Program.cs
└── app.manifest
├── XArcade XInput
├── App.config
├── packages.config
├── Properties
│ └── AssemblyInfo.cs
├── Program.cs
├── XArcade XInput.csproj
├── ScpDriverInterface
│ ├── X360Controller.cs
│ └── ScpBus.cs
├── ControllerManager.cs
├── RestServer.cs
└── KeyboardMapper.cs
├── script
├── get-scp-driver-installer.bat
└── build.bat
├── mappings
├── X-Arcade 2 player D-Pad.json
├── X-Arcade 1 player D-Pad DualStick.json
├── X-Arcade 2 Player Analog.json
└── X-Arcade 1 player Analog DualStick.json
├── .github
└── workflows
│ └── ci.yml
├── XArcade XInput.sln
├── .gitattributes
├── README.md
└── .gitignore
/XArcade XInput Interface.url:
--------------------------------------------------------------------------------
1 | [InternetShortcut]
2 | URL=http://localhost:32123/
3 |
--------------------------------------------------------------------------------
/webapp/.eslintrc:
--------------------------------------------------------------------------------
1 | /Users/mike/Work/xarcade-xinput/webapp/node_modules/react-scripts/eslintrc
--------------------------------------------------------------------------------
/webapp/src/config.js:
--------------------------------------------------------------------------------
1 | const queryParams = new URLSearchParams(window.location.search)
2 |
3 | export const API_URL = queryParams.get('API_URL') || ''
4 |
--------------------------------------------------------------------------------
/Install Driver/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/XArcade XInput/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/XArcade XInput/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/webapp/src/redux/rootReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 |
3 | import status from '../status/reducer'
4 | import mappings from '../mappings/reducer'
5 |
6 | const rootReducer = combineReducers({
7 | status,
8 | mappings,
9 | })
10 |
11 | export default rootReducer
12 |
--------------------------------------------------------------------------------
/webapp/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | npm-debug.log*
16 | yarn-debug.log*
17 | yarn-error.log*
18 |
19 |
--------------------------------------------------------------------------------
/webapp/src/redux/createReducer.js:
--------------------------------------------------------------------------------
1 | export default function createReducer (initialState, handlers) {
2 | return function reducer (state = initialState, action) {
3 | if (handlers.hasOwnProperty(action.type)) {
4 | return handlers[action.type](state, action)
5 | } else {
6 | return state
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/script/get-scp-driver-installer.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | rd /S /Q "Scp Driver Installer"
4 | curl -L https://github.com/mogzol/ScpDriverInterface/releases/download/1.1/ScpDriverInterface_v1.1.zip --output "Scp Driver Installer.zip"
5 | 7z x -o"ScpDriverTemp" "Scp Driver Installer.zip"
6 | move /Y "ScpDriverTemp\Driver Installer" "Scp Driver Installer"
7 | rd /S /Q ScpDriverTemp
8 | del /F /S /Q "Scp Driver Installer.zip"
--------------------------------------------------------------------------------
/webapp/src/App.js:
--------------------------------------------------------------------------------
1 | import {
2 | React,
3 | PureComponent,
4 | } from './common'
5 |
6 | import Status from './Status'
7 | import MappingList from './MappingList'
8 |
9 | class App extends PureComponent {
10 | state = {
11 | isControllerRunning: false,
12 | isKeyboardRunning: false,
13 | }
14 |
15 | render() {
16 | return
17 |
18 |
19 |
20 | }
21 | }
22 |
23 | export default App
24 |
--------------------------------------------------------------------------------
/script/build.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | REM Build webapp
4 | rd /S /Q webapp\build\
5 | pushd webapp\
6 | call yarn install
7 | call yarn run build
8 | popd
9 |
10 | REM Grab Scp Driver Installer
11 | call script\get-scp-driver-installer.bat
12 |
13 | REM "Clean" isn't a total clean.
14 | del /F /S /Q "XArcade XInput"\bin
15 | rd /S /Q "XArcade XInput"\bin
16 |
17 | REM Build Project
18 | nuget restore
19 | MSBuild.exe "XArcade XInput".sln /t:Clean,Build /p:Configuration=Release
20 |
--------------------------------------------------------------------------------
/webapp/src/index.js:
--------------------------------------------------------------------------------
1 | import injectTapEventPlugin from 'react-tap-event-plugin'
2 |
3 | // Needed for onTouchTap
4 | // http://stackoverflow.com/a/34015469/988941
5 | injectTapEventPlugin()
6 |
7 | import React from 'react'
8 | import ReactDOM from 'react-dom'
9 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
10 | import { Provider } from 'react-redux'
11 |
12 | import App from './App'
13 | import createStore from './redux/createStore'
14 |
15 | //import 'bootstrap/dist/css/bootstrap.css'
16 | import './index.css'
17 |
18 | const store = createStore({})
19 |
20 | ReactDOM.render(
21 |
22 |
23 |
24 |
25 | ,
26 | document.getElementById('root')
27 | )
28 |
--------------------------------------------------------------------------------
/webapp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webapp",
3 | "homepage": "./",
4 | "version": "0.1.0",
5 | "private": true,
6 | "dependencies": {},
7 | "devDependencies": {
8 | "brace": "^0.10.0",
9 | "material-ui": "next",
10 | "react": "^15.5.4",
11 | "react-ace": "^4.2.1",
12 | "react-dom": "^15.5.4",
13 | "react-redux": "^5.0.4",
14 | "react-scripts": "0.9.5",
15 | "react-tap-event-plugin": "^2.0.1",
16 | "redux": "^3.6.0",
17 | "redux-logger": "^3.0.1",
18 | "redux-promise-middleware": "^4.2.0",
19 | "redux-thunk": "^2.2.0"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test --env=jsdom",
25 | "eject": "react-scripts eject"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/webapp/src/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | th,
4 | td {
5 | font-family: Roboto, sans-serif;
6 | }
7 |
8 | .monospace,
9 | .form-control.monospace,
10 | {
11 | font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace;
12 | /*font-size: 0.7rem;*/
13 | }
14 |
15 | .App {
16 | max-width: 600px;
17 | margin: auto;
18 | }
19 |
20 | [class*="MuiDialog-dialog"] {
21 | width: 95% !important;
22 | }
23 |
24 | /*
25 | html,
26 | body,
27 | #root,
28 | .App {
29 | height: 100%;
30 | }
31 |
32 | .App {
33 | display: flex;
34 | flex-direction: column;
35 | }
36 |
37 | .Mapping {
38 | flex: 1;
39 | display: flex;
40 | flex-direction: column;
41 | }
42 |
43 | .Mapping .form-group {
44 | height: 100%;
45 | }
46 |
47 | .Mapping textarea {
48 | height: calc(100% - 50px);
49 | }
50 | */
51 |
--------------------------------------------------------------------------------
/webapp/src/status/reducer.js:
--------------------------------------------------------------------------------
1 | import createReducer from '../redux/createReducer'
2 |
3 | import * as actions from './actions'
4 |
5 | const initialState = {
6 | isKeyboardRunning: false,
7 | isControllerRunning: false,
8 | hostname: null,
9 | }
10 |
11 | export default createReducer (initialState, {
12 | [`${actions.STATUS_SET_KEYBOARDMAPPER}_START`] (state, action) {
13 | return {
14 | ...state,
15 | isKeyboardRunning: action.payload,
16 | }
17 | },
18 |
19 | [`${actions.STATUS_SET_CONTROLLERMANAGER}_START`] (state, action) {
20 | return {
21 | ...state,
22 | isControllerRunning: action.payload,
23 | }
24 | },
25 |
26 | [`${actions.STATUS_REFRESH}_SUCCESS`] (state, action) {
27 | return {
28 | ...state,
29 | ...action.payload,
30 | }
31 | },
32 | })
33 |
--------------------------------------------------------------------------------
/webapp/src/redux/createStore.js:
--------------------------------------------------------------------------------
1 | import {
2 | createStore as _createStore,
3 | applyMiddleware,
4 | } from 'redux'
5 | import thunk from 'redux-thunk'
6 | import promise from 'redux-promise-middleware'
7 | import { createLogger } from 'redux-logger'
8 |
9 | const middleware = [
10 | thunk,
11 | promise({ promiseTypeSuffixes: [ 'START', 'SUCCESS', 'ERROR' ] }),
12 | createLogger(),
13 | ]
14 |
15 | const createStoreWithMiddleware = applyMiddleware(...middleware)(_createStore)
16 |
17 | function getRootReducer () {
18 | // eslint-disable-next-line global-require
19 | return require('./rootReducer').default
20 | }
21 |
22 | export default function createStore (initialState) {
23 | const store = createStoreWithMiddleware(getRootReducer(), initialState)
24 |
25 | if (module.hot) {
26 | module.hot.accept('./rootReducer', () => {
27 | store.replaceReducer(getRootReducer())
28 | })
29 | }
30 |
31 | return store
32 | }
33 |
--------------------------------------------------------------------------------
/mappings/X-Arcade 2 player D-Pad.json:
--------------------------------------------------------------------------------
1 | {
2 | "LShiftKey": [0, "A"],
3 | "Z": [0, "B"],
4 | "LControlKey": [0, "X"],
5 | "LMenu": [0, "Y"],
6 |
7 | "D1": [0, "Start"],
8 | "D3": [0, "Back"],
9 | "C": [0, "LeftBumper"],
10 | "Space": [0, "RightBumper"],
11 |
12 | "Up": [0, "Up"],
13 | "Down": [0, "Down"],
14 | "Left": [0, "Left"],
15 | "Right": [0, "Right"],
16 |
17 | "D5": [0, "LeftTrigger"],
18 | "X": [0, "RightTrigger"],
19 |
20 |
21 |
22 | "W": [1, "A"],
23 | "E": [1, "B"],
24 | "A": [1, "X"],
25 | "S": [1, "Y"],
26 |
27 | "D2": [1, "Start"],
28 | "D4": [1, "Back"],
29 | "Oem6": [1, "LeftBumper"],
30 | "OemCloseBrackets": [1, "LeftBumper"],
31 | "Q": [1, "RightBumper"],
32 |
33 | "R": [1, "Up"],
34 | "F": [1, "Down"],
35 | "D": [1, "Left"],
36 | "G": [1, "Right"],
37 |
38 | "D6": [1, "LeftTrigger"],
39 | "OemOpenBrackets": [1, "RightTrigger"]
40 | }
--------------------------------------------------------------------------------
/mappings/X-Arcade 1 player D-Pad DualStick.json:
--------------------------------------------------------------------------------
1 | {
2 | "LShiftKey": [0, "A"],
3 | "Z": [0, "B"],
4 | "LControlKey": [0, "X"],
5 | "LMenu": [0, "Y"],
6 |
7 | "D1": [0, "Start"],
8 | "D3": [0, "Back"],
9 | "C": [0, "LeftBumper"],
10 | "Space": [0, "RightBumper"],
11 |
12 | "Up": [0, "Up"],
13 | "Down": [0, "Down"],
14 | "Left": [0, "Left"],
15 | "Right": [0, "Right"],
16 |
17 | "D5": [0, "LeftTrigger"],
18 | "X": [0, "RightTrigger"],
19 |
20 |
21 |
22 | "W": [0, "A"],
23 | "E": [0, "B"],
24 | "A": [0, "X"],
25 | "S": [0, "Y"],
26 |
27 | "D2": [0, "Start"],
28 | "D4": [0, "Back"],
29 | "Oem6": [0, "LeftBumper"],
30 | "OemCloseBrackets": [0, "LeftBumper"],
31 | "Q": [0, "RightBumper"],
32 |
33 | "R": [0, "RightStickY"],
34 | "F": [0, "RightStickY", -1],
35 | "D": [0, "RightStickX", -1],
36 | "G": [0, "RightStickX"],
37 |
38 | "D6": [0, "LeftTrigger"],
39 | "OemOpenBrackets": [0, "RightTrigger"]
40 | }
--------------------------------------------------------------------------------
/webapp/src/mappings/reducer.js:
--------------------------------------------------------------------------------
1 | import createReducer from '../redux/createReducer'
2 |
3 | import * as actions from './actions'
4 |
5 | const initialState = {
6 | currentMapping: '',
7 | mappingNames: [],
8 | all: {},
9 | currentEditing: null,
10 | editingStartedAt: null,
11 | }
12 |
13 | export default createReducer (initialState, {
14 | [`${actions.MAPPINGS_SET_CURRENT}_START`] (state, action) {
15 | return {
16 | ...state,
17 | currentMapping: action.payload,
18 | }
19 | },
20 |
21 | [`${actions.MAPPINGS_REFRESH}_SUCCESS`] (state, action) {
22 | return {
23 | ...state,
24 | currentMapping: action.payload.currentMapping,
25 | all: action.payload.mappings,
26 | mappingNames: Object.keys(action.payload.mappings),
27 | }
28 | },
29 |
30 | [actions.MAPPINGS_START_EDITING] (state, action) {
31 | return {
32 | ...state,
33 | currentEditing: action.payload,
34 | editingStartedAt: new Date(),
35 | }
36 | },
37 | })
38 |
--------------------------------------------------------------------------------
/mappings/X-Arcade 2 Player Analog.json:
--------------------------------------------------------------------------------
1 | {
2 | "LShiftKey": [0, "A"],
3 | "Z": [0, "B"],
4 | "LControlKey": [0, "X"],
5 | "LMenu": [0, "Y"],
6 |
7 | "D1": [0, "Start"],
8 | "D3": [0, "Back"],
9 | "C": [0, "LeftBumper"],
10 | "Space": [0, "RightBumper"],
11 |
12 | "Up": [0, "LeftStickY"],
13 | "Down": [0, "LeftStickY", -1],
14 | "Left": [0, "LeftStickX", -1],
15 | "Right": [0, "LeftStickX"],
16 |
17 | "D5": [0, "LeftTrigger"],
18 | "X": [0, "RightTrigger"],
19 |
20 |
21 |
22 | "W": [1, "A"],
23 | "E": [1, "B"],
24 | "A": [1, "X"],
25 | "S": [1, "Y"],
26 |
27 | "D2": [1, "Start"],
28 | "D4": [1, "Back"],
29 | "Oem6": [1, "LeftBumper"],
30 | "OemCloseBrackets": [1, "LeftBumper"],
31 | "Q": [1, "RightBumper"],
32 |
33 | "R": [1, "LeftStickY"],
34 | "F": [1, "LeftStickY", -1],
35 | "D": [1, "LeftStickX", -1],
36 | "G": [1, "LeftStickX"],
37 |
38 | "D6": [1, "LeftTrigger"],
39 | "OemOpenBrackets": [1, "RightTrigger"]
40 | }
--------------------------------------------------------------------------------
/webapp/src/common.js:
--------------------------------------------------------------------------------
1 | export { default as React, PureComponent } from 'react'
2 | export { connect } from 'react-redux'
3 |
4 | export { default as Switch, LabelSwitch } from 'material-ui/Switch'
5 | export { default as Button } from 'material-ui/Button'
6 | export { default as TextField } from 'material-ui/TextField'
7 | export { default as Icon } from 'material-ui/Icon'
8 | export { default as IconButton } from 'material-ui/IconButton'
9 | export { default as Text } from 'material-ui/Text'
10 |
11 | export {
12 | Dialog,
13 | DialogActions,
14 | DialogTitle,
15 | DialogContent,
16 | DialogContentText,
17 | } from 'material-ui/Dialog'
18 |
19 | export {
20 | List,
21 | ListSubheader,
22 | ListItem,
23 | ListItemText,
24 | ListItemSecondaryAction,
25 | ListItemIcon,
26 | } from 'material-ui/List'
27 |
28 | export {
29 | Menu,
30 | MenuItem,
31 | } from 'material-ui/Menu'
32 |
33 | export { default as IconMenu } from './IconMenu'
34 | export { default as UnmanagedTextField } from './UnmanagedTextField'
35 |
--------------------------------------------------------------------------------
/mappings/X-Arcade 1 player Analog DualStick.json:
--------------------------------------------------------------------------------
1 | {
2 | "LShiftKey": [0, "A"],
3 | "Z": [0, "B"],
4 | "LControlKey": [0, "X"],
5 | "LMenu": [0, "Y"],
6 |
7 | "D1": [0, "Start"],
8 | "D3": [0, "Back"],
9 | "C": [0, "LeftBumper"],
10 | "Space": [0, "RightBumper"],
11 |
12 | "Up": [0, "LeftStickY"],
13 | "Down": [0, "LeftStickY", -1],
14 | "Left": [0, "LeftStickX", -1],
15 | "Right": [0, "LeftStickX"],
16 |
17 | "D5": [0, "LeftTrigger"],
18 | "X": [0, "RightTrigger"],
19 |
20 |
21 |
22 | "W": [0, "A"],
23 | "E": [0, "B"],
24 | "A": [0, "X"],
25 | "S": [0, "Y"],
26 |
27 | "D2": [0, "Start"],
28 | "D4": [0, "Back"],
29 | "Oem6": [0, "LeftBumper"],
30 | "OemCloseBrackets": [0, "LeftBumper"],
31 | "Q": [0, "RightBumper"],
32 |
33 | "R": [0, "RightStickY"],
34 | "F": [0, "RightStickY", -1],
35 | "D": [0, "RightStickX", -1],
36 | "G": [0, "RightStickX"],
37 |
38 | "D6": [0, "LeftTrigger"],
39 | "OemOpenBrackets": [0, "RightTrigger"]
40 | }
--------------------------------------------------------------------------------
/webapp/src/Status.js:
--------------------------------------------------------------------------------
1 | import {
2 | React,
3 | PureComponent,
4 | ListSubheader,
5 | LabelSwitch,
6 | Button,
7 | connect,
8 | } from './common'
9 |
10 | import * as actions from './status/actions'
11 |
12 | class Status extends PureComponent {
13 | componentDidMount () {
14 | this.props.refresh()
15 | }
16 |
17 | render () {
18 | const isRunning = this.props.isKeyboardRunning && this.props.isControllerRunning
19 | let heading = isRunning
20 | ? `Running`
21 | : 'Not Running'
22 |
23 | if (this.props.hostname) {
24 | heading = `${heading} on ${this.props.hostname}`
25 | }
26 |
27 | return
28 | Status: {heading}
29 | this.props.setAll(shouldEnable)}
33 | />
34 |
35 |
36 |
37 |
38 | }
39 | }
40 |
41 | export default connect((state) => ({
42 | ...state.status,
43 | }), actions)(Status)
44 |
--------------------------------------------------------------------------------
/webapp/src/UnmanagedTextField.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 | import TextField from 'material-ui/TextField'
3 |
4 | export default class UnmanagedTextField extends PureComponent {
5 | TextField = null
6 |
7 | static defaultProps = {
8 | onChange () {},
9 | }
10 |
11 | state = {
12 | value: null,
13 | }
14 |
15 | /*
16 | componentDidMount () {
17 | this.setState({ value: this.props.defaultValue || '' })
18 | }
19 | */
20 |
21 | /*
22 | componentWillReceiveProps (nextProps) {
23 | if (nextProps.defaultValue !== this.props.defaultValue) {
24 | this.setState({ value: null })
25 | }
26 | }
27 | */
28 |
29 | render () {
30 | const {
31 | defaultValue,
32 | ...props,
33 | } = this.props
34 |
35 | const value = this.state.value !== null
36 | ? this.state.value
37 | : defaultValue
38 |
39 | return this.TextField = x}
44 | />
45 | }
46 |
47 | handleChange = (event) => {
48 | this.setState({ value: event.target.value || '' })
49 | this.props.onChange(event)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/webapp/src/IconMenu.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 | import IconButton from 'material-ui/IconButton';
3 | import {
4 | Menu,
5 | } from 'material-ui/Menu';
6 |
7 | export default class IconMenu extends PureComponent {
8 | state = {
9 | menuAnchor: null,
10 | isMenuOpen: false,
11 | }
12 |
13 | componentWillUnmount () {
14 | if (this._closeTimeout) {
15 | clearTimeout(this._closeTimeout)
16 | }
17 | }
18 |
19 | render () {
20 | const {
21 | icon,
22 | children,
23 | ...props,
24 | } = this.props
25 |
26 | return
27 | {icon}
28 |
36 |
37 | }
38 |
39 | openMenu = (event) => {
40 | this.setState({
41 | menuAnchor: event.currentTarget,
42 | isMenuOpen: true,
43 | })
44 | }
45 |
46 | closeMenu = () => {
47 | this._closeTimeout = null
48 | this.setState({
49 | menuAnchor: null,
50 | isMenuOpen: false,
51 | })
52 | }
53 |
54 | closeMenuWithDelay = () => {
55 | if (this._closeTimeout) {
56 | clearTimeout(this._closeTimeout)
57 | }
58 |
59 | this._closeTimeout = setTimeout(this.closeMenu, 200)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/webapp/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
18 | X-Arcade XInput
19 |
20 |
21 |
22 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/Install Driver/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("Install Driver")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("Install Driver")]
13 | [assembly: AssemblyCopyright("Copyright © 2018")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("6c663b31-ed95-43ca-9cca-d2349f8e491e")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/XArcade XInput/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("XArcade XInput")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("XArcade XInput")]
13 | [assembly: AssemblyCopyright("Copyright © 2017")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("2a7f171b-f9e0-42de-b116-9f9ff812caaa")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | build:
13 | runs-on: windows-latest
14 |
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v4
18 |
19 | - name: Setup MSBuild.exe
20 | uses: microsoft/setup-msbuild@v2
21 |
22 | - name: Setup nuget
23 | uses: nuget/setup-nuget@v2
24 |
25 | - name: build
26 | run: script\build.bat
27 |
28 | - name: Upload build artifacts
29 | uses: actions/upload-artifact@v4
30 | with:
31 | name: build
32 | path: XArcade XInput\bin\Release
33 |
34 | deploy:
35 | needs:
36 | - build
37 |
38 | runs-on: windows-latest
39 |
40 | steps:
41 | - name: Download build
42 | if: endsWith(github.ref, '/master')
43 | uses: actions/download-artifact@v4
44 | with:
45 | name: build
46 | path: build
47 |
48 | - name: Create archive
49 | if: endsWith(github.ref, '/master')
50 | run: |
51 | 7z a xarcade-xinput.zip ".\build\*"
52 |
53 | - name: Release
54 | if: endsWith(github.ref, '/master')
55 | uses: marvinpinto/action-automatic-releases@latest
56 | with:
57 | repo_token: ${{ secrets.GITHUB_TOKEN }}
58 | automatic_release_tag: latest
59 | prerelease: false
60 | title: Release ${{ github.sha }}
61 | files: |
62 | xarcade-xinput.zip
63 |
--------------------------------------------------------------------------------
/webapp/src/status/actions.js:
--------------------------------------------------------------------------------
1 | import { API_URL } from '../config'
2 |
3 | export const STATUS_SET_KEYBOARDMAPPER = 'STATUS_SET_KEYBOARDMAPPER'
4 | export const STATUS_SET_CONTROLLERMANAGER = 'STATUS_SET_CONTROLLERMANAGER'
5 | export const STATUS_REFRESH = 'STATUS_REFRESH'
6 |
7 | export function setKeyboardmapper (shouldEnable) {
8 | const endpoint = shouldEnable
9 | ? `${API_URL}/api/keyboard/start`
10 | : `${API_URL}/api/keyboard/stop`
11 |
12 | return {
13 | type: STATUS_SET_KEYBOARDMAPPER,
14 | payload: {
15 | promise: fetch(endpoint, { method: 'POST' }),
16 | data: shouldEnable,
17 | },
18 | }
19 | }
20 |
21 | export function setControllermanager (shouldEnable) {
22 | const endpoint = shouldEnable
23 | ? `${API_URL}/api/controller/start`
24 | : `${API_URL}/api/controller/stop`
25 |
26 | return {
27 | type: STATUS_SET_CONTROLLERMANAGER,
28 | payload: {
29 | promise: fetch(endpoint, { method: 'POST' }),
30 | data: shouldEnable,
31 | },
32 | }
33 | }
34 |
35 | export function setAll (shouldEnable) { return (dispatch, getState) => {
36 | return dispatch(setKeyboardmapper(shouldEnable))
37 | .then(() => dispatch(setControllermanager(shouldEnable)))
38 | } }
39 |
40 | export function restartAll () { return (dispatch, getState) => {
41 | return dispatch(setAll(false))
42 | .then(() => dispatch(setAll(true)))
43 | } }
44 |
45 | export function refresh () {
46 | return {
47 | type: STATUS_REFRESH,
48 | payload: {
49 | promise: fetch(`${API_URL}/api/status`).then(x => x.json()),
50 | },
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/XArcade XInput.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 14
4 | VisualStudioVersion = 14.0.25420.1
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XArcade XInput", "XArcade XInput\XArcade XInput.csproj", "{2A7F171B-F9E0-42DE-B116-9F9FF812CAAA}"
7 | ProjectSection(ProjectDependencies) = postProject
8 | {6C663B31-ED95-43CA-9CCA-D2349F8E491E} = {6C663B31-ED95-43CA-9CCA-D2349F8E491E}
9 | EndProjectSection
10 | EndProject
11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Install Driver", "Install Driver\Install Driver.csproj", "{6C663B31-ED95-43CA-9CCA-D2349F8E491E}"
12 | EndProject
13 | Global
14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
15 | Debug|Any CPU = Debug|Any CPU
16 | Release|Any CPU = Release|Any CPU
17 | EndGlobalSection
18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
19 | {2A7F171B-F9E0-42DE-B116-9F9FF812CAAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
20 | {2A7F171B-F9E0-42DE-B116-9F9FF812CAAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
21 | {2A7F171B-F9E0-42DE-B116-9F9FF812CAAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
22 | {2A7F171B-F9E0-42DE-B116-9F9FF812CAAA}.Release|Any CPU.Build.0 = Release|Any CPU
23 | {6C663B31-ED95-43CA-9CCA-D2349F8E491E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24 | {6C663B31-ED95-43CA-9CCA-D2349F8E491E}.Debug|Any CPU.Build.0 = Debug|Any CPU
25 | {6C663B31-ED95-43CA-9CCA-D2349F8E491E}.Release|Any CPU.ActiveCfg = Release|Any CPU
26 | {6C663B31-ED95-43CA-9CCA-D2349F8E491E}.Release|Any CPU.Build.0 = Release|Any CPU
27 | EndGlobalSection
28 | GlobalSection(SolutionProperties) = preSolution
29 | HideSolutionNode = FALSE
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/webapp/src/mappings/actions.js:
--------------------------------------------------------------------------------
1 | import { API_URL } from '../config'
2 |
3 | export const MAPPINGS_REFRESH = 'MAPPINGS_REFRESH'
4 | export const MAPPINGS_SAVE = 'MAPPINGS_SAVE'
5 | export const MAPPINGS_DELETE_MAPPING = 'MAPPINGS_DELETE_MAPPING'
6 | export const MAPPINGS_RENAME_MAPPING = 'MAPPINGS_RENAME_MAPPING'
7 | export const MAPPINGS_SET_CURRENT = 'MAPPINGS_SET_CURRENT'
8 | export const MAPPINGS_START_EDITING = 'MAPPINGS_START_EDITING'
9 |
10 | export function refresh () {
11 | return {
12 | type: MAPPINGS_REFRESH,
13 | payload: {
14 | promise: fetch(`${API_URL}/api/keyboard/mapping`).then(x => x.json()),
15 | },
16 | }
17 | }
18 |
19 | export function saveMapping (name, mapping) {
20 | return {
21 | type: MAPPINGS_SAVE,
22 | payload: {
23 | promise: fetch(`${API_URL}/api/keyboard/mapping`, {
24 | method: 'POST',
25 | body: JSON.stringify({
26 | name,
27 | mapping,
28 | }),
29 | }),
30 | },
31 | }
32 | }
33 |
34 | export function setCurrent (name) {
35 | return {
36 | type: MAPPINGS_SET_CURRENT,
37 | payload: {
38 | promise: fetch(`${API_URL}/api/keyboard/mapping/current`, {
39 | method: 'POST',
40 | body: name,
41 | }),
42 | data: name,
43 | },
44 | }
45 | }
46 |
47 | export function startEditing (name) {
48 | return {
49 | type: MAPPINGS_START_EDITING,
50 | payload: name,
51 | }
52 | }
53 |
54 | export function deleteMapping (name) {
55 | return {
56 | type: MAPPINGS_DELETE_MAPPING,
57 | payload: {
58 | promise: fetch(`${API_URL}/api/keyboard/mapping`, {
59 | method: 'DELETE',
60 | body: name,
61 | }),
62 | data: name,
63 | },
64 | }
65 | }
66 |
67 | export function renameMapping (name, newName) {
68 | return {
69 | type: MAPPINGS_RENAME_MAPPING,
70 | payload: {
71 | promise: fetch(`${API_URL}/api/keyboard/mapping/rename`, {
72 | method: 'POST',
73 | body: JSON.stringify({
74 | name,
75 | newName,
76 | }),
77 | }),
78 | data: {
79 | name,
80 | newName,
81 | },
82 | },
83 | }
84 | }
--------------------------------------------------------------------------------
/Install Driver/Install Driver.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {6C663B31-ED95-43CA-9CCA-D2349F8E491E}
8 | Exe
9 | Install_Driver
10 | Install Driver
11 | v4.5.2
12 | 512
13 | true
14 |
15 |
16 | AnyCPU
17 | true
18 | full
19 | false
20 | bin\Debug\
21 | DEBUG;TRACE
22 | prompt
23 | 4
24 |
25 |
26 | AnyCPU
27 | pdbonly
28 | true
29 | bin\Release\
30 | TRACE
31 | prompt
32 | 4
33 |
34 |
35 | app.manifest
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | Designer
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | *.inf text eol=crlf
7 |
8 | ###############################################################################
9 | # Set default behavior for command prompt diff.
10 | #
11 | # This is need for earlier builds of msysgit that does not have it on by
12 | # default for csharp files.
13 | # Note: This is only used by command line
14 | ###############################################################################
15 | #*.cs diff=csharp
16 |
17 | ###############################################################################
18 | # Set the merge driver for project and solution files
19 | #
20 | # Merging from the command prompt will add diff markers to the files if there
21 | # are conflicts (Merging from VS is not affected by the settings below, in VS
22 | # the diff markers are never inserted). Diff markers may cause the following
23 | # file extensions to fail to load in VS. An alternative would be to treat
24 | # these files as binary and thus will always conflict and require user
25 | # intervention with every merge. To do so, just uncomment the entries below
26 | ###############################################################################
27 | #*.sln merge=binary
28 | #*.csproj merge=binary
29 | #*.vbproj merge=binary
30 | #*.vcxproj merge=binary
31 | #*.vcproj merge=binary
32 | #*.dbproj merge=binary
33 | #*.fsproj merge=binary
34 | #*.lsproj merge=binary
35 | #*.wixproj merge=binary
36 | #*.modelproj merge=binary
37 | #*.sqlproj merge=binary
38 | #*.wwaproj merge=binary
39 |
40 | ###############################################################################
41 | # behavior for image files
42 | #
43 | # image files are treated as binary by default.
44 | ###############################################################################
45 | #*.jpg binary
46 | #*.png binary
47 | #*.gif binary
48 |
49 | ###############################################################################
50 | # diff behavior for common document formats
51 | #
52 | # Convert binary document formats to text before diffing them. This feature
53 | # is only available from the command line. Turn it on by uncommenting the
54 | # entries below.
55 | ###############################################################################
56 | #*.doc diff=astextplain
57 | #*.DOC diff=astextplain
58 | #*.docx diff=astextplain
59 | #*.DOCX diff=astextplain
60 | #*.dot diff=astextplain
61 | #*.DOT diff=astextplain
62 | #*.pdf diff=astextplain
63 | #*.PDF diff=astextplain
64 | #*.rtf diff=astextplain
65 | #*.RTF diff=astextplain
66 |
--------------------------------------------------------------------------------
/webapp/src/MappingEntry.js:
--------------------------------------------------------------------------------
1 | import {
2 | React,
3 | PureComponent,
4 | ListItem,
5 | ListItemText,
6 | ListItemSecondaryAction,
7 | ListItemIcon,
8 | MenuItem,
9 | Icon,
10 | IconMenu,
11 | connect,
12 | } from './common'
13 |
14 | import * as actions from './mappings/actions'
15 |
16 | class MappingEntry extends PureComponent {
17 | menu = null
18 |
19 | render () {
20 | let icon = null
21 |
22 | if (this.props.isActive) {
23 | icon =
24 | check_circle
25 |
26 | }
27 |
28 | return this.menuIcon = x}>
29 | {icon}
30 |
31 |
32 | this.menu = x}>
33 |
39 |
40 |
46 |
47 |
53 |
54 |
60 |
61 |
62 |
63 | }
64 |
65 | makeActive = () => {
66 | this.menu.closeMenuWithDelay()
67 |
68 | this.props.setCurrent(this.props.name)
69 | .then(this.props.refresh)
70 | }
71 |
72 | startEditing = () => {
73 | this.menu.closeMenuWithDelay()
74 |
75 | this.props.startEditing(this.props.name)
76 | }
77 |
78 | requestDelete = () => {
79 | if (!confirm(`Really delete ${this.props.name}? This cannot be undone.`)) {
80 | return
81 | }
82 |
83 | this.menu.closeMenuWithDelay()
84 | this.props.deleteMapping(this.props.name)
85 | .then(this.props.refresh)
86 | }
87 |
88 | startRename = () => {
89 | const newName = prompt(`New name for ${this.props.name}`, this.props.name)
90 |
91 | if (newName == null) {
92 | return
93 | }
94 |
95 | this.menu.closeMenuWithDelay()
96 | this.props.renameMapping(this.props.name, newName)
97 | .then(this.props.refresh)
98 | }
99 | }
100 |
101 | export default connect(null, actions)(MappingEntry)
102 |
--------------------------------------------------------------------------------
/Install Driver/Program.cs:
--------------------------------------------------------------------------------
1 | namespace Install_Driver {
2 | class Program {
3 | static void Main (string[] args) {
4 | InstallDriver();
5 | SetupFirewall();
6 | }
7 |
8 | static void InstallDriver() {
9 | var driverInstallerDir = System.IO.Path.Combine(
10 | System.Environment.CurrentDirectory,
11 | "Scp Driver Installer"
12 | );
13 |
14 | RunCommand(new System.Diagnostics.ProcessStartInfo {
15 | FileName = System.IO.Path.Combine(
16 | driverInstallerDir,
17 | "ScpDriverInstaller.exe"
18 | ),
19 | Arguments = "--install --quiet",
20 | WorkingDirectory = driverInstallerDir,
21 | });
22 | }
23 |
24 | static void SetupFirewall() {
25 | RunCommand(new System.Diagnostics.ProcessStartInfo {
26 | FileName = "netsh",
27 | Arguments = "advfirewall firewall add rule name=\"XArcade XInput\" dir=in action=allow protocol=TCP localport=32123",
28 | }, true);
29 |
30 | RunCommand(new System.Diagnostics.ProcessStartInfo {
31 | FileName = "netsh",
32 | Arguments = "http add urlacl url=http://+:32123/ user=Everyone",
33 | }, true);
34 | }
35 |
36 | static System.Diagnostics.Process RunCommand (System.Diagnostics.ProcessStartInfo startInfo, bool allowFail = false) {
37 | System.Console.WriteLine($"Running '{startInfo.FileName} {startInfo.Arguments}'");
38 |
39 | startInfo.UseShellExecute = false;
40 | startInfo.RedirectStandardOutput = true;
41 | startInfo.RedirectStandardError = true;
42 |
43 | var proc = new System.Diagnostics.Process {
44 | StartInfo = startInfo,
45 | };
46 |
47 | proc.OutputDataReceived += (sender, e) => System.Console.WriteLine(e.Data);
48 | proc.ErrorDataReceived += (sender, e) => System.Console.WriteLine(e.Data);
49 |
50 | try {
51 | proc.Start();
52 | } catch (System.Exception err) {
53 | System.Console.WriteLine(err.Message);
54 | WaitAndExit(1);
55 | }
56 |
57 | proc.BeginOutputReadLine();
58 | proc.BeginErrorReadLine();
59 | proc.WaitForExit();
60 |
61 | if (!allowFail && proc.ExitCode > 0) {
62 | System.Console.WriteLine($"Command exited with code {proc.ExitCode}");
63 | WaitAndExit(proc.ExitCode);
64 | }
65 |
66 | return proc;
67 | }
68 |
69 | static void WaitAndExit(int exitCode) {
70 | if (exitCode > 0) {
71 | System.Console.WriteLine("Press any key to continue");
72 | System.Console.ReadKey();
73 | }
74 | System.Environment.Exit(exitCode);
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/XArcade XInput/Program.cs:
--------------------------------------------------------------------------------
1 | namespace XArcade_XInput {
2 | class Program {
3 | static public ControllerManager ControllerManagerInstance;
4 | static public KeyboardMapper KeyboardMapperInstance;
5 | static public RestServer RestServerInstance;
6 | static public bool IsDebug = false;
7 | static public bool ForceDefaultMapping = false;
8 | static public bool ShouldOpenUI = true;
9 | static public string InitialMappingName;
10 | static public bool ShouldStartDisabled = false;
11 |
12 | [System.Runtime.InteropServices.DllImport("Kernel32")]
13 | private static extern bool SetConsoleCtrlHandler(ConsoleCtrlHandler Handler, bool Add);
14 |
15 | private delegate bool ConsoleCtrlHandler(int Signal);
16 | private static ConsoleCtrlHandler ConsoleCtrlHandlerRef;
17 |
18 | static void Main (string[] args) {
19 | for (var i = 0; i < args.Length; i++) {
20 | if (args[i] == "--debug") {
21 | IsDebug = true;
22 | }
23 |
24 | if (args[i] == "--default") {
25 | ForceDefaultMapping = true;
26 | }
27 |
28 | if (args[i] == "--skip-ui") {
29 | ShouldOpenUI = false;
30 | }
31 |
32 | if (args[i] == "--mapping") {
33 | InitialMappingName = args[i + 1];
34 | i++;
35 | }
36 |
37 | if (args[i] == "--start-disabled") {
38 | ShouldStartDisabled = true;
39 | }
40 | }
41 |
42 | RestServerInstance = new RestServer();
43 | KeyboardMapperInstance = new KeyboardMapper();
44 | ControllerManagerInstance = new ControllerManager();
45 |
46 | KeyboardMapperInstance.OnParse += (s, e) => {
47 | if (ControllerManagerInstance.IsRunning) {
48 | ControllerManagerInstance.Stop();
49 | ControllerManagerInstance.Start();
50 | }
51 | };
52 |
53 | RestServerInstance.Start();
54 |
55 | if (!ShouldStartDisabled) {
56 | KeyboardMapperInstance.Start();
57 | ControllerManagerInstance.Start();
58 | }
59 |
60 | // See https://github.com/gmamaladze/globalmousekeyhook/issues/3#issuecomment-230909645
61 | System.Windows.Forms.ApplicationContext msgLoop = new System.Windows.Forms.ApplicationContext();
62 |
63 | ConsoleCtrlHandlerRef += new ConsoleCtrlHandler(HandleConsoleExit);
64 | SetConsoleCtrlHandler(ConsoleCtrlHandlerRef, true);
65 |
66 | System.Windows.Forms.Application.Run(msgLoop);
67 | }
68 |
69 | private static void Stop() {
70 | RestServerInstance.Stop();
71 | KeyboardMapperInstance.Stop();
72 | ControllerManagerInstance.Stop();
73 | }
74 |
75 | private static bool HandleConsoleExit(int Signal) {
76 | Stop();
77 | return false;
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Install Driver/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
53 |
60 |
61 |
62 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # X-Arcade XInput
2 |
3 | [![][ci badge]][ci link]
4 |
5 | Turns an X-Arcade joystick into 360 Controllers.
6 |
7 | Technically, turns any keyboard input into 360 Controllers, but the default is to support X-Arcade joysticks in Mode 1.
8 |
9 | ## Installation
10 |
11 | 1. [Download the latest release](https://github.com/mikew/xarcade-xinput/releases/latest)
12 | 1. Double-click `Install Driver.exe`.
13 | 1. Run `XArcade XInput.exe`
14 | 1. [Test in the HTML5 Gamepad Tester](https://greggman.github.io/html5-gamepad-test/)
15 |
16 | ## Manual Installation
17 |
18 | 1. [Download the latest release](https://github.com/mikew/xarcade-xinput/releases/latest)
19 | 1. Run `Scp Driver Installer\ScpDriverInstaller.exe` and press `Install`
20 | 1. Run in Admin Command Prompt:
21 | ```dos
22 | netsh advfirewall firewall add rule name="XArcade XInput" dir=in action=allow protocol=TCP localport=32123
23 | netsh http add urlacl url=http://+:32123/ user=Everyone
24 | ```
25 | 1. Run `XArcade XInput.exe`
26 | 1. [Test in the HTML5 Gamepad Tester](https://greggman.github.io/html5-gamepad-test/)
27 |
28 | ## Usage
29 |
30 | The default mapping will work with [X-Arcade joysticks using recent PCBs](https://shop.xgaming.com/pages/new-x-arcade-pcb) in Mode 1.
31 |
32 | Open [http://localhost:32123/](http://localhost:32123/) in a browser to to access the Web UI. From here you can turn it on or off, and change the mappings. The Web UI can also be accessed on any phone, tablet, or other computer.
33 |
34 | ## Mappings
35 |
36 | You can change any keyboard key to output any single 360 Controller Button or Axis:
37 |
38 | ```json
39 | {
40 | "W": [0, "LeftStickY", 0.5, 0],
41 | "S": [0, "LeftStickY", -1],
42 | "E": [0, "X"]
43 | }
44 | ```
45 |
46 | The syntax is JSON, where the key on the left is one of [System.Windows.Forms.Keys](), and the value is an array of `[controllerIndex, controllerButtonOrAxis, ...parameters]`
47 |
48 | If given an axis, like `LeftStickX` or `RightTrigger`, you can supply up to two more parameters: The first being the percentage when the key is down, and the second being the percentage when the key is released.
49 |
50 | So, in the example above, `W` would push the left stick forward to 50%, and `S` would pull it all the way back to 100%.
51 |
52 | Note that no matter what you have mapped, pressing the equivalent of `RB + Start` will press the `Guide / Home / Logo` button.
53 |
54 | ## Command Line Arguments
55 |
56 | You can pass arguments when running XArcade XInput:
57 |
58 | | Argument | Purpose |
59 | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
60 | | `--debug` | Prints some debug information. |
61 | | `--default` | Force using the default mapping. This can help if you get stuck when writing your own mappings. This takes precedence over other arguments. |
62 | | `--skip-ui` | Will prevent your browser from opening. |
63 | | `--start-disabled` | Won't listen for keyboard events when starting. |
64 | | `--mapping` | Name of mapping, as seen in app, to load instead of the previous. Helps when different games require different configurations. |
65 |
66 | ## Why
67 |
68 | [X-Arcade used to suggest](https://support.xgaming.com/support/solutions/articles/12000003227-use-x-arcade-as-a-windows-joystick-gamepad-controller-xinput-) a number of different methods of getting your joystick to appear as a DirectInput or XInput controller in Windows. What you end up with is:
69 |
70 | 1. Keyboard -> DirectInput (Headsoft VJoy)
71 | 2. DirectInput -> XInput (XOutput)
72 |
73 | Steam's Generic Controller Support should let you skip the second part, but it's not.
74 |
75 | Or you can search the internet, and find any combinations like:
76 |
77 | - Keyboard -> UCR -> A Different vJoy -> vXboxInterface
78 | - Keyboard -> Headsoft VJoy -> x360ce -> ViGEm
79 |
80 | That's a lot of indirection. There's none of that here. Just Keyboard to XInput.
81 |
82 | [ci link]: https://github.com/mikew/xarcade-xinput/actions
83 | [ci badge]: https://github.com/mikew/xarcade-xinput/actions/workflows/ci.yml/badge.svg?branch=master
84 |
--------------------------------------------------------------------------------
/webapp/src/MappingList.js:
--------------------------------------------------------------------------------
1 | import AceEditor from 'react-ace'
2 | import 'brace/mode/json'
3 | import 'brace/theme/tomorrow_night_eighties'
4 |
5 | import {
6 | React,
7 | PureComponent,
8 | Button,
9 | Icon,
10 | List,
11 | ListSubheader,
12 | Dialog,
13 | DialogActions,
14 | DialogTitle,
15 | DialogContent,
16 | DialogContentText,
17 | UnmanagedTextField,
18 | Text,
19 | connect,
20 | } from './common'
21 |
22 | import MappingEntry from './MappingEntry'
23 |
24 | import * as actions from './mappings/actions'
25 |
26 | class MappingList extends PureComponent {
27 | componentDidMount () {
28 | this.props.refresh()
29 | }
30 |
31 | render () {
32 | const children = this.props.mappingNames.map(x => {
33 | const isActive = x === this.props.currentMapping
34 | return
35 | })
36 |
37 | return
38 |
39 | Mappings
40 | {children}
41 |
42 | }
43 | }
44 |
45 | class MappingEditor extends PureComponent {
46 | aceEditor = null
47 | renameInput = null
48 |
49 | state = {
50 | isOpen: false,
51 | isRenameDialogOpen: false,
52 | }
53 |
54 | componentWillReceiveProps (nextProps, nextState) {
55 | if (nextProps.editingStartedAt !== this.props.editingStartedAt) {
56 | this.setState({ isOpen: true })
57 | }
58 | }
59 |
60 | render () {
61 | return
97 | }
98 |
99 | renderRenameDialog () {
100 | const defaultName = `${this.props.currentEditing} - ${Math.random().toString(16).substr(2)}`
101 |
102 | return
119 | }
120 |
121 | handleClose = () => {
122 | this.setState({ isOpen: false })
123 | }
124 |
125 | handleRenameDialogClose = () => {
126 | this.setState({ isRenameDialogOpen: false })
127 | }
128 |
129 | openRenameDialog = () => {
130 | this.setState({ isRenameDialogOpen: true })
131 | }
132 |
133 | save = () => {
134 | this._save()
135 | }
136 |
137 | saveWithRename = () => {
138 | this._save(this.renameInput.TextField.props.value)
139 | }
140 |
141 | _save = (name = this.props.currentEditing, mapping = this.aceEditor.editor.getValue()) => {
142 | this.props.saveMapping(name, mapping)
143 | .then(this.props.refresh)
144 | .then(this.handleRenameDialogClose)
145 | .then(this.handleClose)
146 | }
147 | }
148 |
149 | MappingEditor = connect(state => ({
150 | editingStartedAt: state.mappings.editingStartedAt,
151 | currentEditing: state.mappings.currentEditing,
152 | mapping: state.mappings.currentEditing && state.mappings.all[state.mappings.currentEditing],
153 | }), actions)(MappingEditor)
154 |
155 | export default connect(state => ({
156 | mappingNames: state.mappings.mappingNames,
157 | currentMapping: state.mappings.currentMapping,
158 | }), actions)(MappingList)
159 |
--------------------------------------------------------------------------------
/XArcade XInput/XArcade XInput.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {2A7F171B-F9E0-42DE-B116-9F9FF812CAAA}
8 | Exe
9 | Properties
10 | XArcade_XInput
11 | XArcade XInput
12 | v4.5.2
13 | 512
14 | true
15 |
16 |
17 | AnyCPU
18 | true
19 | full
20 | false
21 | bin\Debug\
22 | DEBUG;TRACE
23 | prompt
24 | 4
25 |
26 |
27 | AnyCPU
28 | pdbonly
29 | true
30 | bin\Release\
31 | TRACE
32 | prompt
33 | 4
34 |
35 |
36 |
37 | ..\packages\MouseKeyHook.5.4.0\lib\net40\Gma.System.MouseKeyHook.dll
38 | True
39 |
40 |
41 | ..\packages\Grapevine.4.0.0.252\lib\net40\Grapevine.dll
42 | True
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | {6c663b31-ed95-43ca-9cca-d2349f8e491e}
86 | Install Driver
87 |
88 |
89 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | [Xx]64/
19 | [Xx]86/
20 | [Bb]uild/
21 | bld/
22 | [Bb]in/
23 | [Oo]bj/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | artifacts/
46 |
47 | *_i.c
48 | *_p.c
49 | *_i.h
50 | *.ilk
51 | *.meta
52 | *.obj
53 | *.pch
54 | *.pdb
55 | *.pgc
56 | *.pgd
57 | *.rsp
58 | *.sbr
59 | *.tlb
60 | *.tli
61 | *.tlh
62 | *.tmp
63 | *.tmp_proj
64 | *.log
65 | *.vspscc
66 | *.vssscc
67 | .builds
68 | *.pidb
69 | *.svclog
70 | *.scc
71 |
72 | # Chutzpah Test files
73 | _Chutzpah*
74 |
75 | # Visual C++ cache files
76 | ipch/
77 | *.aps
78 | *.ncb
79 | *.opendb
80 | *.opensdf
81 | *.sdf
82 | *.cachefile
83 | *.VC.db
84 |
85 | # Visual Studio profiler
86 | *.psess
87 | *.vsp
88 | *.vspx
89 | *.sap
90 |
91 | # TFS 2012 Local Workspace
92 | $tf/
93 |
94 | # Guidance Automation Toolkit
95 | *.gpState
96 |
97 | # ReSharper is a .NET coding add-in
98 | _ReSharper*/
99 | *.[Rr]e[Ss]harper
100 | *.DotSettings.user
101 |
102 | # JustCode is a .NET coding add-in
103 | .JustCode
104 |
105 | # TeamCity is a build add-in
106 | _TeamCity*
107 |
108 | # DotCover is a Code Coverage Tool
109 | *.dotCover
110 |
111 | # NCrunch
112 | _NCrunch_*
113 | .*crunch*.local.xml
114 | nCrunchTemp_*
115 |
116 | # MightyMoose
117 | *.mm.*
118 | AutoTest.Net/
119 |
120 | # Web workbench (sass)
121 | .sass-cache/
122 |
123 | # Installshield output folder
124 | [Ee]xpress/
125 |
126 | # DocProject is a documentation generator add-in
127 | DocProject/buildhelp/
128 | DocProject/Help/*.HxT
129 | DocProject/Help/*.HxC
130 | DocProject/Help/*.hhc
131 | DocProject/Help/*.hhk
132 | DocProject/Help/*.hhp
133 | DocProject/Help/Html2
134 | DocProject/Help/html
135 |
136 | # Click-Once directory
137 | publish/
138 |
139 | # Publish Web Output
140 | *.[Pp]ublish.xml
141 | *.azurePubxml
142 |
143 | # TODO: Un-comment the next line if you do not want to checkin
144 | # your web deploy settings because they may include unencrypted
145 | # passwords
146 | #*.pubxml
147 | *.publishproj
148 |
149 | # NuGet Packages
150 | *.nupkg
151 | # The packages folder can be ignored because of Package Restore
152 | **/packages/*
153 | # except build/, which is used as an MSBuild target.
154 | !**/packages/build/
155 | # Uncomment if necessary however generally it will be regenerated when needed
156 | #!**/packages/repositories.config
157 | # NuGet v3's project.json files produces more ignoreable files
158 | *.nuget.props
159 | *.nuget.targets
160 |
161 | # Microsoft Azure Build Output
162 | csx/
163 | *.build.csdef
164 |
165 | # Microsoft Azure Emulator
166 | ecf/
167 | rcf/
168 |
169 | # Windows Store app package directory
170 | AppPackages/
171 | BundleArtifacts/
172 |
173 | # Visual Studio cache files
174 | # files ending in .cache can be ignored
175 | *.[Cc]ache
176 | # but keep track of directories ending in .cache
177 | !*.[Cc]ache/
178 |
179 | # Others
180 | ClientBin/
181 | [Ss]tyle[Cc]op.*
182 | ~$*
183 | *~
184 | *.dbmdl
185 | *.dbproj.schemaview
186 | *.pfx
187 | *.publishsettings
188 | node_modules/
189 | orleans.codegen.cs
190 |
191 | # RIA/Silverlight projects
192 | Generated_Code/
193 |
194 | # Backup & report files from converting an old project file
195 | # to a newer Visual Studio version. Backup files are not needed,
196 | # because we have git ;-)
197 | _UpgradeReport_Files/
198 | Backup*/
199 | UpgradeLog*.XML
200 | UpgradeLog*.htm
201 |
202 | # SQL Server files
203 | *.mdf
204 | *.ldf
205 |
206 | # Business Intelligence projects
207 | *.rdl.data
208 | *.bim.layout
209 | *.bim_*.settings
210 |
211 | # Microsoft Fakes
212 | FakesAssemblies/
213 |
214 | # GhostDoc plugin setting file
215 | *.GhostDoc.xml
216 |
217 | # Node.js Tools for Visual Studio
218 | .ntvs_analysis.dat
219 |
220 | # Visual Studio 6 build log
221 | *.plg
222 |
223 | # Visual Studio 6 workspace options file
224 | *.opt
225 |
226 | # Visual Studio LightSwitch build output
227 | **/*.HTMLClient/GeneratedArtifacts
228 | **/*.DesktopClient/GeneratedArtifacts
229 | **/*.DesktopClient/ModelManifest.xml
230 | **/*.Server/GeneratedArtifacts
231 | **/*.Server/ModelManifest.xml
232 | _Pvt_Extensions
233 |
234 | # LightSwitch generated files
235 | GeneratedArtifacts/
236 | ModelManifest.xml
237 |
238 | # Paket dependency manager
239 | .paket/paket.exe
240 |
241 | # FAKE - F# Make
242 | .fake/
243 |
244 | !Scp\ Driver\ Installer/**/*
245 | Install\ Driver.exe
246 | vendor/settings
247 | Scp Driver Installer/
248 |
--------------------------------------------------------------------------------
/XArcade XInput/ScpDriverInterface/X360Controller.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ScpDriverInterface
4 | {
5 | ///
6 | /// A virtual XBox 360 Controller. After setting the desired values, use the GetReport() method to generate a controller report that can be used with ScpBus's Report() method.
7 | ///
8 | public class X360Controller
9 | {
10 | ///
11 | /// Generates a new X360Controller object with the default initial state (no buttons pressed, all analog inputs 0).
12 | ///
13 | public X360Controller()
14 | {
15 | Buttons = X360Buttons.None;
16 | LeftTrigger = 0;
17 | RightTrigger = 0;
18 | LeftStickX = 0;
19 | LeftStickY = 0;
20 | RightStickX = 0;
21 | RightStickY = 0;
22 | }
23 |
24 | ///
25 | /// Generates a new X360Controller object. Optionally, you can specify the initial state of the controller.
26 | ///
27 | /// The pressed buttons. Use like flags (i.e. (X360Buttons.A | X360Buttons.X) would be mean both A and X are pressed).
28 | /// Left trigger analog input. 0 to 255.
29 | /// Right trigger analog input. 0 to 255.
30 | /// Left stick X-axis. -32,768 to 32,767.
31 | /// Left stick Y-axis. -32,768 to 32,767.
32 | /// Right stick X-axis. -32,768 to 32,767.
33 | /// Right stick Y-axis. -32,768 to 32,767.
34 | public X360Controller(X360Buttons buttons, byte leftTrigger, byte rightTrigger, short leftStickX, short leftStickY, short rightStickX, short rightStickY)
35 | {
36 | Buttons = buttons;
37 | LeftTrigger = leftTrigger;
38 | RightTrigger = rightTrigger;
39 | LeftStickX = leftStickX;
40 | LeftStickY = leftStickY;
41 | RightStickX = rightStickX;
42 | RightStickY = rightStickY;
43 | }
44 |
45 | ///
46 | /// Generates a new X360Controller object with the same values as the specified X360Controller object.
47 | ///
48 | /// An X360Controller object to copy values from.
49 | public X360Controller(X360Controller controller)
50 | {
51 | Buttons = controller.Buttons;
52 | LeftTrigger = controller.LeftTrigger;
53 | RightTrigger = controller.RightTrigger;
54 | LeftStickX = controller.LeftStickX;
55 | LeftStickY = controller.LeftStickY;
56 | RightStickX = controller.RightStickX;
57 | RightStickY = controller.RightStickY;
58 | }
59 |
60 | ///
61 | /// The controller's currently pressed buttons. Use the X360Button values like flags (i.e. (X360Buttons.A | X360Buttons.X) would be mean both A and X are pressed).
62 | ///
63 | public X360Buttons Buttons { get; set; }
64 |
65 | ///
66 | /// The controller's left trigger analog input. Value can range from 0 to 255.
67 | ///
68 | public byte LeftTrigger { get; set; }
69 |
70 | ///
71 | /// The controller's right trigger analog input. Value can range from 0 to 255.
72 | ///
73 | public byte RightTrigger { get; set; }
74 |
75 | ///
76 | /// The controller's left stick X-axis. Value can range from -32,768 to 32,767.
77 | ///
78 | public short LeftStickX { get; set; }
79 |
80 | ///
81 | /// The controller's left stick Y-axis. Value can range from -32,768 to 32,767.
82 | ///
83 | public short LeftStickY { get; set; }
84 |
85 | ///
86 | /// The controller's right stick X-axis. Value can range from -32,768 to 32,767.
87 | ///
88 | public short RightStickX { get; set; }
89 |
90 | ///
91 | /// The controller's right stick Y-axis. Value can range from -32,768 to 32,767.
92 | ///
93 | public short RightStickY { get; set; }
94 |
95 | ///
96 | /// Generates a XBox 360 controller report as specified here: http://free60.org/wiki/GamePad#Input_report. This can be used with ScpBus's Report() method.
97 | ///
98 | /// A 20-byte XBox 360 controller report.
99 | public byte[] GetReport()
100 | {
101 | byte[] bytes = new byte[20];
102 |
103 | bytes[0] = 0x00; // Message type (input report)
104 | bytes[1] = 0x14; // Message size (20 bytes)
105 |
106 | bytes[2] = (byte)((ushort)Buttons & 0xFF); // Buttons low
107 | bytes[3] = (byte)((ushort)Buttons >> 8 & 0xFF); // Buttons high
108 |
109 | bytes[4] = LeftTrigger; // Left trigger
110 | bytes[5] = RightTrigger; // Right trigger
111 |
112 | bytes[6] = (byte)(LeftStickX & 0xFF); // Left stick X-axis low
113 | bytes[7] = (byte)(LeftStickX >> 8 & 0xFF); // Left stick X-axis high
114 | bytes[8] = (byte)(LeftStickY & 0xFF); // Left stick Y-axis low
115 | bytes[9] = (byte)(LeftStickY >> 8 & 0xFF); // Left stick Y-axis high
116 |
117 | bytes[10] = (byte)(RightStickX & 0xFF); // Right stick X-axis low
118 | bytes[11] = (byte)(RightStickX >> 8 & 0xFF); // Right stick X-axis high
119 | bytes[12] = (byte)(RightStickY & 0xFF); // Right stick Y-axis low
120 | bytes[13] = (byte)(RightStickY >> 8 & 0xFF); // Right stick Y-axis high
121 |
122 | // Remaining bytes are unused
123 |
124 | return bytes;
125 | }
126 | }
127 |
128 | ///
129 | /// The buttons to be used with an X360Controller object.
130 | ///
131 | [Flags]
132 | public enum X360Buttons
133 | {
134 | None = 0,
135 |
136 | Up = 1 << 0,
137 | Down = 1 << 1,
138 | Left = 1 << 2,
139 | Right = 1 << 3,
140 |
141 | Start = 1 << 4,
142 | Back = 1 << 5,
143 |
144 | LeftStick = 1 << 6,
145 | RightStick = 1 << 7,
146 |
147 | LeftBumper = 1 << 8,
148 | RightBumper = 1 << 9,
149 |
150 | Logo = 1 << 10,
151 |
152 | A = 1 << 12,
153 | B = 1 << 13,
154 | X = 1 << 14,
155 | Y = 1 << 15,
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/XArcade XInput/ControllerManager.cs:
--------------------------------------------------------------------------------
1 | using ScpDriverInterface;
2 |
3 | namespace XArcade_XInput {
4 | class ControllerManagerEventArgs : System.EventArgs {
5 | public int Index;
6 | public X360Controller Controller;
7 | }
8 |
9 | public enum X360Axis {
10 | LeftTrigger,
11 | RightTrigger,
12 | LeftStickX,
13 | LeftStickY,
14 | RightStickX,
15 | RightStickY,
16 | }
17 |
18 | class ControllerManager {
19 | ScpBus Bus;
20 | public bool IsRunning = false;
21 | public event System.EventHandler OnChange;
22 | bool DidEmulateGuide = false;
23 |
24 | X360Controller[] controllers = new X360Controller[] {
25 | new X360Controller(),
26 | new X360Controller(),
27 | new X360Controller(),
28 | new X360Controller(),
29 | };
30 |
31 | public ControllerManager () {
32 | Bus = new ScpBus();
33 | }
34 |
35 | public void Start () {
36 | if (IsRunning) {
37 | return;
38 | }
39 |
40 | IsRunning = true;
41 | controllers = new X360Controller[] {
42 | new X360Controller(),
43 | new X360Controller(),
44 | new X360Controller(),
45 | new X360Controller(),
46 | };
47 |
48 | UnplugAll();
49 |
50 | foreach (var index in Program.KeyboardMapperInstance.MappedControllerIndexes) {
51 | Bus.PlugIn(index + 1);
52 | }
53 |
54 | System.Console.WriteLine("Started ControllerManager");
55 | }
56 |
57 | public void Stop () {
58 | IsRunning = false;
59 | UnplugAll();
60 | System.Console.WriteLine("Stopped ControllerManager");
61 | }
62 |
63 | public void UnplugAll () {
64 | // Reset all inputs
65 | foreach (var index in Program.KeyboardMapperInstance.MappedControllerIndexes) {
66 | Bus.Report(index + 1, new X360Controller().GetReport());
67 | }
68 |
69 | System.Threading.Thread.Sleep(250);
70 | Bus.UnplugAll();
71 | System.Threading.Thread.Sleep(250);
72 | }
73 |
74 | public void ButtonDown (int Index, X360Buttons Button) {
75 | if (!IsRunning) {
76 | return;
77 | }
78 |
79 | var current = controllers[Index];
80 | var next = new X360Controller(current);
81 |
82 | next.Buttons |= Button;
83 |
84 | // RB + Start = Guide
85 | if (next.Buttons.HasFlag(X360Buttons.RightBumper | X360Buttons.Start)) {
86 | next.Buttons &= ~(X360Buttons.RightBumper | X360Buttons.Start);
87 | next.Buttons |= X360Buttons.Logo;
88 | DidEmulateGuide = true;
89 | }
90 |
91 | if (current.Buttons == next.Buttons) {
92 | return;
93 | }
94 |
95 | controllers[Index] = next;
96 | InvokeChange(Index);
97 | if (Program.IsDebug) {
98 | System.Console.WriteLine($"Controller #{Index + 1} {Button} Button Down");
99 | }
100 | }
101 |
102 | public void ButtonUp (int Index, X360Buttons Button) {
103 | if (!IsRunning) {
104 | return;
105 | }
106 |
107 | var current = controllers[Index];
108 | var next = new X360Controller(current);
109 | next.Buttons &= ~Button;
110 |
111 | if (DidEmulateGuide && Button == X360Buttons.RightBumper || Button == X360Buttons.Start) {
112 | next.Buttons &= ~(X360Buttons.Logo | X360Buttons.RightBumper | X360Buttons.Start);
113 | DidEmulateGuide = false;
114 | }
115 |
116 | if (current.Buttons == next.Buttons) {
117 | return;
118 | }
119 |
120 | controllers[Index] = next;
121 | InvokeChange(Index);
122 | if (Program.IsDebug) {
123 | System.Console.WriteLine($"Controller #{Index + 1} {Button} Button Up");
124 | }
125 | }
126 |
127 | public void SetAxis (int Index, X360Axis Axis, int Value) {
128 | if (!IsRunning) {
129 | return;
130 | }
131 |
132 | int current = int.MinValue;
133 | var controller = controllers[Index];
134 |
135 | switch (Axis) {
136 | case X360Axis.LeftTrigger:
137 | current = controller.LeftTrigger;
138 | break;
139 | case X360Axis.RightTrigger:
140 | current = controller.RightTrigger;
141 | break;
142 | case X360Axis.LeftStickX:
143 | current = controller.LeftStickX;
144 | break;
145 | case X360Axis.LeftStickY:
146 | current = controller.LeftStickY;
147 | break;
148 | case X360Axis.RightStickX:
149 | current = controller.RightStickX;
150 | break;
151 | case X360Axis.RightStickY:
152 | current = controller.RightStickY;
153 | break;
154 | }
155 |
156 | if (current == int.MinValue) {
157 | return;
158 | }
159 |
160 | if (current == Value) {
161 | return;
162 | }
163 |
164 | switch (Axis) {
165 | case X360Axis.LeftTrigger:
166 | controller.LeftTrigger = (byte)Value;
167 | break;
168 | case X360Axis.RightTrigger:
169 | controller.RightTrigger = (byte)Value;
170 | break;
171 | case X360Axis.LeftStickX:
172 | controller.LeftStickX = (short)Value;
173 | break;
174 | case X360Axis.LeftStickY:
175 | controller.LeftStickY = (short)Value;
176 | break;
177 | case X360Axis.RightStickX:
178 | controller.RightStickX = (short)Value;
179 | break;
180 | case X360Axis.RightStickY:
181 | controller.RightStickY = (short)Value;
182 | break;
183 | }
184 |
185 | InvokeChange(Index);
186 | if (Program.IsDebug) {
187 | System.Console.WriteLine($"Controller #{Index + 1} {Axis} {Value}");
188 | }
189 | }
190 |
191 | void InvokeChange (int Index) {
192 | Bus.Report(Index + 1, controllers[Index].GetReport());
193 |
194 | OnChange?.Invoke(null, new ControllerManagerEventArgs {
195 | Index = Index,
196 | Controller = controllers[Index],
197 | });
198 | }
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/XArcade XInput/RestServer.cs:
--------------------------------------------------------------------------------
1 | using Grapevine.Interfaces.Server;
2 | using Grapevine.Server.Attributes;
3 | using Grapevine.Shared;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 | namespace XArcade_XInput {
8 | class RestServer {
9 | public bool IsRunning = false;
10 | Grapevine.Server.RestServer _server;
11 | int Port = 32123;
12 |
13 | public RestServer () {
14 | var appdir = System.AppDomain.CurrentDomain.BaseDirectory;
15 | var publicPath = System.IO.Path.Combine(new string[] { appdir, "webapp" });
16 |
17 | _server = new Grapevine.Server.RestServer();
18 | _server.Host = "+";
19 | _server.Port = Port.ToString();
20 | _server.PublicFolder = new Grapevine.Server.PublicFolder(publicPath);
21 | if (Program.IsDebug) {
22 | _server.LogToConsole();
23 | }
24 | }
25 |
26 | public void Start () {
27 | if (IsRunning) {
28 | return;
29 | }
30 |
31 | _server.Start();
32 |
33 | if (Program.ShouldOpenUI) {
34 | System.Diagnostics.Process.Start($"http://localhost:{Port}");
35 | }
36 |
37 | IsRunning = true;
38 | }
39 |
40 | public void Stop () {
41 | _server.Stop();
42 | }
43 |
44 | static public void SetCORSHeaders (IHttpContext ctx) {
45 | ctx.Response.Headers["Access-Control-Allow-Origin"] = "*";
46 | }
47 |
48 | static public void SendTextResponse (IHttpContext ctx, string response) {
49 | ctx.Response.SendResponse(System.Text.Encoding.Default.GetBytes(response));
50 | }
51 |
52 | static public void CloseResponse (IHttpContext ctx) {
53 | ctx.Response.Advanced.Close();
54 | }
55 |
56 | static public void SendJsonResponse (IHttpContext ctx, Dictionary jsonObject) {
57 | ctx.Response.ContentType = ContentType.JSON;
58 | var ser = new System.Web.Script.Serialization.JavaScriptSerializer();
59 | SendTextResponse(ctx, ser.Serialize(jsonObject));
60 | }
61 |
62 | static public Dictionary ParseJson (string json) {
63 | var ser = new System.Web.Script.Serialization.JavaScriptSerializer();
64 | return ser.DeserializeObject(json) as Dictionary;
65 | }
66 | }
67 |
68 | [RestResource]
69 | class DefaultRestResource {
70 | [RestRoute(HttpMethod = HttpMethod.GET, PathInfo = "/")]
71 | public IHttpContext Index (IHttpContext ctx) {
72 | string prefix = ctx.Server.PublicFolder.Prefix;
73 | if (string.IsNullOrEmpty(prefix)) {
74 | prefix = "/";
75 | }
76 | ctx.Response.Redirect($"{prefix}{ctx.Server.PublicFolder.IndexFileName}");
77 | RestServer.CloseResponse(ctx);
78 | return ctx;
79 | }
80 |
81 | [RestRoute(HttpMethod = HttpMethod.GET, PathInfo = "/api/status")]
82 | public IHttpContext Status (IHttpContext ctx) {
83 | RestServer.SetCORSHeaders(ctx);
84 | RestServer.SendJsonResponse(ctx, new Dictionary {
85 | { "isControllerRunning", Program.ControllerManagerInstance.IsRunning },
86 | { "isKeyboardRunning", Program.KeyboardMapperInstance.IsRunning },
87 | { "hostname", System.Net.Dns.GetHostName() },
88 | });
89 |
90 | return ctx;
91 | }
92 |
93 | [RestRoute(HttpMethod = HttpMethod.POST, PathInfo = "/api/controller/stop")]
94 | public IHttpContext ControllerStop (IHttpContext ctx) {
95 | Program.ControllerManagerInstance.Stop();
96 |
97 | RestServer.SetCORSHeaders(ctx);
98 | RestServer.CloseResponse(ctx);
99 |
100 | return ctx;
101 | }
102 |
103 | [RestRoute(HttpMethod = HttpMethod.POST, PathInfo = "/api/controller/start")]
104 | public IHttpContext ControllerStart (IHttpContext ctx) {
105 | Program.ControllerManagerInstance.Start();
106 |
107 | RestServer.SetCORSHeaders(ctx);
108 | RestServer.CloseResponse(ctx);
109 |
110 | return ctx;
111 | }
112 |
113 | [RestRoute(HttpMethod = HttpMethod.POST, PathInfo = "/api/keyboard/stop")]
114 | public IHttpContext KeyboardStop (IHttpContext ctx) {
115 | Program.KeyboardMapperInstance.Stop();
116 |
117 | RestServer.SetCORSHeaders(ctx);
118 | RestServer.CloseResponse(ctx);
119 |
120 | return ctx;
121 | }
122 |
123 | [RestRoute(HttpMethod = HttpMethod.POST, PathInfo = "/api/keyboard/start")]
124 | public IHttpContext KeyboardStart (IHttpContext ctx) {
125 | Program.KeyboardMapperInstance.Start();
126 |
127 | RestServer.SetCORSHeaders(ctx);
128 | RestServer.CloseResponse(ctx);
129 |
130 | return ctx;
131 | }
132 |
133 | [RestRoute(HttpMethod = HttpMethod.GET, PathInfo = "/api/keyboard/mapping")]
134 | public IHttpContext KeyboardGetMapping (IHttpContext ctx) {
135 | RestServer.SetCORSHeaders(ctx);
136 | RestServer.SendJsonResponse(ctx, new Dictionary {
137 | { "currentMapping", Program.KeyboardMapperInstance.CurrentMappingName },
138 | { "mappings", Program.KeyboardMapperInstance.GetAllMappings() },
139 | });
140 |
141 | return ctx;
142 | }
143 |
144 | [RestRoute(HttpMethod = HttpMethod.POST, PathInfo = "/api/keyboard/mapping")]
145 | public IHttpContext KeyboardSetMapping (IHttpContext ctx) {
146 | RestServer.SetCORSHeaders(ctx);
147 |
148 | try {
149 | var json = RestServer.ParseJson(ctx.Request.Payload);
150 | Program.KeyboardMapperInstance.SaveMapping((string)json["name"], (string)json["mapping"]);
151 | RestServer.CloseResponse(ctx);
152 | } catch (System.Exception e) {
153 | ctx.Response.StatusCode = HttpStatusCode.InternalServerError;
154 | RestServer.SendJsonResponse(ctx, new Dictionary {
155 | { "error", e.Message },
156 | });
157 | }
158 |
159 | return ctx;
160 | }
161 |
162 | [RestRoute(HttpMethod = HttpMethod.OPTIONS, PathInfo = "/api/keyboard/mapping")]
163 | public IHttpContext KeyboardMappingOptions (IHttpContext ctx) {
164 | var validMethods = new HttpMethod[] {
165 | HttpMethod.GET,
166 | HttpMethod.POST,
167 | HttpMethod.DELETE,
168 | HttpMethod.OPTIONS,
169 | };
170 | ctx.Response.Headers["Access-Control-Allow-Methods"] = string.Join(", ", validMethods.Select(x => x.ToString()));
171 | RestServer.SetCORSHeaders(ctx);
172 | RestServer.CloseResponse(ctx);
173 | return ctx;
174 | }
175 |
176 | [RestRoute(HttpMethod = HttpMethod.DELETE, PathInfo = "/api/keyboard/mapping")]
177 | public IHttpContext KeyboardDeleteMapping (IHttpContext ctx) {
178 | try {
179 | Program.KeyboardMapperInstance.DeleteMapping(ctx.Request.Payload);
180 | RestServer.SetCORSHeaders(ctx);
181 | RestServer.CloseResponse(ctx);
182 | } catch (System.Exception e) {
183 | ctx.Response.StatusCode = HttpStatusCode.InternalServerError;
184 | RestServer.SendJsonResponse(ctx, new Dictionary {
185 | { "error", e.Message },
186 | });
187 | }
188 |
189 | return ctx;
190 | }
191 |
192 | [RestRoute(HttpMethod = HttpMethod.POST, PathInfo = "/api/keyboard/mapping/current")]
193 | public IHttpContext KeyboardSetCurrentName (IHttpContext ctx) {
194 | try {
195 | Program.KeyboardMapperInstance.SetCurrentMappingName(ctx.Request.Payload);
196 | RestServer.SetCORSHeaders(ctx);
197 | RestServer.CloseResponse(ctx);
198 | } catch (System.Exception e) {
199 | ctx.Response.StatusCode = HttpStatusCode.InternalServerError;
200 | RestServer.SendJsonResponse(ctx, new Dictionary {
201 | { "error", e.Message },
202 | });
203 | }
204 |
205 | return ctx;
206 | }
207 |
208 | [RestRoute(HttpMethod = HttpMethod.POST, PathInfo = "/api/keyboard/mapping/rename")]
209 | public IHttpContext KeyboardRenameMapping (IHttpContext ctx) {
210 | try {
211 | var json = RestServer.ParseJson(ctx.Request.Payload);
212 | Program.KeyboardMapperInstance.RenameMapping((string)json["name"], (string)json["newName"]);
213 | RestServer.SetCORSHeaders(ctx);
214 | RestServer.CloseResponse(ctx);
215 | } catch (System.Exception e) {
216 | ctx.Response.StatusCode = HttpStatusCode.InternalServerError;
217 | RestServer.SendJsonResponse(ctx, new Dictionary {
218 | { "error", e.Message },
219 | });
220 | }
221 |
222 | return ctx;
223 | }
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/XArcade XInput/KeyboardMapper.cs:
--------------------------------------------------------------------------------
1 | using ScpDriverInterface;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 |
6 | namespace XArcade_XInput {
7 | class KeyboardMapper {
8 | public bool IsRunning = false;
9 | public List MappedControllerIndexes = new List();
10 | Dictionary KeyboardMappings = new Dictionary();
11 | Gma.System.MouseKeyHook.IKeyboardMouseEvents KeyboardHook;
12 | public event System.EventHandler OnParse;
13 |
14 | public string CurrentMappingName;
15 | string DefaultMappingName = "X-Arcade 2 Player Analog";
16 |
17 | public KeyboardMapper () {
18 | KeyboardHook = Gma.System.MouseKeyHook.Hook.GlobalEvents();
19 |
20 | LoadPreviousMapping();
21 | }
22 |
23 | public void Start () {
24 | if (IsRunning) {
25 | return;
26 | }
27 |
28 | IsRunning = true;
29 | KeyboardHook.KeyDown += KeyboardHook_KeyDown;
30 | KeyboardHook.KeyUp += KeyboardHook_KeyUp;
31 | System.Console.WriteLine("Started KeyboardMapper");
32 | }
33 |
34 | public void Stop () {
35 | IsRunning = false;
36 | KeyboardHook.KeyDown -= KeyboardHook_KeyDown;
37 | KeyboardHook.KeyUp -= KeyboardHook_KeyUp;
38 | System.Console.WriteLine("Stopped KeyboardMapper");
39 | }
40 |
41 | void LoadPreviousMapping () {
42 | var name = "";
43 |
44 | if (File.Exists(GetMappingPath("CurrentMappingName"))) {
45 | name = File.ReadAllText(GetMappingPath("CurrentMappingName")).Trim();
46 | }
47 |
48 | if (!string.IsNullOrEmpty(Program.InitialMappingName)) {
49 | name = Program.InitialMappingName;
50 | }
51 |
52 | if (Program.ForceDefaultMapping) {
53 | name = DefaultMappingName;
54 | }
55 |
56 | if (!DoesMappingExist(name)) {
57 | name = DefaultMappingName;
58 | }
59 |
60 | SetCurrentMappingName(name);
61 | }
62 |
63 | public void SaveMapping (string name, string contents) {
64 | if (name == DefaultMappingName) {
65 | return;
66 | }
67 |
68 | name = SanitizeName(name);
69 | var path = GetMappingPath($"{name}.json");
70 |
71 | System.Console.WriteLine($"Saving mapping to {path}");
72 | File.WriteAllText(path, contents);
73 |
74 | if (name == CurrentMappingName) {
75 | ParseMapping(contents);
76 | }
77 | }
78 |
79 | string GetMappingPath (string part) {
80 | return GetMappingPath(new string[] { part });
81 | }
82 |
83 | string GetMappingPath (string[] parts) {
84 | var allParts = new List {
85 | System.AppDomain.CurrentDomain.BaseDirectory,
86 | "mappings",
87 | };
88 |
89 | allParts.AddRange(parts);
90 |
91 | return Path.Combine(allParts.ToArray());
92 | }
93 |
94 | public void DeleteMapping (string name) {
95 | if (name == DefaultMappingName) {
96 | return;
97 | }
98 |
99 | name = SanitizeName(name);
100 | File.Delete(GetMappingPath($"{name}.json"));
101 | if (name == CurrentMappingName) {
102 | SetCurrentMappingName(DefaultMappingName);
103 | }
104 | }
105 |
106 | public void RenameMapping (string name, string newName) {
107 | if (name == DefaultMappingName) {
108 | return;
109 | }
110 |
111 | name = SanitizeName(name);
112 | newName = SanitizeName(newName);
113 | File.Move(GetMappingPath($"{name}.json"), GetMappingPath($"{newName}.json"));
114 |
115 | if (name == CurrentMappingName) {
116 | CurrentMappingName = newName;
117 | }
118 | }
119 |
120 | public void SetCurrentMappingName (string name) {
121 | if (!DoesMappingExist(name)) {
122 | return;
123 | }
124 |
125 | name = SanitizeName(name);
126 | var path = GetMappingPath($"{name}.json");
127 |
128 | System.Console.WriteLine($"Loading mapping from {path}");
129 | ParseMapping(File.ReadAllText(path));
130 | CurrentMappingName = name;
131 | File.WriteAllText(GetMappingPath("CurrentMappingName"), name);
132 | }
133 |
134 | public bool DoesMappingExist (string name) {
135 | return File.Exists(GetMappingPath($"{SanitizeName(name)}.json"));
136 | }
137 |
138 | public static string SanitizeName (string name) {
139 | if (string.IsNullOrEmpty(name)) {
140 | return System.Guid.NewGuid().ToString();
141 | }
142 |
143 | var pattern = "[^0-9a-zA-Z\\-_]+";
144 | var replacement = " ";
145 | var rgx = new System.Text.RegularExpressions.Regex(pattern);
146 |
147 | return rgx.Replace(name, replacement).Trim();
148 | }
149 |
150 | public Dictionary GetAllMappings () {
151 | var ret = new Dictionary();
152 |
153 | foreach (var f in Directory.GetFiles(GetMappingPath(""), "*.json")) {
154 | var key = Path.GetFileNameWithoutExtension(f);
155 | var value = File.ReadAllText(f).Trim();
156 |
157 | ret.Add(key, value);
158 | }
159 |
160 | return ret;
161 | }
162 |
163 | static List keysDown = new List();
164 |
165 | void KeyboardHook_KeyDown (object sender, System.Windows.Forms.KeyEventArgs e) {
166 | if (KeyboardMappings.ContainsKey(e.KeyCode.ToString())) {
167 | KeyboardMappings[e.KeyCode.ToString()].Run();
168 | e.Handled = true;
169 | }
170 |
171 | if (Program.IsDebug) {
172 | if (!keysDown.Contains(e.KeyCode)) {
173 | keysDown.Add(e.KeyCode);
174 | var message = string.Join(" + ", keysDown.Select(x => x.ToString()));
175 | System.Console.WriteLine($"Keyboard: {message}");
176 | }
177 | }
178 | }
179 |
180 | void KeyboardHook_KeyUp (object sender, System.Windows.Forms.KeyEventArgs e) {
181 | if (KeyboardMappings.ContainsKey(e.KeyCode.ToString())) {
182 | KeyboardMappings[e.KeyCode.ToString()].Run(true);
183 | e.Handled = true;
184 | }
185 |
186 | if (Program.IsDebug) {
187 | if (keysDown.Contains(e.KeyCode)) {
188 | keysDown.Remove(e.KeyCode);
189 | var message = string.Join(" + ", keysDown.Select(x => x.ToString()));
190 | if (string.IsNullOrEmpty(message)) {
191 | message = "All keys released";
192 | }
193 | System.Console.WriteLine($"Keyboard: {message}");
194 | }
195 | }
196 | }
197 |
198 | public void ParseMapping (string mappingJsonContents) {
199 | var ser = new System.Web.Script.Serialization.JavaScriptSerializer();
200 | var mapping = ser.DeserializeObject(mappingJsonContents) as Dictionary;
201 |
202 | MappedControllerIndexes.Clear();
203 | KeyboardMappings.Clear();
204 |
205 | foreach (var pair in mapping) {
206 | System.Console.WriteLine($"Loading mapping for {pair.Key} ...");
207 |
208 | var shorthand = pair.Value as object[];
209 | var controllerIndex = (int)shorthand[0];
210 | var controllerKey = (string)shorthand[1];
211 | var didMap = false;
212 |
213 | switch (controllerKey) {
214 | case "LeftTrigger":
215 | case "RightTrigger":
216 | case "LeftStickX":
217 | case "LeftStickY":
218 | case "RightStickX":
219 | case "RightStickY": {
220 | var maxValue = short.MaxValue;
221 |
222 | if (controllerKey == "LeftTrigger" || controllerKey == "RightTrigger") {
223 | maxValue = byte.MaxValue;
224 | }
225 |
226 | var axis = (X360Axis)System.Enum.Parse(typeof(X360Axis), controllerKey);
227 | var multipliers = ParseAxisMultipliers(shorthand);
228 | var downMultiplier = multipliers[0];
229 | var upMultiplier = multipliers[1];
230 | var downValue = (int)System.Math.Round(maxValue * downMultiplier);
231 | var upValue = (int)System.Math.Round(0 * upMultiplier);
232 |
233 | KeyboardMappings[pair.Key] = new KeyboardDownToAxis { DownValue = downValue, UpValue = upValue, Index = controllerIndex, Axis = axis };
234 | didMap = true;
235 |
236 | break;
237 | }
238 | default: {
239 | var button = (X360Buttons)System.Enum.Parse(typeof(X360Buttons), controllerKey);
240 |
241 | KeyboardMappings[pair.Key] = new KeyboardDownToButton { Index = controllerIndex, Button = button };
242 | didMap = true;
243 |
244 | break;
245 | }
246 | }
247 |
248 | if (didMap) {
249 | if (!MappedControllerIndexes.Contains(controllerIndex)) {
250 | MappedControllerIndexes.Add(controllerIndex);
251 | }
252 | }
253 | }
254 |
255 | OnParse?.Invoke(this, new System.EventArgs());
256 | }
257 |
258 | float[] ParseAxisMultipliers (object[] shorthand) {
259 | float downMultiplier = 1;
260 | float upMultiplier = 0;
261 |
262 | if (shorthand.Length == 3) {
263 | try {
264 | downMultiplier = (int)shorthand[2];
265 | } catch (System.Exception) {
266 | downMultiplier = (float)(decimal)shorthand[2];
267 | }
268 | }
269 | if (shorthand.Length == 4) {
270 | try {
271 | downMultiplier = (int)shorthand[2];
272 | } catch (System.Exception) {
273 | downMultiplier = (float)(decimal)shorthand[2];
274 | }
275 | try {
276 | upMultiplier = (int)shorthand[3];
277 | } catch (System.Exception) {
278 | upMultiplier = (float)(decimal)shorthand[3];
279 | }
280 | }
281 |
282 | return new float[] { downMultiplier, upMultiplier };
283 | }
284 | }
285 |
286 | class IKeyboardActionToGamepad {
287 | public int Index;
288 | public virtual void Run (bool IsRelease = false) { }
289 | }
290 |
291 | class KeyboardDownToButton : IKeyboardActionToGamepad {
292 | public X360Buttons Button;
293 |
294 | public override void Run (bool IsRelease = false) {
295 | if (IsRelease) {
296 | Program.ControllerManagerInstance.ButtonUp(Index, Button);
297 | return;
298 | }
299 |
300 | Program.ControllerManagerInstance.ButtonDown(Index, Button);
301 | }
302 | }
303 |
304 | class KeyboardDownToAxis : IKeyboardActionToGamepad {
305 | public int DownValue;
306 | public int UpValue;
307 | public X360Axis Axis;
308 |
309 | override public void Run (bool IsRelease = false) {
310 | if (IsRelease) {
311 | Program.ControllerManagerInstance.SetAxis(Index, Axis, UpValue);
312 | return;
313 | }
314 |
315 | Program.ControllerManagerInstance.SetAxis(Index, Axis, DownValue);
316 | }
317 | }
318 | }
319 |
--------------------------------------------------------------------------------
/XArcade XInput/ScpDriverInterface/ScpBus.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * ScpDriverInterface - by Mogzol (and of course Scarlet.Crush) - Jan, 2016
3 | *
4 | * This is a simple little DLL which allows you to use Scarlet.Crush's SCP Virtual
5 | * Bus Driver to emulate XBox 360 Controllers.
6 | *
7 | * Most of the code here has been ripped out of his ScpControl source code, mostly
8 | * from the ScpDevice and BusDevice classes, so obviously credit and major props to
9 | * Scarlet.Crush, without him this wouldn't be possible. You can download his
10 | * original source code from here:
11 | * http://forums.pcsx2.net/Thread-XInput-Wrapper-for-DS3-and-Play-com-USB-Dual-DS2-Controller
12 | *
13 | * Note that for this to work the SCP Virtual Bus Driver must be installed. (Duh)
14 | */
15 |
16 | using System;
17 | using System.Globalization;
18 | using System.IO;
19 | using System.Runtime.InteropServices;
20 | using Microsoft.Win32.SafeHandles;
21 | [assembly: CLSCompliant(true)]
22 |
23 | namespace ScpDriverInterface
24 | {
25 | ///
26 | /// Emulates XBox 360 controllers via Scarlet.Crush's SCP Virtual Bus Driver.
27 | ///
28 | public class ScpBus : IDisposable
29 | {
30 | private const string SCP_BUS_CLASS_GUID = "{F679F562-3164-42CE-A4DB-E7DDBE723909}";
31 | private const int ReportSize = 28;
32 |
33 | private readonly SafeFileHandle _deviceHandle;
34 |
35 | ///
36 | /// Creates a new ScpBus object, which will then try to get a handle to the SCP Virtual Bus device. If it is unable to get the handle, an IOException will be thrown.
37 | ///
38 | public ScpBus() : this(0) { }
39 |
40 | ///
41 | /// Creates a new ScpBus object, which will then try to get a handle to the SCP Virtual Bus device. If it is unable to get the handle, an IOException will be thrown.
42 | ///
43 | /// Specifies which SCP Virtual Bus device to use. This is 0-based.
44 | public ScpBus(int instance)
45 | {
46 | string devicePath = "";
47 |
48 | if (Find(new Guid(SCP_BUS_CLASS_GUID), ref devicePath, instance))
49 | {
50 | _deviceHandle = GetHandle(devicePath);
51 | }
52 | else
53 | {
54 | throw new IOException("SCP Virtual Bus Device not found");
55 | }
56 | }
57 |
58 | ///
59 | /// Creates a new ScpBus object, which will then try to get a handle to the specified SCP Virtual Bus device. If it is unable to get the handle, an IOException will be thrown.
60 | ///
61 | /// The path to the SCP Virtual Bus device that you want to use.
62 | public ScpBus(string devicePath)
63 | {
64 | _deviceHandle = GetHandle(devicePath);
65 | }
66 |
67 | ///
68 | /// Closes the handle to the SCP Virtual Bus device. Call this when you are done with your instance of ScpBus.
69 | ///
70 | /// (This method does the same thing as the Dispose() method. Use one or the other.)
71 | ///
72 | public void Close()
73 | {
74 | Dispose();
75 | }
76 |
77 | ///
78 | /// Closes the handle to the SCP Virtual Bus device. Call this when you are done with your instance of ScpBus.
79 | ///
80 | public void Dispose()
81 | {
82 | Dispose(true);
83 | GC.SuppressFinalize(this);
84 | }
85 |
86 | protected virtual void Dispose(bool disposing)
87 | {
88 | if (_deviceHandle != null && !_deviceHandle.IsInvalid)
89 | {
90 | _deviceHandle.Dispose();
91 | }
92 | }
93 |
94 | ///
95 | /// Plugs in an emulated XBox 360 controller.
96 | ///
97 | /// Used to identify the controller. Give each controller you plug in a different number. Number must be non-zero.
98 | /// True if the operation was successful, false otherwise.
99 | public bool PlugIn(int controllerNumber)
100 | {
101 | if (_deviceHandle.IsInvalid)
102 | throw new ObjectDisposedException("SCP Virtual Bus device handle is closed");
103 |
104 | int transfered = 0;
105 | byte[] buffer = new byte[16];
106 |
107 | buffer[0] = 0x10;
108 | buffer[1] = 0x00;
109 | buffer[2] = 0x00;
110 | buffer[3] = 0x00;
111 |
112 | buffer[4] = (byte)((controllerNumber) & 0xFF);
113 | buffer[5] = (byte)((controllerNumber >> 8) & 0xFF);
114 | buffer[6] = (byte)((controllerNumber >> 16) & 0xFF);
115 | buffer[7] = (byte)((controllerNumber >> 24) & 0xFF);
116 |
117 | return NativeMethods.DeviceIoControl(_deviceHandle, 0x2A4000, buffer, buffer.Length, null, 0, ref transfered, IntPtr.Zero);
118 | }
119 |
120 | ///
121 | /// Unplugs an emulated XBox 360 controller.
122 | ///
123 | /// The controller you want to unplug.
124 | /// True if the operation was successful, false otherwise.
125 | public bool Unplug(int controllerNumber)
126 | {
127 | if (_deviceHandle.IsInvalid)
128 | throw new ObjectDisposedException("SCP Virtual Bus device handle is closed");
129 |
130 | int transfered = 0;
131 | byte[] buffer = new Byte[16];
132 |
133 | buffer[0] = 0x10;
134 | buffer[1] = 0x00;
135 | buffer[2] = 0x00;
136 | buffer[3] = 0x00;
137 |
138 | buffer[4] = (byte)((controllerNumber) & 0xFF);
139 | buffer[5] = (byte)((controllerNumber >> 8) & 0xFF);
140 | buffer[6] = (byte)((controllerNumber >> 16) & 0xFF);
141 | buffer[7] = (byte)((controllerNumber >> 24) & 0xFF);
142 |
143 | return NativeMethods.DeviceIoControl(_deviceHandle, 0x2A4004, buffer, buffer.Length, null, 0, ref transfered, IntPtr.Zero);
144 | }
145 |
146 | ///
147 | /// Unplugs all emulated XBox 360 controllers.
148 | ///
149 | /// True if the operation was successful, false otherwise.
150 | public bool UnplugAll()
151 | {
152 | if (_deviceHandle.IsInvalid)
153 | throw new ObjectDisposedException("SCP Virtual Bus device handle is closed");
154 |
155 | int transfered = 0;
156 | byte[] buffer = new byte[16];
157 |
158 | buffer[0] = 0x10;
159 | buffer[1] = 0x00;
160 | buffer[2] = 0x00;
161 | buffer[3] = 0x00;
162 |
163 | return NativeMethods.DeviceIoControl(_deviceHandle, 0x2A4004, buffer, buffer.Length, null, 0, ref transfered, IntPtr.Zero);
164 | }
165 |
166 | ///
167 | /// Sends an input report for the current state of the specified emulated XBox 360 controller. Note: Only use this if you don't care about rumble data, otherwise use the 3-parameter version of Report().
168 | ///
169 | /// The controller to report.
170 | /// The controller report. If using the included X360Controller class, this can be generated with the GetReport() method. Otherwise see http://free60.org/wiki/GamePad#Input_report for details.
171 | /// True if the operation was successful, false otherwise.
172 | public bool Report(int controllerNumber, byte[] controllerReport)
173 | {
174 | return Report(controllerNumber, controllerReport, null);
175 | }
176 |
177 | ///
178 | /// Sends an input report for the current state of the specified emulated XBox 360 controller. If you care about rumble data, make sure you check the output report for rumble data every time you call this.
179 | ///
180 | /// The controller to report.
181 | /// The controller report. If using the included X360Controller class, this can be generated with the GetReport() method. Otherwise see http://free60.org/wiki/GamePad#Input_report for details.
182 | /// The buffer for the output report, which takes the form specified here: http://free60.org/wiki/GamePad#Output_report. Use an 8-byte buffer if you care about rumble data, or null otherwise.
183 | /// True if the operation was successful, false otherwise.
184 | public bool Report(int controllerNumber, byte[] controllerReport, byte[] outputBuffer)
185 | {
186 | if (_deviceHandle.IsInvalid)
187 | throw new ObjectDisposedException("SCP Virtual Bus device handle is closed");
188 |
189 | byte[] head = new byte[8];
190 |
191 | head[0] = 0x1C;
192 | head[4] = (byte)((controllerNumber) & 0xFF);
193 | head[5] = (byte)((controllerNumber >> 8) & 0xFF);
194 | head[6] = (byte)((controllerNumber >> 16) & 0xFF);
195 | head[7] = (byte)((controllerNumber >> 24) & 0xFF);
196 |
197 | byte[] fullReport = new byte[28];
198 |
199 | Buffer.BlockCopy(head, 0, fullReport, 0, head.Length);
200 | Buffer.BlockCopy(controllerReport, 0, fullReport, head.Length, controllerReport.Length);
201 |
202 | int transferred = 0;
203 | return NativeMethods.DeviceIoControl(_deviceHandle, 0x2A400C, fullReport, fullReport.Length, outputBuffer, outputBuffer?.Length ?? 0, ref transferred, IntPtr.Zero) && transferred > 0;
204 | }
205 |
206 | private static bool Find(Guid target, ref string path, int instance = 0)
207 | {
208 | IntPtr detailDataBuffer = IntPtr.Zero;
209 | IntPtr deviceInfoSet = IntPtr.Zero;
210 |
211 | try
212 | {
213 | NativeMethods.SP_DEVICE_INTERFACE_DATA DeviceInterfaceData = new NativeMethods.SP_DEVICE_INTERFACE_DATA(), da = new NativeMethods.SP_DEVICE_INTERFACE_DATA();
214 | int bufferSize = 0, memberIndex = 0;
215 |
216 | deviceInfoSet = NativeMethods.SetupDiGetClassDevs(ref target, IntPtr.Zero, IntPtr.Zero, NativeMethods.DIGCF_PRESENT | NativeMethods.DIGCF_DEVICEINTERFACE);
217 |
218 | DeviceInterfaceData.cbSize = da.cbSize = Marshal.SizeOf(DeviceInterfaceData);
219 |
220 | while (NativeMethods.SetupDiEnumDeviceInterfaces(deviceInfoSet, IntPtr.Zero, ref target, memberIndex, ref DeviceInterfaceData))
221 | {
222 | NativeMethods.SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref DeviceInterfaceData, IntPtr.Zero, 0, ref bufferSize, ref da);
223 | detailDataBuffer = Marshal.AllocHGlobal(bufferSize);
224 |
225 | Marshal.WriteInt32(detailDataBuffer, (IntPtr.Size == 4) ? (4 + Marshal.SystemDefaultCharSize) : 8);
226 |
227 | if (NativeMethods.SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref DeviceInterfaceData, detailDataBuffer, bufferSize, ref bufferSize, ref da))
228 | {
229 | IntPtr pDevicePathName = detailDataBuffer + 4;
230 |
231 | path = Marshal.PtrToStringAuto(pDevicePathName).ToUpper(CultureInfo.InvariantCulture);
232 | Marshal.FreeHGlobal(detailDataBuffer);
233 |
234 | if (memberIndex == instance) return true;
235 | }
236 | else Marshal.FreeHGlobal(detailDataBuffer);
237 |
238 |
239 | memberIndex++;
240 | }
241 | }
242 | finally
243 | {
244 | if (deviceInfoSet != IntPtr.Zero)
245 | {
246 | NativeMethods.SetupDiDestroyDeviceInfoList(deviceInfoSet);
247 | }
248 | }
249 |
250 | return false;
251 | }
252 |
253 | private static SafeFileHandle GetHandle(string devicePath)
254 | {
255 | devicePath = devicePath.ToUpper(CultureInfo.InvariantCulture);
256 |
257 | SafeFileHandle handle = NativeMethods.CreateFile(devicePath, (NativeMethods.GENERIC_WRITE | NativeMethods.GENERIC_READ), NativeMethods.FILE_SHARE_READ | NativeMethods.FILE_SHARE_WRITE, IntPtr.Zero, NativeMethods.OPEN_EXISTING, NativeMethods.FILE_ATTRIBUTE_NORMAL | NativeMethods.FILE_FLAG_OVERLAPPED, UIntPtr.Zero);
258 |
259 | if (handle == null || handle.IsInvalid)
260 | {
261 | throw new IOException("Unable to get SCP Virtual Bus Device handle");
262 | }
263 |
264 | return handle;
265 | }
266 | }
267 |
268 | internal static class NativeMethods
269 | {
270 | [StructLayout(LayoutKind.Sequential)]
271 | internal struct SP_DEVICE_INTERFACE_DATA
272 | {
273 | internal int cbSize;
274 | internal Guid InterfaceClassGuid;
275 | internal int Flags;
276 | internal IntPtr Reserved;
277 | }
278 |
279 | internal const uint FILE_ATTRIBUTE_NORMAL = 0x80;
280 | internal const uint FILE_FLAG_OVERLAPPED = 0x40000000;
281 | internal const uint FILE_SHARE_READ = 1;
282 | internal const uint FILE_SHARE_WRITE = 2;
283 | internal const uint GENERIC_READ = 0x80000000;
284 | internal const uint GENERIC_WRITE = 0x40000000;
285 | internal const uint OPEN_EXISTING = 3;
286 | internal const int DIGCF_PRESENT = 0x0002;
287 | internal const int DIGCF_DEVICEINTERFACE = 0x0010;
288 |
289 | [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
290 | internal static extern SafeFileHandle CreateFile(string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, UIntPtr hTemplateFile);
291 |
292 | [DllImport("kernel32.dll", SetLastError = true)]
293 | [return: MarshalAs(UnmanagedType.Bool)]
294 | internal static extern bool DeviceIoControl(SafeFileHandle hDevice, int dwIoControlCode, byte[] lpInBuffer, int nInBufferSize, byte[] lpOutBuffer, int nOutBufferSize, ref int lpBytesReturned, IntPtr lpOverlapped);
295 |
296 | [DllImport("setupapi.dll", SetLastError = true)]
297 | internal static extern int SetupDiDestroyDeviceInfoList(IntPtr deviceInfoSet);
298 |
299 | [DllImport("setupapi.dll", SetLastError = true)]
300 | [return: MarshalAs(UnmanagedType.Bool)]
301 | internal static extern bool SetupDiEnumDeviceInterfaces(IntPtr hDevInfo, IntPtr devInfo, ref Guid interfaceClassGuid, int memberIndex, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData);
302 |
303 | [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)]
304 | internal static extern IntPtr SetupDiGetClassDevs(ref Guid classGuid, IntPtr enumerator, IntPtr hwndParent, int flags);
305 |
306 | [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)]
307 | [return: MarshalAs(UnmanagedType.Bool)]
308 | internal static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr hDevInfo, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData, IntPtr deviceInterfaceDetailData, int deviceInterfaceDetailDataSize, ref int requiredSize, ref SP_DEVICE_INTERFACE_DATA deviceInfoData);
309 | }
310 | }
--------------------------------------------------------------------------------