Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

System 2449/fix gamepad #73

Merged
merged 1 commit into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
pgivel marked this conversation as resolved.
Show resolved Hide resolved
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}`);
}
}
};
Loading