Initial commit

This commit is contained in:
redphx
2024-04-20 07:24:16 +07:00
commit 0febae28da
26 changed files with 8648 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
export const GamepadKey: {[index: string | number]: string | number} = {};
GamepadKey[GamepadKey.A = 0] = 'A';
GamepadKey[GamepadKey.B = 1] = 'B';
GamepadKey[GamepadKey.X = 2] = 'X';
GamepadKey[GamepadKey.Y = 3] = 'Y';
GamepadKey[GamepadKey.LB = 4] = 'LB';
GamepadKey[GamepadKey.RB = 5] = 'RB';
GamepadKey[GamepadKey.LT = 6] = 'LT';
GamepadKey[GamepadKey.RT = 7] = 'RT';
GamepadKey[GamepadKey.SELECT = 8] = 'SELECT';
GamepadKey[GamepadKey.START = 9] = 'START';
GamepadKey[GamepadKey.L3 = 10] = 'L3';
GamepadKey[GamepadKey.R3 = 11] = 'R3';
GamepadKey[GamepadKey.UP = 12] = 'UP';
GamepadKey[GamepadKey.DOWN = 13] = 'DOWN';
GamepadKey[GamepadKey.LEFT = 14] = 'LEFT';
GamepadKey[GamepadKey.RIGHT = 15] = 'RIGHT';
GamepadKey[GamepadKey.HOME = 16] = 'HOME';
GamepadKey[GamepadKey.LS_UP = 100] = 'LS_UP';
GamepadKey[GamepadKey.LS_DOWN = 101] = 'LS_DOWN';
GamepadKey[GamepadKey.LS_LEFT = 102] = 'LS_LEFT';
GamepadKey[GamepadKey.LS_RIGHT = 103] = 'LS_RIGHT';
GamepadKey[GamepadKey.RS_UP = 200] = 'RS_UP';
GamepadKey[GamepadKey.RS_DOWN = 201] = 'RS_DOWN';
GamepadKey[GamepadKey.RS_LEFT = 202] = 'RS_LEFT';
GamepadKey[GamepadKey.RS_RIGHT = 203] = 'RS_RIGHT';
export const GamepadKeyName: {[index: string | number]: string[]} = {
[GamepadKey.A]: ['A', '⇓'],
[GamepadKey.B]: ['B', '⇒'],
[GamepadKey.X]: ['X', '⇐'],
[GamepadKey.Y]: ['Y', '⇑'],
[GamepadKey.LB]: ['LB', '↘'],
[GamepadKey.RB]: ['RB', '↙'],
[GamepadKey.LT]: ['LT', '↖'],
[GamepadKey.RT]: ['RT', '↗'],
[GamepadKey.SELECT]: ['Select', '⇺'],
[GamepadKey.START]: ['Start', '⇻'],
[GamepadKey.HOME]: ['Home', ''],
[GamepadKey.UP]: ['D-Pad Up', '≻'],
[GamepadKey.DOWN]: ['D-Pad Down', '≽'],
[GamepadKey.LEFT]: ['D-Pad Left', '≺'],
[GamepadKey.RIGHT]: ['D-Pad Right', '≼'],
[GamepadKey.L3]: ['L3', '↺'],
[GamepadKey.LS_UP]: ['Left Stick Up', '↾'],
[GamepadKey.LS_DOWN]: ['Left Stick Down', '⇂'],
[GamepadKey.LS_LEFT]: ['Left Stick Left', '↼'],
[GamepadKey.LS_RIGHT]: ['Left Stick Right', '⇀'],
[GamepadKey.R3]: ['R3', '↻'],
[GamepadKey.RS_UP]: ['Right Stick Up', '↿'],
[GamepadKey.RS_DOWN]: ['Right Stick Down', '⇃'],
[GamepadKey.RS_LEFT]: ['Right Stick Left', '↽'],
[GamepadKey.RS_RIGHT]: ['Right Stick Right', '⇁'],
};
export enum GamepadStick {
LEFT = 0,
RIGHT = 1,
};
export enum MouseButtonCode {
LEFT_CLICK = 'Mouse0',
RIGHT_CLICK = 'Mouse2',
MIDDLE_CLICK = 'Mouse1',
};
export const MouseMapTo: {[index: string | number]: string | number} = {};
MouseMapTo[MouseMapTo.OFF = 0] = 'OFF';
MouseMapTo[MouseMapTo.LS = 1] = 'LS';
MouseMapTo[MouseMapTo.RS = 2] = 'RS';
export enum WheelCode {
SCROLL_UP = 'ScrollUp',
SCROLL_DOWN = 'ScrollDown',
SCROLL_LEFT = 'ScrollLeft',
SCROLL_RIGHT = 'ScrollRight',
};
export enum MkbPresetKey {
MOUSE_MAP_TO = 'map_to',
MOUSE_SENSITIVITY_X = 'sensitivity_x',
MOUSE_SENSITIVITY_Y = 'sensitivity_y',
MOUSE_DEADZONE_COUNTERWEIGHT = 'deadzone_counterweight',
MOUSE_STICK_DECAY_STRENGTH = 'stick_decay_strength',
MOUSE_STICK_DECAY_MIN = 'stick_decay_min',
}

View File

@@ -0,0 +1,67 @@
import { MouseButtonCode, WheelCode } from "./definitions";
export class KeyHelper {
static #NON_PRINTABLE_KEYS = {
'Backquote': '`',
// Mouse buttons
[MouseButtonCode.LEFT_CLICK]: 'Left Click',
[MouseButtonCode.RIGHT_CLICK]: 'Right Click',
[MouseButtonCode.MIDDLE_CLICK]: 'Middle Click',
[WheelCode.SCROLL_UP]: 'Scroll Up',
[WheelCode.SCROLL_DOWN]: 'Scroll Down',
[WheelCode.SCROLL_LEFT]: 'Scroll Left',
[WheelCode.SCROLL_RIGHT]: 'Scroll Right',
};
static getKeyFromEvent(e: Event) {
let code;
let name;
if (e instanceof KeyboardEvent) {
code = e.code;
} else if (e instanceof MouseEvent) {
code = 'Mouse' + e.button;
} else if (e instanceof WheelEvent) {
if (e.deltaY < 0) {
code = WheelCode.SCROLL_UP;
} else if (e.deltaY > 0) {
code = WheelCode.SCROLL_DOWN;
} else if (e.deltaX < 0) {
code = WheelCode.SCROLL_LEFT;
} else {
code = WheelCode.SCROLL_RIGHT;
}
}
if (code) {
name = KeyHelper.codeToKeyName(code);
}
return code ? {code, name} : null;
}
static codeToKeyName(code: string) {
return (
// @ts-ignore
KeyHelper.#NON_PRINTABLE_KEYS[code]
||
(code.startsWith('Key') && code.substring(3))
||
(code.startsWith('Digit') && code.substring(5))
||
(code.startsWith('Numpad') && ('Numpad ' + code.substring(6)))
||
(code.startsWith('Arrow') && ('Arrow ' + code.substring(5)))
||
(code.endsWith('Lock') && (code.replace('Lock', ' Lock')))
||
(code.endsWith('Left') && ('Left ' + code.replace('Left', '')))
||
(code.endsWith('Right') && ('Right ' + code.replace('Right', '')))
||
code
);
}
}

View File

@@ -0,0 +1,467 @@
import { MkbPreset } from "./mkb-preset";
import { GamepadKey, MkbPresetKey, GamepadStick, MouseMapTo } from "./definitions";
import { createButton, Icon, ButtonStyle, CE } from "../../utils/html";
import { BxEvent } from "../bx-event";
import { PrefKey, getPref } from "../preferences";
import { Toast } from "../../utils/toast";
import { t } from "../translation";
import { LocalDb } from "../../utils/local-db";
import { KeyHelper } from "./key-helper";
/*
This class uses some code from Yuzu emulator to handle mouse's movements
Source: https://github.com/yuzu-emu/yuzu-mainline/blob/master/src/input_common/drivers/mouse.cpp
*/
export class MkbHandler {
static #instance: MkbHandler;
static get INSTANCE() {
if (!MkbHandler.#instance) {
MkbHandler.#instance = new MkbHandler();
}
return MkbHandler.#instance;
}
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);
static get DEFAULT_PANNING_SENSITIVITY() { return 0.0010; }
static get DEFAULT_STICK_SENSITIVITY() { return 0.0006; }
static get DEFAULT_DEADZONE_COUNTERWEIGHT() { return 0.01; }
static get MAXIMUM_STICK_RANGE() { return 1.1; }
static VIRTUAL_GAMEPAD_ID = 'Xbox 360 Controller';
#VIRTUAL_GAMEPAD = {
id: MkbHandler.VIRTUAL_GAMEPAD_ID,
index: 3,
connected: false,
hapticActuators: null,
mapping: 'standard',
axes: [0, 0, 0, 0],
buttons: new Array(17).fill(null).map(() => ({pressed: false, value: 0})),
timestamp: performance.now(),
vibrationActuator: null,
};
#nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator);
#enabled = false;
#prevWheelCode = null;
#wheelStoppedTimeout?: number | null;
#detectMouseStoppedTimeout?: number | null;
#allowStickDecaying = false;
#$message?: HTMLElement;
#STICK_MAP: {[index: keyof typeof GamepadKey]: (number | number[])[]};
#LEFT_STICK_X: number[] = [];
#LEFT_STICK_Y: number[] = [];
#RIGHT_STICK_X: number[] = [];
#RIGHT_STICK_Y: number[] = [];
constructor() {
this.#STICK_MAP = {
[GamepadKey.LS_LEFT]: [this.#LEFT_STICK_X, 0, -1],
[GamepadKey.LS_RIGHT]: [this.#LEFT_STICK_X, 0, 1],
[GamepadKey.LS_UP]: [this.#LEFT_STICK_Y, 1, -1],
[GamepadKey.LS_DOWN]: [this.#LEFT_STICK_Y, 1, 1],
[GamepadKey.RS_LEFT]: [this.#RIGHT_STICK_X, 2, -1],
[GamepadKey.RS_RIGHT]: [this.#RIGHT_STICK_X, 2, 1],
[GamepadKey.RS_UP]: [this.#RIGHT_STICK_Y, 3, -1],
[GamepadKey.RS_DOWN]: [this.#RIGHT_STICK_Y, 3, 1],
};
}
#patchedGetGamepads = () => {
const gamepads = this.#nativeGetGamepads() || [];
(gamepads as any)[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD;
return gamepads;
}
#getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD;
#updateStick(stick: GamepadStick, x: number, y: number) {
const virtualGamepad = this.#getVirtualGamepad();
virtualGamepad.axes[stick * 2] = x;
virtualGamepad.axes[stick * 2 + 1] = y;
virtualGamepad.timestamp = performance.now();
}
#getStickAxes(stick: GamepadStick) {
const virtualGamepad = this.#getVirtualGamepad();
return {
x: virtualGamepad.axes[stick * 2],
y: virtualGamepad.axes[stick * 2 + 1],
};
}
#vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2);
#disableContextMenu = (e: Event) => e.preventDefault();
#resetGamepad = () => {
const gamepad = this.#getVirtualGamepad();
// Reset axes
gamepad.axes = [0, 0, 0, 0];
// Reset buttons
for (const button of gamepad.buttons) {
button.pressed = false;
button.value = 0;
}
gamepad.timestamp = performance.now();
}
#pressButton = (buttonIndex: number, pressed: boolean) => {
const virtualGamepad = this.#getVirtualGamepad();
if (buttonIndex >= 100) {
let [valueArr, axisIndex, fullValue] = this.#STICK_MAP[buttonIndex];
valueArr = valueArr as number[];
axisIndex = axisIndex as number;
// Remove old index of the array
for (let i = valueArr.length - 1; i >= 0; i--) {
if (valueArr[i] === buttonIndex) {
valueArr.splice(i, 1);
}
}
pressed && valueArr.push(buttonIndex);
let value;
if (valueArr.length) {
// Get value of the last key of the axis
value = this.#STICK_MAP[valueArr[valueArr.length - 1]][2] as number;
} else {
value = 0;
}
virtualGamepad.axes[axisIndex] = value;
} else {
virtualGamepad.buttons[buttonIndex].pressed = pressed;
virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0;
}
virtualGamepad.timestamp = performance.now();
}
#onKeyboardEvent = (e: KeyboardEvent) => {
const isKeyDown = e.type === 'keydown';
// Toggle MKB feature
if (isKeyDown && e.code === 'F8') {
e.preventDefault();
this.toggle();
return;
}
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code];
if (typeof buttonIndex === 'undefined') {
return;
}
// Ignore repeating keys
if (e.repeat) {
return;
}
e.preventDefault();
this.#pressButton(buttonIndex, isKeyDown);
}
#onMouseEvent = e => {
const isMouseDown = e.type === 'mousedown';
const key = KeyHelper.getKeyFromEvent(e);
if (!key) {
return;
}
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code];
if (typeof buttonIndex === 'undefined') {
return;
}
e.preventDefault();
this.#pressButton(buttonIndex, isMouseDown);
}
#onWheelEvent = (e: WheelEvent) => {
const key = KeyHelper.getKeyFromEvent(e);
if (!key) {
return;
}
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code];
if (typeof buttonIndex === 'undefined') {
return;
}
e.preventDefault();
if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) {
this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout);
this.#pressButton(buttonIndex, true);
}
this.#wheelStoppedTimeout = setTimeout(e => {
this.#prevWheelCode = null;
this.#pressButton(buttonIndex, false);
}, 20);
}
#decayStick = () => {
if (!this.#allowStickDecaying) {
return;
}
const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO];
if (mouseMapTo === MouseMapTo.OFF) {
return;
}
const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
const virtualGamepad = this.#getVirtualGamepad();
let { x, y } = this.#getStickAxes(analog);
const length = this.#vectorLength(x, y);
const clampedLength = Math.min(1.0, length);
const decayStrength = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH];
const decay = 1 - clampedLength * clampedLength * decayStrength;
const minDecay = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_STICK_DECAY_MIN];
const clampedDecay = Math.min(1 - minDecay, decay);
x *= clampedDecay;
y *= clampedDecay;
const deadzoneCounterweight = 20 * MkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT;
if (Math.abs(x) <= deadzoneCounterweight && Math.abs(y) <= deadzoneCounterweight) {
x = 0;
y = 0;
}
if (this.#allowStickDecaying) {
this.#updateStick(analog, x, y);
(x !== 0 || y !== 0) && requestAnimationFrame(this.#decayStick);
}
}
#onMouseStopped = (e: MouseEvent) => {
this.#allowStickDecaying = true;
requestAnimationFrame(this.#decayStick);
}
#onMouseMoveEvent = (e: MouseEvent) => {
// TODO: optimize this
const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO];
if (mouseMapTo === MouseMapTo.OFF) {
// Ignore mouse movements
return;
}
this.#allowStickDecaying = false;
this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout);
this.#detectMouseStoppedTimeout = setTimeout(this.#onMouseStopped.bind(this, e), 100);
const deltaX = e.movementX;
const deltaY = e.movementY;
const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT];
let x = deltaX * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X];
let y = deltaY * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y];
let length = this.#vectorLength(x, y);
if (length !== 0 && length < deadzoneCounterweight) {
x *= deadzoneCounterweight / length;
y *= deadzoneCounterweight / length;
} else if (length > MkbHandler.MAXIMUM_STICK_RANGE) {
x *= MkbHandler.MAXIMUM_STICK_RANGE / length;
y *= MkbHandler.MAXIMUM_STICK_RANGE / length;
}
const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
this.#updateStick(analog, x, y);
}
toggle = () => {
this.#enabled = !this.#enabled;
this.#enabled ? document.pointerLockElement && this.start() : this.stop();
Toast.show(t('mouse-and-keyboard'), t(this.#enabled ? 'enabled' : 'disabled'), {instant: true});
if (this.#enabled) {
!document.pointerLockElement && this.#waitForPointerLock(true);
} else {
this.#waitForPointerLock(false);
document.pointerLockElement && document.exitPointerLock();
}
}
#getCurrentPreset = () => {
return new Promise(resolve => {
const presetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
LocalDb.INSTANCE.getPreset(presetId).then(preset => {
resolve(preset ? preset : MkbPreset.DEFAULT_PRESET);
});
});
}
refreshPresetData = () => {
this.#getCurrentPreset().then(preset => {
this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset.data);
this.#resetGamepad();
});
}
#onPointerLockChange = e => {
if (this.#enabled && !document.pointerLockElement) {
this.stop();
this.#waitForPointerLock(true);
}
}
#onPointerLockError = e => {
console.log(e);
this.stop();
}
#onActivatePointerLock = () => {
if (!document.pointerLockElement) {
document.body.requestPointerLock();
}
this.#waitForPointerLock(false);
this.start();
}
#waitForPointerLock = (wait: boolean) => {
this.#$message && this.#$message.classList.toggle('bx-gone', !wait);
}
#onStreamMenuShown = () => {
this.#enabled && this.#waitForPointerLock(false);
}
#onStreamMenuHidden = () => {
this.#enabled && this.#waitForPointerLock(true);
}
init = () => {
this.refreshPresetData();
this.#enabled = true;
window.addEventListener('keydown', this.#onKeyboardEvent);
document.addEventListener('pointerlockchange', this.#onPointerLockChange);
document.addEventListener('pointerlockerror', this.#onPointerLockError);
this.#$message = CE('div', {'class': 'bx-mkb-pointer-lock-msg bx-gone'},
createButton({
icon: Icon.MOUSE_SETTINGS,
style: ButtonStyle.PRIMARY as number,
onClick: e => {
e.preventDefault();
e.stopPropagation();
showStreamSettings('mkb');
},
}),
CE('div', {},
CE('p', {}, t('mkb-click-to-activate')),
CE('p', {}, t<any>('press-key-to-toggle-mkb')({key: 'F8'})),
),
);
this.#$message.addEventListener('click', this.#onActivatePointerLock);
document.documentElement.appendChild(this.#$message);
window.addEventListener(BxEvent.STREAM_MENU_SHOWN, this.#onStreamMenuShown);
window.addEventListener(BxEvent.STREAM_MENU_HIDDEN, this.#onStreamMenuHidden);
this.#waitForPointerLock(true);
}
destroy = () => {
this.#enabled = false;
this.stop();
this.#waitForPointerLock(false);
document.pointerLockElement && document.exitPointerLock();
window.removeEventListener('keydown', this.#onKeyboardEvent);
document.removeEventListener('pointerlockchange', this.#onPointerLockChange);
document.removeEventListener('pointerlockerror', this.#onPointerLockError);
window.removeEventListener(BxEvent.STREAM_MENU_SHOWN, this.#onStreamMenuShown);
window.removeEventListener(BxEvent.STREAM_MENU_HIDDEN, this.#onStreamMenuHidden);
}
start = () => {
window.navigator.getGamepads = this.#patchedGetGamepads;
this.#resetGamepad();
window.addEventListener('keyup', this.#onKeyboardEvent);
window.addEventListener('mousemove', this.#onMouseMoveEvent);
window.addEventListener('mousedown', this.#onMouseEvent);
window.addEventListener('mouseup', this.#onMouseEvent);
window.addEventListener('wheel', this.#onWheelEvent);
window.addEventListener('contextmenu', this.#disableContextMenu);
// Dispatch "gamepadconnected" event
const virtualGamepad = this.#getVirtualGamepad();
virtualGamepad.connected = true;
virtualGamepad.timestamp = performance.now();
BxEvent.dispatch(window, 'gamepadconnected', {
gamepad: virtualGamepad,
});
}
stop = () => {
// Dispatch "gamepaddisconnected" event
const virtualGamepad = this.#getVirtualGamepad();
virtualGamepad.connected = false;
virtualGamepad.timestamp = performance.now();
BxEvent.dispatch(window, 'gamepaddisconnected', {
gamepad: virtualGamepad,
});
window.navigator.getGamepads = this.#nativeGetGamepads;
this.#resetGamepad();
window.removeEventListener('keyup', this.#onKeyboardEvent);
window.removeEventListener('mousemove', this.#onMouseMoveEvent);
window.removeEventListener('mousedown', this.#onMouseEvent);
window.removeEventListener('mouseup', this.#onMouseEvent);
window.removeEventListener('wheel', this.#onWheelEvent);
window.removeEventListener('contextmenu', this.#disableContextMenu);
}
static setupEvents() {
window.addEventListener(BxEvent.STREAM_PLAYING, e => {
// Enable MKB
if (getPref(PrefKey.MKB_ENABLED) && (!ENABLE_NATIVE_MKB_BETA || !window.NATIVE_MKB_TITLES.includes(GAME_PRODUCT_ID))) {
console.log('Emulate MKB');
MkbHandler.INSTANCE.init();
}
});
}
}

View File

@@ -0,0 +1,163 @@
import { t } from "../translation";
import { SettingElementType } from "../settings";
import { GamepadKey, MouseButtonCode, MouseMapTo, MkbPresetKey } from "./definitions";
import { MkbHandler } from "./mkb-handler";
export class MkbPreset {
static MOUSE_SETTINGS = {
[MkbPresetKey.MOUSE_MAP_TO]: {
label: t('map-mouse-to'),
type: SettingElementType.OPTIONS,
default: MouseMapTo[MouseMapTo.RS],
options: {
[MouseMapTo[MouseMapTo.RS]]: t('right-stick'),
[MouseMapTo[MouseMapTo.LS]]: t('left-stick'),
[MouseMapTo[MouseMapTo.OFF]]: t('off'),
},
},
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: {
label: t('horizontal-sensitivity'),
type: SettingElementType.NUMBER_STEPPER,
default: 50,
min: 1,
max: 200,
params: {
suffix: '%',
exactTicks: 20,
},
},
[MkbPresetKey.MOUSE_SENSITIVITY_X]: {
label: t('vertical-sensitivity'),
type: SettingElementType.NUMBER_STEPPER,
default: 50,
min: 1,
max: 200,
params: {
suffix: '%',
exactTicks: 20,
},
},
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: {
label: t('deadzone-counterweight'),
type: SettingElementType.NUMBER_STEPPER,
default: 20,
min: 1,
max: 100,
params: {
suffix: '%',
exactTicks: 10,
},
},
[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH]: {
label: t('stick-decay-strength'),
type: SettingElementType.NUMBER_STEPPER,
default: 100,
min: 10,
max: 100,
params: {
suffix: '%',
exactTicks: 10,
},
},
[MkbPresetKey.MOUSE_STICK_DECAY_MIN]: {
label: t('stick-decay-minimum'),
type: SettingElementType.NUMBER_STEPPER,
default: 10,
min: 1,
max: 10,
params: {
suffix: '%',
},
},
};
static DEFAULT_PRESET = {
'mapping': {
// Use "e.code" value from https://keyjs.dev
[GamepadKey.UP]: ['ArrowUp'],
[GamepadKey.DOWN]: ['ArrowDown'],
[GamepadKey.LEFT]: ['ArrowLeft'],
[GamepadKey.RIGHT]: ['ArrowRight'],
[GamepadKey.LS_UP]: ['KeyW'],
[GamepadKey.LS_DOWN]: ['KeyS'],
[GamepadKey.LS_LEFT]: ['KeyA'],
[GamepadKey.LS_RIGHT]: ['KeyD'],
[GamepadKey.RS_UP]: ['KeyI'],
[GamepadKey.RS_DOWN]: ['KeyK'],
[GamepadKey.RS_LEFT]: ['KeyJ'],
[GamepadKey.RS_RIGHT]: ['KeyL'],
[GamepadKey.A]: ['Space', 'KeyE'],
[GamepadKey.X]: ['KeyR'],
[GamepadKey.B]: ['ControlLeft', 'Backspace'],
[GamepadKey.Y]: ['KeyV'],
[GamepadKey.START]: ['Enter'],
[GamepadKey.SELECT]: ['Tab'],
[GamepadKey.LB]: ['KeyC', 'KeyG'],
[GamepadKey.RB]: ['KeyQ'],
[GamepadKey.HOME]: ['Backquote'],
[GamepadKey.RT]: [MouseButtonCode.LEFT_CLICK],
[GamepadKey.LT]: [MouseButtonCode.RIGHT_CLICK],
[GamepadKey.L3]: ['ShiftLeft'],
[GamepadKey.R3]: ['KeyF'],
},
'mouse': {
[MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo[MouseMapTo.RS],
[MkbPresetKey.MOUSE_SENSITIVITY_X]: 50,
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: 50,
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20,
[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH]: 18,
[MkbPresetKey.MOUSE_STICK_DECAY_MIN]: 6,
},
};
static convert(preset) {
const obj = {
'mapping': {},
'mouse': Object.assign({}, preset.mouse),
};
for (const buttonIndex in preset.mapping) {
for (const keyName of preset.mapping[buttonIndex]) {
obj.mapping[keyName] = parseInt(buttonIndex);
}
}
// Pre-calculate mouse's sensitivities
const mouse = obj.mouse;
mouse[MkbPresetKey.MOUSE_SENSITIVITY_X] *= MkbHandler.DEFAULT_PANNING_SENSITIVITY;
mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y] *= MkbHandler.DEFAULT_PANNING_SENSITIVITY;
mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT] *= MkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT;
mouse[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH] *= 0.01;
mouse[MkbPresetKey.MOUSE_STICK_DECAY_MIN] *= 0.01;
const mouseMapTo = MouseMapTo[mouse[MkbPresetKey.MOUSE_MAP_TO]];
if (typeof mouseMapTo !== 'undefined') {
mouse[MkbPresetKey.MOUSE_MAP_TO] = mouseMapTo;
} else {
mouse[MkbPresetKey.MOUSE_MAP_TO] = MkbPreset.MOUSE_SETTINGS[MkbPresetKey.MOUSE_MAP_TO].default;
}
console.log(obj);
return obj;
}
}

View File

@@ -0,0 +1,531 @@
import { GamepadKey } from "./definitions";
import { CE, createButton, ButtonStyle } from "../../utils/html";
import { t } from "../translation";
import { Dialog } from "../dialog";
import { getPref, setPref, PrefKey } from "../preferences";
import { MkbPresetKey, GamepadKeyName } from "./definitions";
import { KeyHelper } from "./key-helper";
import { MkbPreset } from "./mkb-preset";
import { MkbHandler } from "./mkb-handler";
import { LocalDb } from "../../utils/local-db";
import { Icon } from "../../utils/html";
import { SettingElement } from "../settings";
type MkbRemapperElements = {
wrapper: HTMLElement | null,
presetsSelect: HTMLSelectElement | null,
activateButton: HTMLButtonElement | null,
currentBindingKey: HTMLElement | null,
allKeyElements: HTMLElement[],
allMouseElements: {[key in MkbPresetKey]?: HTMLElement},
}
export class MkbRemapper {
get #BUTTON_ORDERS() {
return [
GamepadKey.UP,
GamepadKey.DOWN,
GamepadKey.LEFT,
GamepadKey.RIGHT,
GamepadKey.A,
GamepadKey.B,
GamepadKey.X,
GamepadKey.Y,
GamepadKey.LB,
GamepadKey.RB,
GamepadKey.LT,
GamepadKey.RT,
GamepadKey.SELECT,
GamepadKey.START,
GamepadKey.HOME,
GamepadKey.L3,
GamepadKey.LS_UP,
GamepadKey.LS_DOWN,
GamepadKey.LS_LEFT,
GamepadKey.LS_RIGHT,
GamepadKey.R3,
GamepadKey.RS_UP,
GamepadKey.RS_DOWN,
GamepadKey.RS_LEFT,
GamepadKey.RS_RIGHT,
];
};
static #instance: MkbRemapper;
static get INSTANCE() {
if (!MkbRemapper.#instance) {
MkbRemapper.#instance = new MkbRemapper();
}
return MkbRemapper.#instance;
};
#STATE = {
currentPresetId: 0,
presets: [],
editingPresetData: {},
isEditing: false,
};
#$: MkbRemapperElements = {
wrapper: null,
presetsSelect: null,
activateButton: null,
currentBindingKey: null,
allKeyElements: [],
allMouseElements: {},
};
bindingDialog: Dialog;
constructor() {
this.#STATE.currentPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
this.bindingDialog = new Dialog({
className: 'bx-binding-dialog',
content: CE('div', {},
CE('p', {}, t('press-to-bind')),
CE('i', {}, t('press-esc-to-cancel')),
),
hideCloseButton: true,
});
}
#clearEventListeners = () => {
window.removeEventListener('keydown', this.#onKeyDown);
window.removeEventListener('mousedown', this.#onMouseDown);
window.removeEventListener('wheel', this.#onWheel);
};
#bindKey = ($elm: HTMLElement, key: any) => {
const buttonIndex = parseInt($elm.getAttribute('data-button-index')!);
const keySlot = parseInt($elm.getAttribute('data-key-slot')!);
// Ignore if bind the save key to the same element
if ($elm.getAttribute('data-key-code') === key.code) {
return;
}
// Unbind duplicated keys
for (const $otherElm of this.#$.allKeyElements) {
if ($otherElm.getAttribute('data-key-code') === key.code) {
this.#unbindKey($otherElm);
}
}
this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = key.code;
$elm.textContent = key.name;
$elm.setAttribute('data-key-code', key.code);
}
#unbindKey = ($elm: HTMLElement) => {
const buttonIndex = parseInt($elm.getAttribute('data-button-index')!);
const keySlot = parseInt($elm.getAttribute('data-key-slot')!);
// Remove key from preset
this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = null;
$elm.textContent = '';
$elm.removeAttribute('data-key-code');
}
#onWheel = (e: WheelEvent) => {
e.preventDefault();
this.#clearEventListeners();
this.#bindKey(this.#$.currentBindingKey!, KeyHelper.getKeyFromEvent(e));
setTimeout(() => this.bindingDialog.hide(), 200);
};
#onMouseDown = e => {
e.preventDefault();
this.#clearEventListeners();
this.#bindKey(this.#$.currentBindingKey!, KeyHelper.getKeyFromEvent(e));
setTimeout(() => this.bindingDialog.hide(), 200);
};
#onKeyDown = e => {
e.preventDefault();
e.stopPropagation();
this.#clearEventListeners();
if (e.code !== 'Escape') {
this.#bindKey(this.#$.currentBindingKey!, KeyHelper.getKeyFromEvent(e));
}
setTimeout(() => this.bindingDialog.hide(), 200);
};
#onBindingKey = e => {
if (!this.#STATE.isEditing || e.button !== 0) {
return;
}
console.log(e);
this.#$.currentBindingKey = e.target;
window.addEventListener('keydown', this.#onKeyDown);
window.addEventListener('mousedown', this.#onMouseDown);
window.addEventListener('wheel', this.#onWheel);
this.bindingDialog.show({title: e.target.getAttribute('data-prompt')});
};
#onContextMenu = (e: Event) => {
e.preventDefault();
if (!this.#STATE.isEditing) {
return;
}
this.#unbindKey(e.target as HTMLElement);
};
#getPreset = (presetId: string) => {
return this.#STATE.presets[presetId];
}
#getCurrentPreset = () => {
return this.#getPreset(this.#STATE.currentPresetId);
}
#switchPreset = presetId => {
presetId = parseInt(presetId);
this.#STATE.currentPresetId = presetId;
const presetData = this.#getCurrentPreset().data;
for (const $elm of this.#$.allKeyElements) {
const buttonIndex = $elm.getAttribute('data-button-index');
const keySlot = $elm.getAttribute('data-key-slot');
const buttonKeys = presetData.mapping[buttonIndex];
if (buttonKeys && buttonKeys[keySlot]) {
$elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]);
$elm.setAttribute('data-key-code', buttonKeys[keySlot]);
} else {
$elm.textContent = '';
$elm.removeAttribute('data-key-code');
}
}
for (const key in this.#$.allMouseElements) {
const $elm = this.#$.allMouseElements[key as MkbPresetKey]!;
let value = presetData.mouse[key];
if (typeof value === 'undefined') {
value = MkbPreset.MOUSE_SETTINGS[key as MkbPresetKey].default;
}
'setValue' in $elm && ($elm as any).setValue(value);
}
// Update state of Activate button
const activated = getPref(PrefKey.MKB_DEFAULT_PRESET_ID) === this.#STATE.currentPresetId;
this.#$.activateButton!.disabled = activated;
this.#$.activateButton!.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
}
#refresh() {
// Clear presets select
while (this.#$.presetsSelect!.firstChild) {
this.#$.presetsSelect!.removeChild(this.#$.presetsSelect!.firstChild);
}
LocalDb.INSTANCE.getPresets()
.then(presets => {
this.#STATE.presets = presets;
const $fragment = document.createDocumentFragment();
let defaultPresetId;
if (this.#STATE.currentPresetId === 0) {
this.#STATE.currentPresetId = parseInt(Object.keys(presets)[0]);
defaultPresetId = this.#STATE.currentPresetId;
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, defaultPresetId);
MkbHandler.INSTANCE.refreshPresetData();
} else {
defaultPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
}
for (let id in presets) {
id = parseInt(id);
const preset = presets[id];
let name = preset.name;
if (id === defaultPresetId) {
name = `🎮 ` + name;
}
const $options = CE('option', {value: id}, name);
$options.selected = id === this.#STATE.currentPresetId;
$fragment.appendChild($options);
};
this.#$.presetsSelect!.appendChild($fragment);
// Update state of Activate button
const activated = defaultPresetId === this.#STATE.currentPresetId;
this.#$.activateButton!.disabled = activated;
this.#$.activateButton!.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
!this.#STATE.isEditing && this.#switchPreset(this.#STATE.currentPresetId);
});
}
#toggleEditing = (force?: boolean) => {
this.#STATE.isEditing = typeof force !== 'undefined' ? force : !this.#STATE.isEditing;
this.#$.wrapper!.classList.toggle('bx-editing', this.#STATE.isEditing);
if (this.#STATE.isEditing) {
this.#STATE.editingPresetData = structuredClone(this.#getCurrentPreset().data);
} else {
this.#STATE.editingPresetData = {};
}
const childElements = this.#$.wrapper!.querySelectorAll('select, button, input');
for (const $elm of Array.from(childElements)) {
if ($elm.parentElement!.parentElement!.classList.contains('bx-mkb-action-buttons')) {
continue;
}
let disable = !this.#STATE.isEditing;
if ($elm.parentElement!.classList.contains('bx-mkb-preset-tools')) {
disable = !disable;
}
($elm as HTMLButtonElement).disabled = disable;
}
}
render() {
this.#$.wrapper = CE('div', {'class': 'bx-mkb-settings'});
this.#$.presetsSelect = CE<HTMLSelectElement>('select', {});
this.#$.presetsSelect!.addEventListener('change', e => {
this.#switchPreset((e.target as HTMLSelectElement).value);
});
const promptNewName = (value?: string) => {
let newName: string | null = '';
while (!newName) {
newName = prompt(t('prompt-preset-name'), value);
if (newName === null) {
return false;
}
newName = newName.trim();
}
return newName ? newName : false;
};
const $header = CE('div', {'class': 'bx-mkb-preset-tools'},
this.#$.presetsSelect,
// Rename button
createButton({
title: t('rename'),
icon: Icon.CURSOR_TEXT,
onClick: e => {
const preset = this.#getCurrentPreset();
let newName = promptNewName(preset.name);
if (!newName || newName === preset.name) {
return;
}
// Update preset with new name
preset.name = newName;
LocalDb.INSTANCE.updatePreset(preset).then(id => this.#refresh());
},
}),
// New button
createButton({
icon: Icon.NEW,
title: t('new'),
onClick: e => {
let newName = promptNewName('');
if (!newName) {
return;
}
// Create new preset selected name
LocalDb.INSTANCE.newPreset(newName, MkbPreset.DEFAULT_PRESET).then(id => {
this.#STATE.currentPresetId = id;
this.#refresh();
});
},
}),
// Copy button
createButton({
icon: Icon.COPY,
title: t('copy'),
onClick: e => {
const preset = this.#getCurrentPreset();
let newName = promptNewName(`${preset.name} (2)`);
if (!newName) {
return;
}
// Create new preset selected name
LocalDb.INSTANCE.newPreset(newName, preset.data).then(id => {
this.#STATE.currentPresetId = id;
this.#refresh();
});
},
}),
// Delete button
createButton({
icon: Icon.TRASH,
style: ButtonStyle.DANGER,
title: t('delete'),
onClick: e => {
if (!confirm(t('confirm-delete-preset'))) {
return;
}
LocalDb.INSTANCE.deletePreset(this.#STATE.currentPresetId).then(id => {
this.#STATE.currentPresetId = 0;
this.#refresh();
});
},
}),
);
this.#$.wrapper!.appendChild($header);
const $rows = CE('div', {'class': 'bx-mkb-settings-rows'},
CE('i', {'class': 'bx-mkb-note'}, t('right-click-to-unbind')),
);
// Render keys
const keysPerButton = 2;
for (const buttonIndex of this.#BUTTON_ORDERS) {
const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex];
let $elm;
const $fragment = document.createDocumentFragment();
for (let i = 0; i < keysPerButton; i++) {
$elm = CE('button', {
'data-prompt': buttonPrompt,
'data-button-index': buttonIndex,
'data-key-slot': i,
}, ' ');
$elm.addEventListener('mouseup', this.#onBindingKey);
$elm.addEventListener('contextmenu', this.#onContextMenu);
$fragment.appendChild($elm);
this.#$.allKeyElements.push($elm);
}
const $keyRow = CE('div', {'class': 'bx-mkb-key-row'},
CE('label', {'title': buttonName}, buttonPrompt),
$fragment,
);
$rows.appendChild($keyRow);
}
$rows.appendChild(CE('i', {'class': 'bx-mkb-note'}, t('mkb-adjust-ingame-settings')),);
// Render mouse settings
const $mouseSettings = document.createDocumentFragment();
for (const key in MkbPreset.MOUSE_SETTINGS) {
const setting = MkbPreset.MOUSE_SETTINGS[key as MkbPresetKey];
const value = setting.default;
let $elm;
const onChange = (e, value) => {
this.#STATE.editingPresetData.mouse[key] = value;
};
const $row = CE('div', {'class': 'bx-quick-settings-row'},
CE('label', {'for': `bx_setting_${key}`}, setting.label),
$elm = SettingElement.render(setting.type, key, setting, value, onChange, setting.params),
);
$mouseSettings.appendChild($row);
this.#$.allMouseElements[key as MkbPresetKey] = $elm;
}
$rows.appendChild($mouseSettings);
this.#$.wrapper!.appendChild($rows);
// Render action buttons
const $actionButtons = CE('div', {'class': 'bx-mkb-action-buttons'},
CE('div', {},
// Edit button
createButton({
label: t('edit'),
onClick: e => this.#toggleEditing(true),
}),
// Activate button
this.#$.activateButton = createButton({
label: t('activate'),
style: ButtonStyle.PRIMARY,
onClick: e => {
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.#STATE.currentPresetId);
MkbHandler.INSTANCE.refreshPresetData();
this.#refresh();
},
}),
),
CE('div', {},
// Cancel button
createButton({
label: t('cancel'),
style: ButtonStyle.GHOST,
onClick: e => {
// Restore preset
this.#switchPreset(this.#STATE.currentPresetId);
this.#toggleEditing(false);
},
}),
// Save button
createButton({
label: t('save'),
style: ButtonStyle.PRIMARY,
onClick: e => {
const updatedPreset = structuredClone(this.#getCurrentPreset());
updatedPreset.data = this.#STATE.editingPresetData;
LocalDb.INSTANCE.updatePreset(updatedPreset).then(id => {
// If this is the default preset => refresh preset data
if (id === getPref(PrefKey.MKB_DEFAULT_PRESET_ID)) {
MkbHandler.INSTANCE.refreshPresetData();
}
this.#toggleEditing(false);
this.#refresh();
});
},
}),
),
);
this.#$.wrapper!.appendChild($actionButtons);
this.#toggleEditing(false);
this.#refresh();
return this.#$.wrapper;
}
}