mirror of
https://github.com/Lolendor/reVCDOS.git
synced 2026-10-10 20:03:14 +08:00
1138 lines
34 KiB
JavaScript
1138 lines
34 KiB
JavaScript
const defineProperty = Object.defineProperty;
|
|
const defineProperties = Object.defineProperties;
|
|
const getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors;
|
|
const getOwnPropertySymbols = Object.getOwnPropertySymbols;
|
|
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable;
|
|
|
|
const setProperty = (obj, key, value) => key in obj ? defineProperty(obj, key, {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: value
|
|
}) : obj[key] = value;
|
|
|
|
const mergeObjects = (target, source) => {
|
|
for (var key in source || (source = {})) {
|
|
if (hasOwnProperty.call(source, key)) {
|
|
setProperty(target, key, source[key]);
|
|
}
|
|
}
|
|
if (getOwnPropertySymbols) {
|
|
for (var symbol of getOwnPropertySymbols(source)) {
|
|
if (propertyIsEnumerable.call(source, symbol)) {
|
|
setProperty(target, symbol, source[symbol]);
|
|
}
|
|
}
|
|
}
|
|
return target;
|
|
};
|
|
|
|
const definePropertiesFromSource = (target, source) => defineProperties(target, getOwnPropertyDescriptors(source));
|
|
|
|
const definePublicProperty = (obj, key, value) => setProperty(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
|
|
// Polyfill for modulepreload
|
|
(function() {
|
|
const relList = document.createElement("link").relList;
|
|
if (relList && relList.supports && relList.supports("modulepreload")) return;
|
|
|
|
function getFetchOptions(link) {
|
|
const options = {};
|
|
if (link.integrity) options.integrity = link.integrity;
|
|
if (link.referrerPolicy) options.referrerPolicy = link.referrerPolicy;
|
|
if (link.crossOrigin === "use-credentials") {
|
|
options.credentials = "include";
|
|
} else if (link.crossOrigin === "anonymous") {
|
|
options.credentials = "omit";
|
|
} else {
|
|
options.credentials = "same-origin";
|
|
}
|
|
return options;
|
|
}
|
|
|
|
function processModulePreload(link) {
|
|
if (link.ep) return;
|
|
link.ep = true;
|
|
const options = getFetchOptions(link);
|
|
fetch(link.href, options);
|
|
}
|
|
|
|
for (const link of document.querySelectorAll('link[rel="modulepreload"]')) {
|
|
processModulePreload(link);
|
|
}
|
|
|
|
new MutationObserver(mutations => {
|
|
for (const mutation of mutations) {
|
|
if (mutation.type === "childList") {
|
|
for (const node of mutation.addedNodes) {
|
|
if (node.tagName === "LINK" && node.rel === "modulepreload") {
|
|
processModulePreload(node);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}).observe(document, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
})();
|
|
|
|
var Direction = (dir => {
|
|
dir.up = "up";
|
|
dir.down = "down";
|
|
dir.left = "left";
|
|
dir.right = "right";
|
|
return dir;
|
|
})(Direction || {});
|
|
|
|
var ControlType = (type => {
|
|
type.onOff = "onOff";
|
|
type.variable = "variable";
|
|
return type;
|
|
})(ControlType || {});
|
|
|
|
var EmulationMode = (mode => {
|
|
mode.real = "real";
|
|
mode.emulated = "emulated";
|
|
mode.overlay = "overlay";
|
|
return mode;
|
|
})(EmulationMode || {});
|
|
|
|
var GamepadButton = (btn => {
|
|
btn[btn.A = 0] = "A";
|
|
btn[btn.B = 1] = "B";
|
|
btn[btn.X = 2] = "X";
|
|
btn[btn.Y = 3] = "Y";
|
|
btn[btn.LShoulder = 4] = "LShoulder";
|
|
btn[btn.RShoulder = 5] = "RShoulder";
|
|
btn[btn.LTrigger = 6] = "LTrigger";
|
|
btn[btn.RTrigger = 7] = "RTrigger";
|
|
btn[btn.Back = 8] = "Back";
|
|
btn[btn.Start = 9] = "Start";
|
|
btn[btn.LStick = 10] = "LStick";
|
|
btn[btn.RStick = 11] = "RStick";
|
|
btn[btn.DPadUp = 12] = "DPadUp";
|
|
btn[btn.DPadDown = 13] = "DPadDown";
|
|
btn[btn.DPadLeft = 14] = "DPadLeft";
|
|
btn[btn.DPadRight = 15] = "DPadRight";
|
|
btn[btn.Vendor = 16] = "Vendor";
|
|
return btn;
|
|
})(GamepadButton || {});
|
|
|
|
var GamepadAxis = (axis => {
|
|
axis[axis.LStickX = 0] = "LStickX";
|
|
axis[axis.LStickY = 1] = "LStickY";
|
|
axis[axis.RStickX = 2] = "RStickX";
|
|
axis[axis.RStickY = 3] = "RStickY";
|
|
return axis;
|
|
})(GamepadAxis || {});
|
|
|
|
var GamepadElementClass = (cls => {
|
|
cls.TapTarget = "gpad-tap-target";
|
|
cls.ButtonIcon = "gpad-btn-icon";
|
|
cls.ButtonShadow = "gpad-shadow";
|
|
cls.ButtonHighlight = "gpad-highlight";
|
|
cls.ButtonBackground = "gpad-btn-bg";
|
|
cls.DirectionHighlight = "gpad-direction-highlight";
|
|
cls.Thumbstick = "gpad-thumbstick";
|
|
cls.GpadBaseShape = "gpad-base";
|
|
cls.StickBaseShape = "gpad-stick-base";
|
|
return cls;
|
|
})(GamepadElementClass || {});
|
|
|
|
const BUTTON_NAMES = [
|
|
"button_1", "button_2", "button_3", "button_4",
|
|
"shoulder_button_front_left", "shoulder_button_front_right",
|
|
"shoulder_trigger_back_left", "shoulder_trigger_back_right",
|
|
"select_button", "start_button",
|
|
"stick_button_left", "stick_button_right",
|
|
"dpad_up", "dpad_down", "dpad_left", "dpad_right"
|
|
];
|
|
|
|
const TAP_TARGET_NAMES = BUTTON_NAMES.map(name => name + "_tap_target");
|
|
|
|
const DIAGONAL_DPAD_MAPPING = {
|
|
dpad_up_left_tap_target: [GamepadButton.DPadUp, GamepadButton.DPadLeft],
|
|
dpad_up_right_tap_target: [GamepadButton.DPadUp, GamepadButton.DPadRight],
|
|
dpad_down_left_tap_target: [GamepadButton.DPadDown, GamepadButton.DPadLeft],
|
|
dpad_down_right_tap_target: [GamepadButton.DPadDown, GamepadButton.DPadRight]
|
|
};
|
|
|
|
class GamepadStateTracker {
|
|
constructor(config) {
|
|
definePublicProperty(this, "updateDelay");
|
|
definePublicProperty(this, "axisDeadZone");
|
|
definePublicProperty(this, "buttonConfigs");
|
|
definePublicProperty(this, "currentStateOfGamepads");
|
|
definePublicProperty(this, "gamepadConnectListeners");
|
|
definePublicProperty(this, "gamepadDisconnectListeners");
|
|
definePublicProperty(this, "gamepadButtonChangeListeners");
|
|
definePublicProperty(this, "gamepadAxisChangeListeners");
|
|
definePublicProperty(this, "_requestAnimationFrame");
|
|
definePublicProperty(this, "_getGamepads");
|
|
|
|
this.updateDelay = config.updateDelay || 0;
|
|
this.axisDeadZone = config.axisDeadZone || 0;
|
|
this.buttonConfigs = config.buttonConfigs || [];
|
|
this.currentStateOfGamepads = [];
|
|
this.gamepadConnectListeners = [];
|
|
this.gamepadDisconnectListeners = [];
|
|
this.gamepadButtonChangeListeners = [];
|
|
this.gamepadAxisChangeListeners = [];
|
|
|
|
navigator.gamepadInputEmulation = "gamepad";
|
|
|
|
this._requestAnimationFrame = window.requestAnimationFrame ||
|
|
window.mozRequestAnimationFrame ||
|
|
window.webkitRequestAnimationFrame ||
|
|
window.msRequestAnimationFrame;
|
|
|
|
this._getGamepads = navigator.getGamepads ||
|
|
navigator.webkitGetGamepads ||
|
|
navigator.mozGetGamepads ||
|
|
navigator.msGetGamepads;
|
|
|
|
if (this.gamepadApiSupported() || navigator.getNativeGamepads !== undefined) {
|
|
this.tickLoop();
|
|
}
|
|
}
|
|
|
|
setButtonsConfig(config) {
|
|
this.buttonConfigs = config;
|
|
}
|
|
|
|
setUpdateDelay(delay) {
|
|
this.updateDelay = delay;
|
|
}
|
|
|
|
onGamepadConnect(listener) {
|
|
this.gamepadConnectListeners.push(listener);
|
|
window.addEventListener("gamepadconnected", listener, true);
|
|
return listener;
|
|
}
|
|
|
|
offGamepadConnect(listener) {
|
|
this.gamepadConnectListeners = this.gamepadConnectListeners.filter(l => l !== listener);
|
|
window.removeEventListener("gamepadconnected", listener, true);
|
|
}
|
|
|
|
onGamepadDisconnect(listener) {
|
|
this.gamepadDisconnectListeners.push(listener);
|
|
window.addEventListener("gamepaddisconnected", listener, true);
|
|
return listener;
|
|
}
|
|
|
|
offGamepadDisconnect(listener) {
|
|
this.gamepadDisconnectListeners = this.gamepadDisconnectListeners.filter(l => l !== listener);
|
|
window.removeEventListener("gamepaddisconnected", listener, true);
|
|
}
|
|
|
|
onGamepadAxisChange(listener) {
|
|
this.gamepadAxisChangeListeners.push(listener);
|
|
return listener;
|
|
}
|
|
|
|
offGamepadAxisChange(listener) {
|
|
this.gamepadAxisChangeListeners = this.gamepadAxisChangeListeners.filter(l => l !== listener);
|
|
}
|
|
|
|
onGamepadButtonChange(listener) {
|
|
this.gamepadButtonChangeListeners.push(listener);
|
|
return listener;
|
|
}
|
|
|
|
offGamepadButtonChange(listener) {
|
|
this.gamepadButtonChangeListeners = this.gamepadButtonChangeListeners.filter(l => l !== listener);
|
|
}
|
|
|
|
gamepadApiSupported() {
|
|
const getGamepads = navigator.getNativeGamepads ||
|
|
navigator.getGamepads ||
|
|
navigator.webkitGetGamepads ||
|
|
navigator.mozGetGamepads ||
|
|
navigator.msGetGamepads;
|
|
|
|
if (getGamepads != null && typeof getGamepads === "function") {
|
|
try {
|
|
const gamepads = getGamepads.apply(navigator);
|
|
return gamepads != null && (gamepads[0] !== undefined || gamepads.length !== 0 || window.isSecureContext);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
getGamepads() {
|
|
const getGamepads = navigator.getGamepads ||
|
|
navigator.webkitGetGamepads ||
|
|
navigator.mozGetGamepads ||
|
|
navigator.msGetGamepads;
|
|
|
|
return (getGamepads && typeof getGamepads === "function") ? (getGamepads.apply(navigator) || []) : [];
|
|
}
|
|
|
|
getCurrentGamepadStates(forceCheck = false) {
|
|
if (forceCheck) {
|
|
this.checkForGamepadChanges();
|
|
}
|
|
return this.currentStateOfGamepads;
|
|
}
|
|
|
|
cleanup() {
|
|
this.updateDelay = -1;
|
|
this.gamepadConnectListeners.forEach(l => window.removeEventListener("gamepadconnected", l, true));
|
|
this.gamepadDisconnectListeners.forEach(l => window.removeEventListener("gamepaddisconnected", l, true));
|
|
this.gamepadConnectListeners = [];
|
|
this.gamepadDisconnectListeners = [];
|
|
this.gamepadButtonChangeListeners = [];
|
|
this.gamepadAxisChangeListeners = [];
|
|
}
|
|
|
|
tickLoop() {
|
|
if (this.updateDelay < 0) return;
|
|
|
|
this.checkForGamepadChanges();
|
|
|
|
if (this.updateDelay === 0) {
|
|
requestAnimationFrame(this.tickLoop.bind(this));
|
|
} else {
|
|
setTimeout(() => {
|
|
requestAnimationFrame(this.tickLoop.bind(this));
|
|
}, this.updateDelay);
|
|
}
|
|
}
|
|
|
|
checkForGamepadChanges() {
|
|
let gamepads = this.getGamepads();
|
|
for (let i = 0; i < gamepads.length; i++) {
|
|
let gamepad = gamepads[i];
|
|
if (gamepad) {
|
|
this.checkForAxisChanges(i, gamepad);
|
|
this.checkForButtonChanges(i, gamepad);
|
|
this.currentStateOfGamepads[i] = gamepad;
|
|
}
|
|
}
|
|
}
|
|
|
|
checkForAxisChanges(index, gamepad) {
|
|
let axes = gamepad.axes;
|
|
if (axes.length === 0) return;
|
|
|
|
const prevState = this.currentStateOfGamepads[index];
|
|
let prevAxes = (prevState?.axes) || [];
|
|
let changedAxes = [];
|
|
let hasChanges = false;
|
|
|
|
for (let i = 0; i < axes.length; i++) {
|
|
let currentVal = axes[i] || 0;
|
|
let prevVal = prevAxes[i] || 0;
|
|
|
|
if (currentVal !== prevVal) {
|
|
if (Math.abs(currentVal) < this.axisDeadZone && Math.abs(prevVal) < this.axisDeadZone) {
|
|
continue;
|
|
}
|
|
changedAxes[i] = true;
|
|
hasChanges = true;
|
|
} else {
|
|
changedAxes[i] = false;
|
|
}
|
|
}
|
|
|
|
if (hasChanges) {
|
|
this.gamepadAxisChangeListeners.forEach(listener => listener(index, gamepad, changedAxes));
|
|
}
|
|
}
|
|
|
|
checkForButtonChanges(index, gamepad) {
|
|
let buttons = gamepad.buttons;
|
|
if (buttons.length === 0) return;
|
|
|
|
const prevState = this.currentStateOfGamepads[index];
|
|
const prevButtons = (prevState?.buttons) || buttons;
|
|
const changedButtons = new Array(buttons.length).fill(false);
|
|
let hasChanges = false;
|
|
|
|
for (let i = 0; i < buttons.length; i++) {
|
|
let changed = false;
|
|
const currentBtn = buttons[i] || {
|
|
pressed: false,
|
|
value: 0,
|
|
touched: false
|
|
};
|
|
const prevBtn = prevButtons[i] || {
|
|
pressed: false,
|
|
value: 0,
|
|
touched: false
|
|
};
|
|
const config = this.buttonConfigs[i] || {};
|
|
const changes = {};
|
|
|
|
if (currentBtn.touched && !prevBtn.touched) {
|
|
changes.touchDown = true;
|
|
changed = true;
|
|
} else if (!currentBtn.touched && prevBtn.touched) {
|
|
changes.touchUp = true;
|
|
changed = true;
|
|
}
|
|
|
|
if (currentBtn.pressed && !prevBtn.pressed) {
|
|
changes.pressed = true;
|
|
changed = true;
|
|
} else if (!currentBtn.pressed && prevBtn.pressed) {
|
|
changes.released = true;
|
|
changed = true;
|
|
}
|
|
|
|
if (config.fireWhileHolding && currentBtn.pressed && prevBtn.pressed) {
|
|
changes.heldDown = true;
|
|
changed = true;
|
|
}
|
|
|
|
if (currentBtn.value !== prevBtn.value) {
|
|
changes.valueChanged = true;
|
|
changed = true;
|
|
}
|
|
|
|
if (changed) {
|
|
hasChanges = true;
|
|
changedButtons[i] = changes;
|
|
} else {
|
|
changedButtons[i] = false;
|
|
}
|
|
}
|
|
|
|
if (hasChanges) {
|
|
this.gamepadButtonChangeListeners.forEach(listener => listener(index, gamepad, changedButtons));
|
|
}
|
|
}
|
|
}
|
|
|
|
function normalizeVector(x, y, maxDistance) {
|
|
const distance = Math.sqrt(x * x + y * y);
|
|
if (distance > maxDistance) {
|
|
return {
|
|
x: x / distance,
|
|
y: y / distance
|
|
};
|
|
} else {
|
|
return {
|
|
x: x / maxDistance,
|
|
y: y / maxDistance
|
|
};
|
|
}
|
|
}
|
|
|
|
function setTransformOriginToCenter(element) {
|
|
if (element instanceof SVGGraphicsElement) {
|
|
if (element.getAttribute("transform")) {
|
|
console.warn("VirtualGamepadLib: Setting Transform origin on an element that already has a transform attribute. This may break the transform!", element);
|
|
}
|
|
const bbox = element.getBBox();
|
|
element.style.transformOrigin = `${bbox.x + bbox.width / 2}px ${bbox.y + bbox.height / 2}px`;
|
|
} else if (element instanceof HTMLElement) {
|
|
console.warn("VirtualGamepadLib: Setting Transform origin on an element that is not an SVG element. This may break the transform!", element);
|
|
const rect = element.getBoundingClientRect();
|
|
element.style.transformOrigin = `${rect.width / 2}px ${rect.height / 2}px`;
|
|
}
|
|
}
|
|
|
|
const DEFAULT_BUTTON_COUNT = 18;
|
|
const DEFAULT_AXIS_COUNT = 4;
|
|
|
|
class GamepadEmulator {
|
|
constructor(buttonPressThreshold) {
|
|
definePublicProperty(this, "getNativeGamepads");
|
|
definePublicProperty(this, "buttonPressThreshold", 0.1);
|
|
definePublicProperty(this, "realGpadToPatchedIndexMap", []);
|
|
definePublicProperty(this, "patchedGpadToRealIndexMap", []);
|
|
definePublicProperty(this, "emulatedGamepads", []);
|
|
definePublicProperty(this, "emulatedGamepadsMetadata", []);
|
|
definePublicProperty(this, "undoEventPatch", () => {});
|
|
|
|
definePublicProperty(this, "AddDisplayButtonEventListeners", this.AddButtonTouchEventListeners);
|
|
definePublicProperty(this, "AddDisplayJoystickEventListeners", this.AddJoystickTouchEventListeners);
|
|
definePublicProperty(this, "ClearDisplayButtonEventListeners", this.ClearButtonTouchEventListeners);
|
|
definePublicProperty(this, "ClearDisplayJoystickEventListeners", this.ClearJoystickTouchEventListeners);
|
|
|
|
this.buttonPressThreshold = buttonPressThreshold || this.buttonPressThreshold;
|
|
|
|
if (GamepadEmulator.instanceRunning) {
|
|
throw new Error("Only one GamepadEmulator instance may exist at a time!");
|
|
}
|
|
GamepadEmulator.instanceRunning = true;
|
|
|
|
this.undoEventPatch = this.monkeyPatchGamepadEvents();
|
|
this.monkeyPatchGetGamepads();
|
|
}
|
|
|
|
gamepadApiNativelySupported() {
|
|
return !!this.getNativeGamepads && !!this.getNativeGamepads.apply(navigator);
|
|
}
|
|
|
|
AddEmulatedGamepad(index, overlayMode, buttonCount = DEFAULT_BUTTON_COUNT, axisCount = DEFAULT_AXIS_COUNT) {
|
|
if ((index === -1 || (!index && index !== 0))) {
|
|
index = this.nextEmptyEGpadIndex(overlayMode);
|
|
}
|
|
|
|
if (this.emulatedGamepads[index]) return false;
|
|
|
|
const gamepad = {
|
|
emulation: EmulationMode.emulated,
|
|
connected: true,
|
|
timestamp: performance.now(),
|
|
displayId: "Emulated Gamepad " + index,
|
|
id: "Emulated Gamepad " + index + " (Xinput STANDARD GAMEPAD)",
|
|
mapping: "standard",
|
|
index: index,
|
|
buttons: new Array(buttonCount).fill({
|
|
pressed: false,
|
|
value: 0,
|
|
touched: false
|
|
}, 0, buttonCount),
|
|
axes: new Array(axisCount).fill(0, 0, axisCount),
|
|
hapticActuators: []
|
|
};
|
|
|
|
this.emulatedGamepads[index] = gamepad;
|
|
this.emulatedGamepadsMetadata[index] = {
|
|
overlayMode: overlayMode
|
|
};
|
|
|
|
const event = new Event("gamepadconnected");
|
|
event.gamepad = gamepad;
|
|
window.dispatchEvent(event);
|
|
|
|
return gamepad;
|
|
}
|
|
|
|
RemoveEmulatedGamepad(index) {
|
|
this.ClearButtonTouchEventListeners(index);
|
|
this.ClearJoystickTouchEventListeners(index);
|
|
|
|
const gamepad = this.emulatedGamepads[index];
|
|
if (gamepad) {
|
|
delete this.emulatedGamepads[index];
|
|
delete this.emulatedGamepadsMetadata[index];
|
|
|
|
const disconnectedGamepad = definePropertiesFromSource(mergeObjects({}, gamepad), {
|
|
connected: false,
|
|
timestamp: performance.now()
|
|
});
|
|
|
|
const event = new Event("gamepaddisconnected");
|
|
event.gamepad = disconnectedGamepad;
|
|
window.dispatchEvent(event);
|
|
} else {
|
|
console.warn("GamepadEmulator Error: Cannot remove emulated gamepad. No emulated gamepad exists at index " + index);
|
|
}
|
|
}
|
|
|
|
PressButton(gamepadIndex, buttonIndexOrIndices, value, touched) {
|
|
if (this.emulatedGamepads[gamepadIndex] == null) {
|
|
throw new Error("Error: PressButton() - no emulated gamepad at index " + gamepadIndex + ", pass a valid index, or call AddEmulatedGamepad() first to create an emulated gamepad at that index");
|
|
}
|
|
|
|
const buttons = [...(this.emulatedGamepads[gamepadIndex]?.buttons || [])];
|
|
const isPressed = value > this.buttonPressThreshold;
|
|
|
|
if (Array.isArray(buttonIndexOrIndices)) {
|
|
const isTouched = isPressed || (touched ?? buttons[buttonIndexOrIndices[0]]?.touched) || false;
|
|
for (let i = 0; i < buttonIndexOrIndices.length; i++) {
|
|
const idx = buttonIndexOrIndices[i];
|
|
if (idx < 0 || idx >= this.emulatedGamepads[gamepadIndex].buttons.length) {
|
|
console.error("Error: PressButton() - button index " + idx + " out of range, pass a valid index between 0 and " + (this.emulatedGamepads[gamepadIndex].buttons.length - 1));
|
|
continue;
|
|
}
|
|
buttons[idx] = {
|
|
pressed: isPressed,
|
|
value: value || 0,
|
|
touched: isTouched
|
|
};
|
|
}
|
|
} else {
|
|
const isTouched = isPressed || (touched ?? buttons[buttonIndexOrIndices]?.touched) || false;
|
|
if (buttonIndexOrIndices < 0 || buttonIndexOrIndices >= this.emulatedGamepads[gamepadIndex].buttons.length) {
|
|
console.error("Error: PressButton() - button index " + buttonIndexOrIndices + " out of range, pass a valid index between 0 and " + (this.emulatedGamepads[gamepadIndex].buttons.length - 1));
|
|
return;
|
|
}
|
|
buttons[buttonIndexOrIndices] = {
|
|
pressed: isPressed,
|
|
value: value || 0,
|
|
touched: isTouched
|
|
};
|
|
}
|
|
|
|
defineProperty(this.emulatedGamepads[gamepadIndex], "buttons", {
|
|
value: buttons,
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
MoveAxis(gamepadIndex, axisIndex, value) {
|
|
if (this.emulatedGamepads[gamepadIndex] == null) {
|
|
throw new Error("Error: MoveAxis() - no emulated gamepad at index " + gamepadIndex + ", pass a valid index, or call AddEmulatedGamepad() first to create an emulated gamepad at that index");
|
|
}
|
|
|
|
const axes = [...(this.emulatedGamepads[gamepadIndex]?.axes || [])];
|
|
axes[axisIndex] = value;
|
|
|
|
defineProperty(this.emulatedGamepads[gamepadIndex], "axes", {
|
|
value: axes,
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
AddButtonTouchEventListeners(gamepadIndex, buttonConfigs) {
|
|
if (!this.emulatedGamepads[gamepadIndex]) {
|
|
throw new Error("Error: AddJoystickTouchEventListeners() - no emulated gamepad at index " + gamepadIndex + ", pass a valid index, or call AddEmulatedGamepad() first to create an emulated gamepad at that index");
|
|
}
|
|
|
|
let cleanupFuncs = [];
|
|
|
|
for (let i = 0; i < buttonConfigs.length; i++) {
|
|
const config = buttonConfigs[i];
|
|
if (!config) continue;
|
|
|
|
const buttonIndices = config.buttonIndexes ?? config.buttonIndex;
|
|
const tapTarget = config.tapTarget;
|
|
|
|
if (!tapTarget) {
|
|
console.warn("GamepadEmulator: No tap target in gamepad " + gamepadIndex + " display config for button " + buttonIndices + ", skipping...");
|
|
continue;
|
|
}
|
|
|
|
const onTouchStart = (e) => {
|
|
const target = e.changedTouches[0].target;
|
|
if (target === tapTarget || target.parentElement === tapTarget) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
window.addEventListener("touchstart", onTouchStart, {
|
|
passive: false
|
|
});
|
|
|
|
const onPointerEnter = (e) => {
|
|
const isPressed = e.buttons === 1 ? 1 : 0;
|
|
if (!config.lockTargetWhilePressed || isPressed === 0) {
|
|
this.PressButton(gamepadIndex, buttonIndices, isPressed, true);
|
|
}
|
|
};
|
|
tapTarget.addEventListener("pointerenter", onPointerEnter);
|
|
|
|
const onPointerLeave = (e) => {
|
|
const isPressed = e.buttons === 1 ? 1 : 0;
|
|
if (!config.lockTargetWhilePressed || isPressed === 0) {
|
|
this.PressButton(gamepadIndex, buttonIndices, 0, false);
|
|
}
|
|
};
|
|
tapTarget.addEventListener("pointerleave", onPointerLeave);
|
|
|
|
const onPointerCancel = (e) => {
|
|
this.PressButton(gamepadIndex, buttonIndices, 0, false);
|
|
};
|
|
tapTarget.addEventListener("pointercancel", onPointerCancel);
|
|
|
|
if (config.type === ControlType.onOff) {
|
|
const onPointerDown = (e) => {
|
|
e.preventDefault();
|
|
this.PressButton(gamepadIndex, buttonIndices, 1, true);
|
|
if (config.lockTargetWhilePressed) {
|
|
tapTarget.setPointerCapture(e.pointerId);
|
|
} else {
|
|
tapTarget.releasePointerCapture(e.pointerId);
|
|
}
|
|
};
|
|
tapTarget.addEventListener("pointerdown", onPointerDown);
|
|
|
|
const onPointerUp = () => {
|
|
this.PressButton(gamepadIndex, buttonIndices, 0);
|
|
};
|
|
tapTarget.addEventListener("pointerup", onPointerUp);
|
|
|
|
cleanupFuncs.push(() => {
|
|
window.removeEventListener("touchstart", onTouchStart);
|
|
tapTarget.removeEventListener("pointerenter", onPointerEnter);
|
|
tapTarget.removeEventListener("pointerleave", onPointerLeave);
|
|
tapTarget.removeEventListener("pointerdown", onPointerDown);
|
|
tapTarget.removeEventListener("pointerup", onPointerUp);
|
|
tapTarget.removeEventListener("pointercancel", onPointerCancel);
|
|
});
|
|
} else if (config.type === ControlType.variable) {
|
|
const removeDragListener = this.AddDragControlListener(config, (isDragging, x, y) => {
|
|
let val = isDragging ? this.buttonPressThreshold + 0.00001 : 0;
|
|
val += (config.directions[Direction.left] || config.directions[Direction.right]) ? Math.abs(x) : 0;
|
|
val += (config.directions[Direction.up] || config.directions[Direction.down]) ? Math.abs(y) : 0;
|
|
this.PressButton(gamepadIndex, buttonIndices, Math.min(val, 1));
|
|
});
|
|
|
|
cleanupFuncs.push(() => {
|
|
window.removeEventListener("touchstart", onTouchStart);
|
|
tapTarget.removeEventListener("pointerenter", onPointerEnter);
|
|
tapTarget.removeEventListener("pointerleave", onPointerLeave);
|
|
tapTarget.removeEventListener("pointercancel", onPointerCancel);
|
|
removeDragListener();
|
|
});
|
|
}
|
|
}
|
|
|
|
this.emulatedGamepadsMetadata[gamepadIndex].removeButtonListenersFunc = () => {
|
|
cleanupFuncs.forEach(fn => fn());
|
|
};
|
|
}
|
|
|
|
AddJoystickTouchEventListeners(gamepadIndex, joystickConfigs) {
|
|
if (!this.emulatedGamepads[gamepadIndex]) {
|
|
throw new Error("Error: AddJoystickTouchEventListeners() - no emulated gamepad at index " + gamepadIndex + ", pass a valid index, or call AddEmulatedGamepad() first to create an emulated gamepad at that index");
|
|
}
|
|
|
|
let cleanupFuncs = [];
|
|
|
|
for (let i = 0; i < joystickConfigs.length; i++) {
|
|
const config = joystickConfigs[i];
|
|
if (!config) continue;
|
|
|
|
if (config.tapTarget == null) {
|
|
console.warn("GamepadEmulator: No tap target in gamepad " + gamepadIndex + " display config for joystick " + i + ", skipping...");
|
|
continue;
|
|
}
|
|
|
|
const removeDragListener = this.AddDragControlListener(config, (isDragging, x, y) => {
|
|
if (config.xAxisIndex !== undefined) {
|
|
this.MoveAxis(gamepadIndex, config.xAxisIndex, x);
|
|
}
|
|
if (config.yAxisIndex !== undefined) {
|
|
this.MoveAxis(gamepadIndex, config.yAxisIndex, y);
|
|
}
|
|
});
|
|
cleanupFuncs.push(removeDragListener);
|
|
}
|
|
|
|
this.emulatedGamepadsMetadata[gamepadIndex].removeJoystickListenersFunc = () => {
|
|
cleanupFuncs.forEach(fn => fn());
|
|
};
|
|
}
|
|
|
|
ClearButtonTouchEventListeners(gamepadIndex) {
|
|
const metadata = this.emulatedGamepadsMetadata[gamepadIndex];
|
|
if (metadata && metadata.removeButtonListenersFunc) {
|
|
metadata.removeButtonListenersFunc();
|
|
}
|
|
}
|
|
|
|
ClearJoystickTouchEventListeners(gamepadIndex) {
|
|
const metadata = this.emulatedGamepadsMetadata[gamepadIndex];
|
|
if (metadata && metadata.removeJoystickListenersFunc) {
|
|
metadata.removeJoystickListenersFunc();
|
|
}
|
|
}
|
|
|
|
AddDragControlListener(config, callback) {
|
|
let startPos = {
|
|
startX: 0,
|
|
startY: 0
|
|
};
|
|
let activePointerId = -1;
|
|
|
|
const onPointerMove = (e) => {
|
|
if (activePointerId === e.pointerId) {
|
|
const minX = config.directions[Direction.left] ? -1 : 0;
|
|
const maxX = config.directions[Direction.right] ? 1 : 0;
|
|
const minY = config.directions[Direction.up] ? -1 : 0;
|
|
const maxY = config.directions[Direction.down] ? 1 : 0;
|
|
|
|
const deltaX = e.clientX - startPos.startX;
|
|
const deltaY = e.clientY - startPos.startY;
|
|
|
|
let {
|
|
x,
|
|
y
|
|
} = normalizeVector(deltaX, deltaY, config.dragDistance);
|
|
|
|
x = Math.max(Math.min(x, maxX), minX);
|
|
y = Math.max(Math.min(y, maxY), minY);
|
|
|
|
callback(true, x, y);
|
|
}
|
|
};
|
|
|
|
const onPointerUp = (e) => {
|
|
if (activePointerId === e.pointerId) {
|
|
document.removeEventListener("pointermove", onPointerMove, false);
|
|
document.removeEventListener("pointerup", onPointerUp, false);
|
|
activePointerId = -1;
|
|
callback(false, 0, 0);
|
|
}
|
|
};
|
|
|
|
config.tapTarget.addEventListener("pointerdown", (e) => {
|
|
e.preventDefault();
|
|
startPos.startX = e.clientX;
|
|
startPos.startY = e.clientY;
|
|
activePointerId = e.pointerId;
|
|
|
|
if (config.lockTargetWhilePressed) {
|
|
config.tapTarget.setPointerCapture(e.pointerId);
|
|
} else {
|
|
config.tapTarget.releasePointerCapture(e.pointerId);
|
|
}
|
|
|
|
callback(true, 0, 0);
|
|
document.addEventListener("pointermove", onPointerMove, false);
|
|
document.addEventListener("pointerup", onPointerUp, false);
|
|
});
|
|
|
|
const onTouchStart = (e) => {
|
|
if (e.changedTouches[0].target === config.tapTarget) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
window.addEventListener("touchstart", onTouchStart, {
|
|
passive: false
|
|
});
|
|
|
|
return function cleanup() {
|
|
window.removeEventListener("touchstart", onTouchStart);
|
|
config.tapTarget.removeEventListener("pointerdown", onPointerMove); // Note: This looks like a bug in original code, should probably be the pointerdown listener
|
|
};
|
|
}
|
|
|
|
cloneGamepad(gamepad) {
|
|
if (!gamepad) return gamepad;
|
|
|
|
const axisCount = gamepad.axes ? gamepad.axes.length : 0;
|
|
const buttonCount = gamepad.buttons ? gamepad.buttons.length : 0;
|
|
const clone = {};
|
|
|
|
for (let key in gamepad) {
|
|
if (key === "axes") {
|
|
const axes = new Array(axisCount);
|
|
for (let i = 0; i < axisCount; i++) {
|
|
axes[i] = Number(gamepad.axes[i]);
|
|
}
|
|
defineProperty(clone, "axes", {
|
|
value: axes,
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
} else if (key === "buttons") {
|
|
const buttons = new Array(buttonCount);
|
|
for (let i = 0; i < buttonCount; i++) {
|
|
const btn = gamepad.buttons[i];
|
|
if (btn == null) {
|
|
buttons[i] = btn;
|
|
} else {
|
|
buttons[i] = {
|
|
pressed: btn.pressed,
|
|
value: btn.value,
|
|
touched: btn.touched || false
|
|
};
|
|
}
|
|
}
|
|
defineProperty(clone, "buttons", {
|
|
value: buttons,
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
} else {
|
|
defineProperty(clone, key, {
|
|
get: () => gamepad[key],
|
|
configurable: true,
|
|
enumerable: true
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!clone.emulation) {
|
|
defineProperty(clone, "emulation", {
|
|
value: EmulationMode.real,
|
|
configurable: true,
|
|
enumerable: true
|
|
});
|
|
}
|
|
|
|
return clone;
|
|
}
|
|
|
|
nextEmptyEGpadIndex(overlayMode) {
|
|
let index = 0;
|
|
if (overlayMode) {
|
|
do {
|
|
if (!this.emulatedGamepads[index]) break;
|
|
index++;
|
|
} while (index < this.emulatedGamepads.length);
|
|
} else {
|
|
const maxLen = Math.max(this.emulatedGamepads.length, this.patchedGpadToRealIndexMap.length);
|
|
do {
|
|
if (!this.emulatedGamepads[index] && this.patchedGpadToRealIndexMap[index] == null) break;
|
|
index++;
|
|
} while (index < maxLen);
|
|
}
|
|
return index;
|
|
}
|
|
|
|
nextEmptyRealGpadIndex(startIndex) {
|
|
let index = startIndex;
|
|
const maxLen = Math.max(this.emulatedGamepads.length, this.patchedGpadToRealIndexMap.length);
|
|
do {
|
|
const metadata = this.emulatedGamepadsMetadata[index];
|
|
const isFree = this.realGpadToPatchedIndexMap[index] == null && this.patchedGpadToRealIndexMap[index] == null;
|
|
if ((metadata && metadata.overlayMode) || (!metadata && isFree)) break;
|
|
index++;
|
|
} while (index < maxLen);
|
|
return index;
|
|
}
|
|
|
|
monkeyPatchGamepadEvents() {
|
|
let originalOnConnectDescriptor, originalOnDisconnectDescriptor;
|
|
let onConnectHandler, onDisconnectHandler;
|
|
|
|
if (window.hasOwnProperty("ongamepadconnected")) {
|
|
originalOnConnectDescriptor = Object.getOwnPropertyDescriptor(window, "ongamepadconnected");
|
|
originalOnConnectDescriptor.configurable = true;
|
|
onConnectHandler = window.ongamepadconnected;
|
|
window.ongamepadconnected = null;
|
|
|
|
Object.defineProperty(window, "ongamepadconnected", {
|
|
get: () => function(e) {},
|
|
set: (handler) => {
|
|
onConnectHandler = handler;
|
|
},
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
if (window.hasOwnProperty("ongamepaddisconnected")) {
|
|
originalOnDisconnectDescriptor = Object.getOwnPropertyDescriptor(window, "ongamepaddisconnected");
|
|
originalOnDisconnectDescriptor.configurable = true;
|
|
onDisconnectHandler = window.ongamepaddisconnected;
|
|
window.ongamepaddisconnected = null;
|
|
|
|
Object.defineProperty(window, "ongamepaddisconnected", {
|
|
get: () => function(e) {},
|
|
set: (handler) => {
|
|
onDisconnectHandler = handler;
|
|
},
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
const handleConnect = (event) => {
|
|
const gamepad = event.gamepad;
|
|
if (gamepad && gamepad.emulation === undefined) {
|
|
event.stopImmediatePropagation();
|
|
event.preventDefault();
|
|
|
|
const cloned = this.cloneGamepad(event.gamepad);
|
|
const realIndex = cloned.index;
|
|
const patchedIndex = this.nextEmptyRealGpadIndex(realIndex);
|
|
|
|
this.realGpadToPatchedIndexMap[realIndex] = patchedIndex;
|
|
this.patchedGpadToRealIndexMap[patchedIndex] = realIndex;
|
|
|
|
Object.defineProperty(cloned, "index", {
|
|
get: () => patchedIndex
|
|
});
|
|
Object.defineProperty(cloned, "emulation", {
|
|
get: () => EmulationMode.real
|
|
});
|
|
|
|
const newEvent = new Event(event.type || "gamepadconnected");
|
|
newEvent.gamepad = cloned;
|
|
window.dispatchEvent(newEvent);
|
|
}
|
|
if (onConnectHandler) onConnectHandler.call(window, event);
|
|
};
|
|
window.addEventListener("gamepadconnected", handleConnect);
|
|
|
|
const handleDisconnect = (event) => {
|
|
const gamepad = event.gamepad;
|
|
if (gamepad && gamepad.emulation === undefined) {
|
|
event.stopImmediatePropagation();
|
|
event.preventDefault();
|
|
|
|
const cloned = this.cloneGamepad(event.gamepad);
|
|
const patchedIndex = this.realGpadToPatchedIndexMap[cloned.index] || cloned.index;
|
|
|
|
Object.defineProperty(cloned, "index", {
|
|
get: () => patchedIndex
|
|
});
|
|
Object.defineProperty(cloned, "emulation", {
|
|
get: () => EmulationMode.real
|
|
});
|
|
|
|
delete this.realGpadToPatchedIndexMap[cloned.index];
|
|
delete this.patchedGpadToRealIndexMap[patchedIndex];
|
|
|
|
const newEvent = new Event(event.type || "gamepaddisconnected");
|
|
newEvent.gamepad = cloned;
|
|
window.dispatchEvent(newEvent);
|
|
}
|
|
if (onDisconnectHandler) onDisconnectHandler.call(window, event);
|
|
};
|
|
window.addEventListener("gamepaddisconnected", handleDisconnect);
|
|
|
|
return function undo() {
|
|
window.removeEventListener("gamepadconnected", handleConnect);
|
|
if (window.hasOwnProperty("ongamepadconnected")) {
|
|
Object.defineProperty(window, "ongamepadconnected", originalOnConnectDescriptor);
|
|
window.ongamepadconnected = onConnectHandler;
|
|
}
|
|
window.removeEventListener("gamepaddisconnected", handleDisconnect);
|
|
if (window.hasOwnProperty("ongamepaddisconnected")) {
|
|
Object.defineProperty(window, "ongamepaddisconnected", originalOnDisconnectDescriptor);
|
|
window.ongamepaddisconnected = onDisconnectHandler;
|
|
}
|
|
};
|
|
}
|
|
|
|
monkeyPatchGetGamepads() {
|
|
const self = this;
|
|
const originalGetGamepads = navigator.getGamepads ||
|
|
navigator.webkitGetGamepads ||
|
|
navigator.mozGetGamepads ||
|
|
navigator.msGetGamepads;
|
|
|
|
this.getNativeGamepads = originalGetGamepads;
|
|
navigator.getNativeGamepads = originalGetGamepads || function() {
|
|
return [];
|
|
};
|
|
|
|
Object.defineProperty(navigator, "getGamepads", {
|
|
configurable: true,
|
|
value: function() {
|
|
const emulated = self.emulatedGamepads;
|
|
const real = originalGetGamepads ? (originalGetGamepads.apply(navigator) || []) : [];
|
|
const result = new Array(Math.max(real.length, emulated.length)).fill(null);
|
|
|
|
for (let i = 0; i < real.length; i++) {
|
|
const gamepad = real[i];
|
|
if (!gamepad) continue;
|
|
|
|
let cloned = self.cloneGamepad(gamepad);
|
|
let patchedIndex = self.realGpadToPatchedIndexMap[cloned.index] || cloned.index;
|
|
|
|
Object.defineProperty(cloned, "index", {
|
|
get: () => patchedIndex
|
|
});
|
|
result[patchedIndex] = cloned;
|
|
}
|
|
|
|
for (let i = 0; i < emulated.length; i++) {
|
|
let existing = result[i];
|
|
let emu = emulated[i];
|
|
|
|
if (emu && existing) {
|
|
Object.defineProperty(result[i], "emulation", {
|
|
value: EmulationMode.overlay,
|
|
configurable: true
|
|
});
|
|
|
|
let btnCount = Math.max(existing.buttons?.length || 0, emu.buttons.length);
|
|
let buttons = new Array(btnCount);
|
|
|
|
for (let j = 0; j < btnCount; j++) {
|
|
const emuBtn = emu.buttons[j] || {
|
|
touched: false,
|
|
pressed: false,
|
|
value: 0
|
|
};
|
|
const realBtn = existing.buttons[j] || {
|
|
touched: false,
|
|
pressed: false,
|
|
value: 0
|
|
};
|
|
buttons[j] = {
|
|
touched: emuBtn.touched || realBtn.touched || false,
|
|
pressed: emuBtn.pressed || realBtn.pressed || false,
|
|
value: Math.max(emuBtn.value, realBtn.value) || 0
|
|
};
|
|
}
|
|
|
|
Object.defineProperty(result[i], "buttons", {
|
|
value: buttons,
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
|
|
let axisCount = Math.max(emu.axes.length, existing.axes.length);
|
|
let axes = new Array(axisCount);
|
|
|
|
for (let j = 0; j < axisCount; j++) {
|
|
const emuAxis = emu.axes[j] ?? 0;
|
|
const realAxis = existing.axes[j] ?? 0;
|
|
axes[j] = Math.abs(emuAxis) > Math.abs(realAxis) ? emuAxis : realAxis;
|
|
}
|
|
|
|
Object.defineProperty(result[i], "axes", {
|
|
value: axes,
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
} else if (emu) {
|
|
Object.defineProperty(emu, "emulation", {
|
|
value: EmulationMode.emulated,
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
Object.defineProperty(emu, "timestamp", {
|
|
value: performance.now(),
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
result[i] = self.cloneGamepad(emu);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
});
|
|
}
|
|
|
|
cleanup() {
|
|
for (let i = 0; i < this.emulatedGamepads.length; i++) {
|
|
this.ClearButtonTouchEventListeners(i);
|
|
this.ClearJoystickTouchEventListeners(i);
|
|
}
|
|
this.emulatedGamepads = [];
|
|
this.undoEventPatch();
|
|
|
|
if (this.getNativeGamepads) {
|
|
Object.defineProperty(navigator, "getGamepads", {
|
|
value: this.getNativeGamepads,
|
|
configurable: true
|
|
});
|
|
} else {
|
|
Object.defineProperty(navigator, "getGamepads", {
|
|
value: undefined,
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
GamepadEmulator.instanceRunning = false;
|
|
delete navigator.getNativeGamepads;
|
|
}
|
|
}
|
|
|
|
definePublicProperty(GamepadEmulator, "instanceRunning", false);
|
|
|
|
// export default GamepadEmulator;
|
|
// export {
|
|
// setTransformOriginToCenter,
|
|
// DEFAULT_BUTTON_COUNT,
|
|
// GamepadStateTracker,
|
|
// DEFAULT_AXIS_COUNT,
|
|
// GamepadButton,
|
|
// TAP_TARGET_NAMES,
|
|
// Direction,
|
|
// DIAGONAL_DPAD_MAPPING,
|
|
// ControlType,
|
|
// GamepadElementClass,
|
|
// EmulationMode,
|
|
// GamepadAxis
|
|
// };
|