import { log } from "log"; const endpoints = { LATENCY_CHECK: 3, INIT: 4, INIT_WEBRTC_STREAM: 100, WEBRTC_SIGNAL: 101, 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, GAME_RESET: 113, APP_VIDEO_CHANGE: 150, }; const endpointName = Object.fromEntries( Object.entries(endpoints).map(([k, v]) => [v, k]), ); 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 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, }; })(); // data type converters const toJson = (val) => JSON.stringify(val); const fromJson = (val) => JSON.parse(val); const fromBytes = (val) => JSON.parse(String.fromCharCode.apply(null, new Uint8Array(val))); /** * Server API. * * Requires the actual api.transport implementation. */ export const api = { set transport(t) { transport = t; }, endpoint: endpoints, endpointName: endpointName, fromBytes, fromJson, server: { /** Initializes the stream with the given config. * @property {boolean} initiator - whether the user is an initiator or not. * @property {string} sdpOffer - optional SDP offer, just to make negotiation a bit faster. */ initWebrtcStream: ({ initiator = false, sdpOffer = "" } = {}) => packet(endpoints.INIT_WEBRTC_STREAM, { initiator, ...(sdpOffer && { sdp: toJson(sdpOffer) }), }), sendIceCandidate: (candidate) => packet(endpoints.WEBRTC_SIGNAL, { ice: toJson(candidate) }), sendSdp: (sdp) => packet(endpoints.WEBRTC_SIGNAL, { sdp: toJson(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), reset: (roomId) => packet(endpoints.GAME_RESET, { room_id: roomId }), 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 }), }, };