├── 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 | 34 | {children} 35 | 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 | 34 | 35 | check_circle 36 | 37 | Make Active 38 | 39 | 40 | 41 | 42 | edit 43 | 44 | Edit 45 | 46 | 47 | 48 | 49 | label 50 | 51 | Rename 52 | 53 | 54 | 55 | 56 | delete_forever 57 | 58 | Delete 59 | 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 { 65 | // HACK Reset overflow / padding because material-ui ain't 66 | setTimeout(() => { 67 | document.body.style.overflow = '' 68 | document.body.style.paddingRight = '' 69 | }, 300) 70 | }} 71 | > 72 | 73 | 74 | Edit "{this.props.currentEditing}" 75 | 76 | 77 | 78 | {this.renderRenameDialog()} 79 | 80 | 81 | 82 | this.aceEditor = x} 90 | /> 91 | 92 | 93 | 94 | 95 | 96 | 97 | } 98 | 99 | renderRenameDialog () { 100 | const defaultName = `${this.props.currentEditing} - ${Math.random().toString(16).substr(2)}` 101 | 102 | return 105 | 106 | Enter a new name 107 | 108 | 109 | this.renameInput = x} 112 | /> 113 | 114 | 115 | 116 | 117 | 118 | 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 | } --------------------------------------------------------------------------------