Skip to content

Commit

Permalink
Rework Gamepad and Gamepad Managet to fix:
Browse files Browse the repository at this point in the history
    - handle gamepad pugged/unplugged
    - catch and handle confirmation
    - send correct vinput gamepad redis message
    - Handle axis changes
    - Rework catch and dispatch event to dispatch it only when axes changed
    - Add vendor id and product id to the plugg in command
  • Loading branch information
cmajed committed Apr 12, 2024
1 parent 2033f62 commit 2f6c615
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 101 deletions.
109 changes: 63 additions & 46 deletions src/plugins/Gamepad.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ module.exports = class Gamepad extends OverlayPlugin {

this.instance.addListener(window, 'gm-gamepadButtonPressed', this.onGamepadButtonPressed.bind(this));
this.instance.addListener(window, 'gm-gamepadButtonReleased', this.onGamepadButtonReleased.bind(this));
this.instance.addListener(window, 'gm-gamepadAxis', this.onGamepadAxisChanged.bind(this));

this.instance.registerEventCallback('vinput', this.handleConfirmation.bind(this));

// Display widget
this.renderToolbarButton();
Expand Down Expand Up @@ -70,8 +73,8 @@ module.exports = class Gamepad extends OverlayPlugin {
this.content = document.createElement('div');
this.container.appendChild(this.content);
this.generateContent();
this.instance.addListener(window, 'gm-gamepadConnected', this.generateContent.bind(this));
this.instance.addListener(window, 'gm-gamepadDisconnected', this.generateContent.bind(this));
this.instance.addListener(window, 'gm-gamepadConnected', this.handleGamepadPlugged.bind(this));
this.instance.addListener(window, 'gm-gamepadDisconnected', this.handleGamepadUnplugged.bind(this));

// Setup
this.widget.className = 'gm-overlay gm-gamepad-plugin gm-hidden';
Expand Down Expand Up @@ -110,6 +113,42 @@ module.exports = class Gamepad extends OverlayPlugin {
}
}

handleGamepadPlugged(event) {
log.debug('Gamepad plugged');
const gamepad = event.detail;
log.debug(gamepad);
this.generateContent();
this.sendGamepadPlugEvent(gamepad.hostIndex, gamepad.name.split(' ').join('_'),
gamepad.vendorID, gamepad.productID);
}

handleGamepadUnplugged(event) {
log.debug('Gamepad unplugged');
const gamepad = event.detail;
this.generateContent();
this.sendGamepadUnplugEvent(gamepad.guestIndex);
}

sendGamepadPlugEvent(index, name, vendorID, productID) {
const json = {
channel : 'vinput' , messages : [
'gamepad_plugin ' + index + ' ' + name + ' ' + vendorID + ' ' + productID
]
};

this.instance.sendEvent(json);
}

sendGamepadUnplugEvent(index) {
const json = {
channel : 'vinput' , messages : [
'gamepad_plugout ' + index
]
};

this.instance.sendEvent(json);
}

/**
* Display or hide the widget.
*/
Expand All @@ -127,65 +166,43 @@ module.exports = class Gamepad extends OverlayPlugin {
this.toolbarBtnImage.classList.toggle('gm-active');
}

gamepadButtonToKeyboardButton(button) {
switch (button) {
case 0: // BUTTON_A
return 304;
case 1: // BUTTON_B
return 305;
case 2: // BUTTON_X
return 307;
case 3: // BUTTON_Y
return 308;
case 4: // BUTTON_L1
return 310;
case 5: // BUTTON_R1
return 311;
case 6: // BUTTON_L2
return 312;
case 7: // BUTTON_R2
return 313;
case 8: // BUTTON_SELECT
return 314;
case 9: // BUTTON_START
return 315;
case 10: // BUTTON_THUMBL
return 317;
case 11: // BUTTON_THUMBR
return 318;
case 12: // DPAD_UP
return 103;
case 13: // DPAD_DOWN
return 108;
case 14: // DPAD_LEFT
return 105;
case 15: // DPAD_RIGHT
return 106;
default:
return null;
handleConfirmation(message) {
log.debug('Plugin confirmation');
const values = message.split(' ');
if (values[0] === 'gamepad_plugin_confirmation' && values.length === 3) {
this.instance.gamepadManager.listenForInputs(parseInt(values[1]), parseInt(values[2]));
}
}

onGamepadButtonPressed(event) {
log.debug('button pressed');
log.debug(event.detail);

const json = {
type: 'KEYBOARD_PRESS',
keychar: '',
keycode: this.gamepadButtonToKeyboardButton(event.detail.buttonIndex),
channel : 'vinput' , messages : [
'gamepad_press ' + event.detail.gamepadIndex + ' ' + event.detail.buttonIndex
]
};
this.instance.sendEvent(json);
}

onGamepadButtonReleased(event) {
log.debug('button released');
log.debug(event.detail);

const json = {
type: 'KEYBOARD_RELEASE',
keychar: '',
keycode: this.gamepadButtonToKeyboardButton(event.detail.buttonIndex),
channel : 'vinput' , messages : [
'gamepad_release ' + event.detail.gamepadIndex + ' ' + event.detail.buttonIndex
]
};
this.instance.sendEvent(json);
}

onGamepadAxisChanged(event) {
log.debug('Axis changed');

const json = {
channel : 'vinput' , messages : [
'gamepad_axis ' + event.detail.gamepadIndex + ' ' + event.detail.axisIndex + ' ' + event.detail.value
]
};
this.instance.sendEvent(json);
}
Expand Down
98 changes: 43 additions & 55 deletions src/plugins/GamepadManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ module.exports = class GamepadManager {
*/
addGamepadCallbacks() {
this.instance.addListener(window,'gamepadconnected', this.onGamepadConnected.bind(this));
this.instance.addListener(window,'gamepaddisconnected', this.onGamepadConnected.bind(this));
this.instance.addListener(window,'gamepaddisconnected', this.onGamepadDisconnected.bind(this));
}

/**
Expand All @@ -262,23 +262,17 @@ module.exports = class GamepadManager {
* @param {GamepadEvent} event raw event coming from the browser Gamepad API
*/
onGamepadConnected(event) {
const parsedGamepad = this.parseGamepad(event.gamepad);
const customEvent = new CustomEvent('gm-gamepadConnected', {detail: parsedGamepad});
if (event.gamepad.mapping === '') {
log.error(`Unsupported gamepad mapping for gamepad ${parsedGamepad.name}`);
return;
}
const customEvent = new CustomEvent('gm-gamepadConnected', {detail: this.parseGamepad(event.gamepad)});
window.dispatchEvent(customEvent);
}

/**
* Handler for gamepad disconnection. Also stops listening for inputs for this gamepad and emits a custom event
* @param {GamepadEvent} event raw event coming from the browser Gamepad API
*/
onGamepadDisonnected(event) {
onGamepadDisconnected(event) {
const customEvent = new CustomEvent('gm-gamepadDisconnected', {detail: this.parseGamepad(event.gamepad)});
window.dispatchEvent(customEvent);

this.stopListeningInputs(event.gamepad.index);
}

Expand Down Expand Up @@ -306,28 +300,13 @@ module.exports = class GamepadManager {
}

/**
* Finds the parsed gamepad by the remoteIndex. If not found, returns undefined.
* @param {number} remoteIndex index of this gamepad in the remote VM
* @returns {Object} the parsed gamepad by the remoteIndex if found, otherwise undefined.
* Finds the raw gamepad by the guestIndex. If not found, returns undefined.
* @param {number} guestIndex index of this gamepad in the remote VM
* @returns {Object} the raw gamepad by the guestIndex if found, otherwise undefined.
*/
getByRemoteIndex(remoteIndex) {
getRawGamepadByGuestIndex(guestIndex) {
for (let i = 0; i < this.currentGamepads.length; i++) {
if (this.currentGamepads[i].remoteIndex === remoteIndex) {
return this.getGamepads()[i];
}
}
// eslint-disable-next-line no-undefined
return undefined;
}

/**
* Finds the raw gamepad by the remoteIndex. If not found, returns undefined.
* @param {number} remoteIndex index of this gamepad in the remote VM
* @returns {Object} the raw gamepad by the remoteIndex if found, otherwise undefined.
*/
getRawGamepadByRemoteIndex(remoteIndex) {
for (let i = 0; i < this.currentGamepads.length; i++) {
if (this.currentGamepads[i].remoteIndex === remoteIndex) {
if (this.currentGamepads[i].guestIndex === guestIndex) {
return this.getRawGamepads()[i];
}
}
Expand All @@ -347,22 +326,25 @@ module.exports = class GamepadManager {

/**
* Starts listening for inputs for this gamepad: Adds it to the list with its remote index and starts the polling loop.
* @param {number} localIndex index of this gamepad provided by the browser API
* @param {number} remoteIndex index of this gamepad in the remote VM
* @param {number} hostIndex index of this gamepad provided by the browser API
* @param {number} guestIndex index of this gamepad in the remote VM
*/
listenForInputs(localIndex, remoteIndex) {
this.currentGamepads[localIndex] = {remoteIndex: remoteIndex, buttons: []};
listenForInputs(hostIndex, guestIndex) {
this.currentGamepads[hostIndex] = {
guestIndex,
buttons: [],
axes: []};
if (!this.isRunning) {
this.loop();
}
}

/**
* Stops listening for inputs for this gamepad: removes it from the list
* @param {number} localIndex index of this gamepad provided by the browser API
* @param {number} hostIndex index of this gamepad provided by the browser API
*/
stopListeningInputs(localIndex) {
this.currentGamepads.splice(localIndex, 1);
stopListeningInputs(hostIndex) {
this.currentGamepads.splice(hostIndex, 1);
}

/**
Expand Down Expand Up @@ -408,7 +390,7 @@ module.exports = class GamepadManager {
}
const buttonEvent = new CustomEvent('gm-gamepadButtonPressed', {
detail: {
gamepadIndex: this.currentGamepads[gamepad.index].remoteIndex,
gamepadIndex: this.currentGamepads[gamepad.index].guestIndex,
buttonIndex: i,
value: gamepad.buttons[i].value,
}
Expand All @@ -418,7 +400,7 @@ module.exports = class GamepadManager {
this.currentGamepads[gamepad.index].buttons.splice(pressedButtonIndex, 1);
const buttonEvent = new CustomEvent('gm-gamepadButtonReleased', {
detail: {
gamepadIndex: this.currentGamepads[gamepad.index].remoteIndex,
gamepadIndex: this.currentGamepads[gamepad.index].guestIndex,
buttonIndex: i,
value: gamepad.buttons[i].value,
}
Expand All @@ -428,14 +410,20 @@ module.exports = class GamepadManager {
}

for (let i = 0; i < gamepad.axes.length; i++) {
const axisEvent = new CustomEvent('gm-gamepadAxis', {
detail: {
gamepadIndex: this.currentGamepads[gamepad.index].remoteIndex,
axisIndex: i,
value: gamepad.axes[i],
}
});
window.dispatchEvent(axisEvent);
// If the axes value is different from stored one
if (gamepad.axes[i] !== this.currentGamepads[gamepad.index].axes[i]) {
// Store current axes value
this.currentGamepads[gamepad.index].axes[i] = gamepad.axes[i];
// Dipatch event since axes value changed
const axisEvent = new CustomEvent('gm-gamepadAxis', {
detail: {
gamepadIndex: this.currentGamepads[gamepad.index].guestIndex,
axisIndex: i,
value: gamepad.axes[i],
}
});
window.dispatchEvent(axisEvent);
}
}
}
}
Expand All @@ -451,15 +439,15 @@ module.exports = class GamepadManager {
parseGamepad(rawGamepad) {
const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1;
const gamepad = {
localIndex: rawGamepad.index,
hostIndex: rawGamepad.index,
name: rawGamepad.id,
power: 'unknown',
controllerType: ControllerType.Xbox360,
vendor: 'Default',
vendorID: 0,
productID: 0,
state: rawGamepad.connected ? 'plugged' : 'undefined',
remoteIndex: this.currentGamepads[rawGamepad.index]?.remoteIndex,
guestIndex: this.currentGamepads[rawGamepad.index]?.guestIndex,
};
if (isFirefox) {
const regex = /^([0-9a-f]{1,4})-([0-9a-f]{1,4})-\s*(.*)\s*$/i;
Expand Down Expand Up @@ -488,16 +476,16 @@ module.exports = class GamepadManager {
}

/**
* Plays a vibration effect in the requested gamepad (if found by remoteIndex)
* Plays a vibration effect in the requested gamepad (if found by guestIndex)
* Depending on the platform, this may do nothing or use the haptics actuator instead of the vibration actuator
* In that case, only the strong magnitude is taken into account.
* @param {number} remoteIndex index of this gamepad in the remote VM
* @param {number} guestIndex index of this gamepad in the remote VM
* @param {number} weak weak magnitude intensity, between 0.0 & 1.0
* @param {number} strong strong magnitude intensity, between 0.0 & 1.0
*/
vibration(remoteIndex, weak, strong) {
vibration(guestIndex, weak, strong) {
// raw gamepad are needed here to access actuators
const gamepad = this.getRawGamepadByRemoteIndex(remoteIndex);
const gamepad = this.getRawGamepadByGuestIndex(guestIndex);
if (!gamepad) {
return;
}
Expand All @@ -513,19 +501,19 @@ module.exports = class GamepadManager {
strongMagnitude: newStrongValue
});
} else {
log.error(`could not use vibration actuator for controller ${remoteIndex}`);
log.error(`could not use vibration actuator for controller ${guestIndex}`);
log.debug(gamepad);
}
} else if (gamepad.hapticActuators && gamepad.hapticActuators[0]) { // firefox
const actuator = gamepad.hapticActuators[0];
if (actuator.pulse) {
actuator.pulse(strong, 200);
} else {
log.error(`could not use haptic actuator for controller ${remoteIndex}`);
log.error(`could not use haptic actuator for controller ${guestIndex}`);
log.debug(gamepad);
}
} else { // unrecognised, for example DualSense
log.error(`no vibration actuator for controller ${remoteIndex}`);
log.error(`no vibration actuator for controller ${guestIndex}`);
}
}
};

0 comments on commit 2f6c615

Please sign in to comment.