cloud-game/web/js/api.js
Sergey Stepanov 7ee98c1b03 Add keyboard and mouse support
Keyboard and mouse controls will now work if you use the kbMouseSupport parameter in the config for Libretro cores. Be aware that capturing mouse and keyboard controls properly is only possible in fullscreen mode.

Note: In the case of DOSBox, a virtual filesystem handler is not yet implemented, thus each game state will be shared between all rooms (DOS game instances) of CloudRetro.
2024-08-02 11:04:44 +03:00

335 lines
8.5 KiB
JavaScript

import {log} from 'log';
const endpoints = {
LATENCY_CHECK: 3,
INIT: 4,
INIT_WEBRTC: 100,
OFFER: 101,
ANSWER: 102,
ICE_CANDIDATE: 103,
GAME_START: 104,
GAME_QUIT: 105,
GAME_SAVE: 106,
GAME_LOAD: 107,
GAME_SET_PLAYER_INDEX: 108,
GAME_RECORDING: 110,
GET_WORKER_LIST: 111,
GAME_ERROR_NO_FREE_SLOTS: 112,
APP_VIDEO_CHANGE: 150,
}
let transport = {
send: (packet) => {
log.warn('Default transport is used! Change it with the api.transport variable.', packet)
},
keyboard: (packet) => {
log.warn('Default transport is used! Change it with the api.transport variable.', packet)
},
mouse: (packet) => {
log.warn('Default transport is used! Change it with the api.transport variable.', packet)
}
}
const packet = (type, payload, id) => {
const packet = {t: type}
if (id !== undefined) packet.id = id
if (payload !== undefined) packet.p = payload
transport.send(packet)
}
const decodeBytes = (b) => String.fromCharCode.apply(null, new Uint8Array(b))
const keyboardPress = (() => {
// 0 1 2 3 4 5 6
// [CODE ] P MOD
const buffer = new ArrayBuffer(7)
const dv = new DataView(buffer)
return (pressed = false, e) => {
if (e.repeat) return // skip pressed key events
const key = libretro.mod
let code = libretro.map('', e.code)
let shift = e.shiftKey
// a special Esc for &$&!& Firefox
if (shift && code === 96) {
code = 27
shift = false
}
const mod = 0
| (e.altKey && key.ALT)
| (e.ctrlKey && key.CTRL)
| (e.metaKey && key.META)
| (shift && key.SHIFT)
| (e.getModifierState('NumLock') && key.NUMLOCK)
| (e.getModifierState('CapsLock') && key.CAPSLOCK)
| (e.getModifierState('ScrollLock') && key.SCROLLOCK)
dv.setUint32(0, code)
dv.setUint8(4, +pressed)
dv.setUint16(5, mod)
transport.keyboard(buffer)
}
})()
const mouse = {
MOVEMENT: 0,
BUTTONS: 1
}
const mouseMove = (() => {
// 0 1 2 3 4
// T DX DY
const buffer = new ArrayBuffer(5)
const dv = new DataView(buffer)
return (dx = 0, dy = 0) => {
dv.setUint8(0, mouse.MOVEMENT)
dv.setInt16(1, dx)
dv.setInt16(3, dy)
transport.mouse(buffer)
}
})()
const mousePress = (() => {
// 0 1
// T B
const buffer = new ArrayBuffer(2)
const dv = new DataView(buffer)
// 0: Main button pressed, usually the left button or the un-initialized state
// 1: Auxiliary button pressed, usually the wheel button or the middle button (if present)
// 2: Secondary button pressed, usually the right button
// 3: Fourth button, typically the Browser Back button
// 4: Fifth button, typically the Browser Forward button
const b2r = [1, 4, 2, 0, 0] // browser mouse button to retro button
// assumed that only one button pressed / released
return (button = 0, pressed = false) => {
dv.setUint8(0, mouse.BUTTONS)
dv.setUint8(1, pressed ? b2r[button] : 0)
transport.mouse(buffer)
}
})()
const libretro = function () {// RETRO_KEYBOARD
const retro = {
'': 0,
'Unidentified': 0,
'Unknown': 0, // ???
'First': 0, // ???
'Backspace': 8,
'Tab': 9,
'Clear': 12,
'Enter': 13, 'Return': 13,
'Pause': 19,
'Escape': 27,
'Space': 32,
'Exclaim': 33,
'Quotedbl': 34,
'Hash': 35,
'Dollar': 36,
'Ampersand': 38,
'Quote': 39,
'Leftparen': 40, '(': 40,
'Rightparen': 41, ')': 41,
'Asterisk': 42,
'Plus': 43,
'Comma': 44,
'Minus': 45,
'Period': 46,
'Slash': 47,
'Digit0': 48,
'Digit1': 49,
'Digit2': 50,
'Digit3': 51,
'Digit4': 52,
'Digit5': 53,
'Digit6': 54,
'Digit7': 55,
'Digit8': 56,
'Digit9': 57,
'Colon': 58, ':': 58,
'Semicolon': 59, ';': 59,
'Less': 60, '<': 60,
'Equal': 61, '=': 61,
'Greater': 62, '>': 62,
'Question': 63, '?': 63,
// RETROK_AT = 64,
'BracketLeft': 91, '[': 91,
'Backslash': 92, '\\': 92,
'BracketRight': 93, ']': 93,
// RETROK_CARET = 94,
// RETROK_UNDERSCORE = 95,
'Backquote': 96, '`': 96,
'KeyA': 97,
'KeyB': 98,
'KeyC': 99,
'KeyD': 100,
'KeyE': 101,
'KeyF': 102,
'KeyG': 103,
'KeyH': 104,
'KeyI': 105,
'KeyJ': 106,
'KeyK': 107,
'KeyL': 108,
'KeyM': 109,
'KeyN': 110,
'KeyO': 111,
'KeyP': 112,
'KeyQ': 113,
'KeyR': 114,
'KeyS': 115,
'KeyT': 116,
'KeyU': 117,
'KeyV': 118,
'KeyW': 119,
'KeyX': 120,
'KeyY': 121,
'KeyZ': 122,
'{': 123,
'|': 124,
'}': 125,
'Tilde': 126, '~': 126,
'Delete': 127,
'Numpad0': 256,
'Numpad1': 257,
'Numpad2': 258,
'Numpad3': 259,
'Numpad4': 260,
'Numpad5': 261,
'Numpad6': 262,
'Numpad7': 263,
'Numpad8': 264,
'Numpad9': 265,
'NumpadDecimal': 266,
'NumpadDivide': 267,
'NumpadMultiply': 268,
'NumpadSubtract': 269,
'NumpadAdd': 270,
'NumpadEnter': 271,
'NumpadEqual': 272,
'ArrowUp': 273,
'ArrowDown': 274,
'ArrowRight': 275,
'ArrowLeft': 276,
'Insert': 277,
'Home': 278,
'End': 279,
'PageUp': 280,
'PageDown': 281,
'F1': 282,
'F2': 283,
'F3': 284,
'F4': 285,
'F5': 286,
'F6': 287,
'F7': 288,
'F8': 289,
'F9': 290,
'F10': 291,
'F11': 292,
'F12': 293,
'F13': 294,
'F14': 295,
'F15': 296,
'NumLock': 300,
'CapsLock': 301,
'ScrollLock': 302,
'ShiftRight': 303,
'ShiftLeft': 304,
'ControlRight': 305,
'ControlLeft': 306,
'AltRight': 307,
'AltLeft': 308,
'MetaRight': 309,
'MetaLeft': 310,
// RETROK_LSUPER = 311,
// RETROK_RSUPER = 312,
// RETROK_MODE = 313,
// RETROK_COMPOSE = 314,
// RETROK_HELP = 315,
// RETROK_PRINT = 316,
// RETROK_SYSREQ = 317,
// RETROK_BREAK = 318,
// RETROK_MENU = 319,
'Power': 320,
// RETROK_EURO = 321,
// RETROK_UNDO = 322,
// RETROK_OEM_102 = 323,
}
const retroMod = {
NONE: 0x0000,
SHIFT: 0x01,
CTRL: 0x02,
ALT: 0x04,
META: 0x08,
NUMLOCK: 0x10,
CAPSLOCK: 0x20,
SCROLLOCK: 0x40,
}
const _map = (key = '', code = '') => {
return retro[code] || retro[key] || 0
}
return {
map: _map,
mod: retroMod,
}
}()
/**
* Server API.
*
* Requires the actual api.transport implementation.
*/
export const api = {
set transport(t) {
transport = t;
},
endpoint: endpoints,
decode: (b) => JSON.parse(decodeBytes(b)),
server: {
initWebrtc: () => packet(endpoints.INIT_WEBRTC),
sendIceCandidate: (candidate) => packet(endpoints.ICE_CANDIDATE, btoa(JSON.stringify(candidate))),
sendSdp: (sdp) => packet(endpoints.ANSWER, btoa(JSON.stringify(sdp))),
latencyCheck: (id, list) => packet(endpoints.LATENCY_CHECK, list, id),
getWorkerList: () => packet(endpoints.GET_WORKER_LIST),
},
game: {
input: {
keyboard: {
press: keyboardPress,
},
mouse: {
move: mouseMove,
press: mousePress,
}
},
load: () => packet(endpoints.GAME_LOAD),
save: () => packet(endpoints.GAME_SAVE),
setPlayerIndex: (i) => packet(endpoints.GAME_SET_PLAYER_INDEX, i),
start: (game, roomId, record, recordUser, player) => packet(endpoints.GAME_START, {
game_name: game,
room_id: roomId,
player_index: player,
record: record,
record_user: recordUser,
}),
toggleRecording: (active = false, userName = '') =>
packet(endpoints.GAME_RECORDING, {active: active, user: userName}),
quit: (roomId) => packet(endpoints.GAME_QUIT, {room_id: roomId}),
}
}