From ef67a724fd5d6805c0dcd42318e3b12424614fcd Mon Sep 17 00:00:00 2001
From: S-Coding23 <>
Date: Tue, 10 Mar 2026 14:05:53 -0600
Subject: [PATCH] Final Netplay and Dynamic Cheat fixes
---
data/emulator.css | 162 +-
data/localization/vi.json | 606 +--
data/src/consts.js | 90 +-
data/src/emulator.js | 8291 ++++++++++++++++++++++---------------
data/src/netplay.js | 2119 ++++++++++
5 files changed, 7576 insertions(+), 3692 deletions(-)
create mode 100644 data/src/netplay.js
diff --git a/data/emulator.css b/data/emulator.css
index 873547b7a..59e8d8b0c 100644
--- a/data/emulator.css
+++ b/data/emulator.css
@@ -234,23 +234,149 @@
}
.netplay-hidden {
- display: none !important;
+ display: none !important;
}
.option-enabled {
- color: inherit;
- opacity: 1;
+ color: inherit;
+ opacity: 1;
}
.ejs_netplay_warning {
- padding: 10px;
- margin-bottom: 15px;
- text-align: center;
- color: black !important;
- background-color: rgba(255, 165, 0, 0.8);
- border: 1px solid orange;
- border-radius: 5px;
- font-size: 0.9em;
+ padding: 10px;
+ margin-bottom: 15px;
+ text-align: center;
+ color: black !important;
+ background-color: rgba(255, 165, 0, 0.8);
+ border: 1px solid orange;
+ border-radius: 5px;
+ font-size: 0.9em;
+}
+
+.ejs_netplay_chat_header_row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 6px;
+}
+
+.ejs_netplay_chat_hint {
+ font-size: 11px;
+ color: #888;
+}
+
+.ejs_netplay_offscreen {
+ position: fixed;
+ left: -9999px;
+ top: -9999px;
+ width: 1px;
+ height: 1px;
+ opacity: 0;
+}
+
+.ejs_netplay_offscreen_canvas {
+ position: fixed;
+ left: -9999px;
+ top: -9999px;
+ visibility: hidden;
+}
+
+.ejs_netplay_overlay {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100vw;
+ height: 100vh;
+ z-index: 10001;
+ pointer-events: none;
+ overflow: visible;
+ background: transparent;
+}
+
+.ejs_netplay_canvas {
+ position: fixed;
+ left: 0;
+ top: 0;
+ background: #000;
+ z-index: 10001;
+ image-rendering: pixelated;
+ image-rendering: crisp-edges;
+ pointer-events: none;
+}
+
+.ejs_netplay_chat_container {
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+ padding-top: 8px;
+}
+
+.ejs_netplay_chat_log {
+ background: rgba(0, 0, 0, 0.4);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 4px;
+ padding: 6px 8px;
+ min-height: 60px;
+ max-height: 120px;
+ overflow-y: auto;
+ font-size: 12px;
+ margin-bottom: 6px;
+ word-break: break-word;
+}
+
+.ejs_netplay_chat_row {
+ display: flex;
+ gap: 4px;
+ align-items: stretch;
+}
+
+.ejs_netplay_chat_to {
+ background-color: #333;
+ color: #fff;
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 4px;
+ padding: 4px 6px;
+ font-size: 12px;
+ min-width: 90px;
+ max-width: 120px;
+ cursor: pointer;
+}
+
+.ejs_netplay_chat_input {
+ flex: 1;
+ background-color: #222;
+ color: #fff;
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 4px;
+ padding: 4px 8px;
+ font-size: 13px;
+ outline: none;
+}
+
+.ejs_netplay_chat_input:focus {
+ border-color: rgba(var(--ejs-primary-color), 0.6);
+}
+
+.ejs_netplay_dialog_label {
+ margin-bottom: 8px;
+ font-size: 13px;
+ color: #bcbcbc;
+}
+
+.ejs_netplay_dialog_buttons {
+ display: flex;
+ justify-content: center;
+ gap: 10px;
+ margin-top: 15px;
+}
+
+.ejs_netplay_error_box {
+ background: rgba(248, 35, 0, 0.15);
+ border: 1px solid rgba(248, 35, 0, 0.4);
+ border-radius: 4px;
+ padding: 8px 12px;
+ margin-top: 10px;
+ color: #ff6b6b;
+ font-size: 13px;
+ text-align: center;
}
.ejs_context_menu {
@@ -474,9 +600,6 @@
right: -3px;
}
-/* .ejs_big_screen .ejs_settings_parent::after {
- right: 15px;
-} */
.ejs_big_screen .ejs_settings_text {
display: none;
}
@@ -927,15 +1050,6 @@
-webkit-backdrop-filter: blur(8px);
}
-/* .ejs_settings_parent::after {
- border: 5px solid transparent;
- border-top-color: rgba(119, 119, 119, 0.9);
- content: '';
- height: 0;
- position: absolute;
- top: 100%;
- width: 0;
-} */
.ejs_settings_parent::before,
.ejs_settings_parent::after {
position: absolute;
@@ -1649,4 +1763,4 @@
justify-content: center;
display: flex;
align-items: center;
-}
+}
\ No newline at end of file
diff --git a/data/localization/vi.json b/data/localization/vi.json
index 781ddb054..afedf1bb7 100644
--- a/data/localization/vi.json
+++ b/data/localization/vi.json
@@ -1,303 +1,303 @@
-{
- "0": "0",
- "1": "1",
- "2": "2",
- "3": "3",
- "4": "4",
- "5": "5",
- "6": "6",
- "7": "7",
- "8": "8",
- "9": "9",
- "Restart": "Chạy lại",
- "Pause": "Tạm dừng",
- "Play": "Chơi",
- "Save State": "Lưu State",
- "Load State": "Nạp State",
- "Control Settings": "Cài đặt điều khiển",
- "Cheats": "Gian lận xíu",
- "Cache Manager": "Bộ nhớ đệm",
- "Export Save File": "Xuất tệp lưu",
- "Import Save File": "Nhập tệp lưu ",
- "Netplay": "Chơi qua mạng",
- "Mute": "Tắt âm",
- "Unmute": "Mở âm",
- "Settings": "Cài đặt",
- "Enter Fullscreen": "Toàn màn hình",
- "Exit Fullscreen": "Thoát toàn màn hình",
- "Context Menu": "Menu chuột phải",
- "Reset": "Đặt lại",
- "Clear": "Xoá",
- "Close": "Đóng",
- "QUICK SAVE STATE": "LƯU NHANH",
- "QUICK LOAD STATE": "NẠP NHANH",
- "CHANGE STATE SLOT": "ĐỔI NHANH",
- "FAST FORWARD": "TIẾN NHANH ",
- "Player": "Người chơi",
- "Connected Gamepad": "Bảng điều khiển đã kết nối",
- "Gamepad": "Bảng điều khiển ",
- "Keyboard": "Bàn phím",
- "Set": "Đặt",
- "Add Cheat": "Thêm mật mã",
- "Create a Room": "Tạo phòng",
- "Rooms": "Các phòng",
- "Start Game": "Bắt đầu chơi",
- "Loading...": "Đang nạp...",
- "Download Game Core": "Tải xuống nhân trò chơi",
- "Decompress Game Core": "Giải nén nhân trò chơi",
- "Download Game Data": "Tải xuống dữ liệu trò chơi",
- "Decompress Game Data": "Giải nén dữ liệu trò chơi ",
- "Shaders": "Shaders",
- "Disabled": "Vô hiệu",
- "2xScaleHQ": "2xScaleHQ",
- "4xScaleHQ": "4xScaleHQ",
- "CRT easymode": "CRT chế độ dễ",
- "CRT aperture": "CRT aperture",
- "CRT geom": "CRT geom",
- "CRT mattias": "CRT mattias",
- "FPS": "FPS",
- "show": "hiện",
- "hide": "ẩn",
- "Fast Forward Ratio": "Tỷ lệ tiến nhanh",
- "Fast Forward": "Tiến nhanh",
- "Enabled": "Cho phép",
- "Save State Slot": "Lưu trạng thái thẻ",
- "Save State Location": "Lưu trạng thái vị trí",
- "Download": "Tải về",
- "Keep in Browser": "Giữ ở trình duyệt",
- "Auto": "Auto",
- "NTSC": "NTSC",
- "PAL": "PAL",
- "Dendy": "Dendy",
- "8:7 PAR": "8:7 PAR",
- "4:3": "4:3",
- "Low": "Thấp",
- "High": "Cao",
- "Very High": "Rất cao",
- "None": "Không gì",
- "Player 1": "Game thủ 1",
- "Player 2": "Game thủ 2",
- "Both": "Cả hai",
- "SAVED STATE TO SLOT": "SAVED STATE TO SLOT",
- "LOADED STATE FROM SLOT": "LOADED STATE FROM SLOT",
- "SET SAVE STATE SLOT TO": "SET SAVE STATE SLOT TO",
- "Network Error": "Mạng bị lỗi",
- "Submit": "Gửi đi",
- "Description": "Mô tả",
- "Code": "Mã",
- "Add Cheat Code": "Thêm mã gian lận",
- "Leave Room": "Rời phòng",
- "Password": "Mật khẩu",
- "Password (optional)": "Mật khẩu (tùy chọn)",
- "Max Players": "Người chơi tối đa",
- "Room Name": "Tên phòng",
- "Join": "Tham gia",
- "Player Name": "Tên người chơi",
- "Set Player Name": "Đặt tên người chơi",
- "Left Handed Mode": "Chế độ tay trái",
- "Virtual Gamepad": "Bàn phím ảo",
- "Disk": "Đĩa",
- "Press Keyboard": "Bàn phím",
- "INSERT COIN": "THÊM XU",
- "Remove": "Loại bỏ",
- "LOADED STATE FROM BROWSER": "TRẠNG THÁI ĐÃ TẢI TỪ TRÌNH DUYỆT",
- "SAVED STATE TO BROWSER": "TRẠNG THÁI ĐÃ LƯU VÀO TRÌNH DUYỆT",
- "Join the discord": "Tham gia thảo luận",
- "View on GitHub": "Xem trên GitHub",
- "Failed to start game": "Thất bại khởi động game",
- "Download Game BIOS": "Tải Game BIOS",
- "Decompress Game BIOS": "Giải nén Game BIOS",
- "Download Game Parent": "Tải Game cha",
- "Decompress Game Parent": "Giải nén Game cha",
- "Download Game Patch": "Tải vá Game ",
- "Decompress Game Patch": "Giải nén Game vá",
- "Download Game State": "Tải trạng thái Game",
- "Check console": "Kiểm tra log console",
- "Error for site owner": "Lỗi sở hữu trang chủ",
- "EmulatorJS": "EmulatorJS",
- "Clear All": "Xóa hết",
- "Take Screenshot": "Chụp màn hình",
- "Quick Save": "Lưu nhanh",
- "Quick Load": "Nạp nhanh",
- "REWIND": "REWIND",
- "Rewind Enabled (requires restart)": "Cho phép quay lui (cần khởi động lại)",
- "Rewind Granularity": "Rewind Granularity",
- "Slow Motion Ratio": "Tỷ lệ chuyển động chậm",
- "Slow Motion": "chuyển động chậm",
- "Home": "Nhà",
- "EmulatorJS License": "Giấy phép EmulatorJS",
- "RetroArch License": "Giấy phép RetroArch ",
- "SLOW MOTION": "CHUYỂN ĐỘNG CHẬM",
- "A": "A",
- "B": "B",
- "SELECT": "SELECT",
- "START": "START",
- "UP": "UP",
- "DOWN": "DOWN",
- "LEFT": "LEFT",
- "RIGHT": "RIGHT",
- "X": "X",
- "Y": "Y",
- "L": "L",
- "R": "R",
- "Z": "Z",
- "STICK UP": "STICK UP",
- "STICK DOWN": "STICK DOWN",
- "STICK LEFT": "STICK LEFT",
- "STICK RIGHT": "STICK RIGHT",
- "C-PAD UP": "C-PAD UP",
- "C-PAD DOWN": "C-PAD DOWN",
- "C-PAD LEFT": "C-PAD LEFT",
- "C-PAD RIGHT": "C-PAD RIGHT",
- "MICROPHONE": "MICROPHONE",
- "BUTTON 1 / START": "BUTTON 1 / START",
- "BUTTON 2": "BUTTON 2",
- "BUTTON": "BUTTON",
- "LEFT D-PAD UP": "LEFT D-PAD UP",
- "LEFT D-PAD DOWN": "LEFT D-PAD DOWN",
- "LEFT D-PAD LEFT": "LEFT D-PAD LEFT",
- "LEFT D-PAD RIGHT": "LEFT D-PAD RIGHT",
- "RIGHT D-PAD UP": "RIGHT D-PAD UP",
- "RIGHT D-PAD DOWN": "RIGHT D-PAD DOWN",
- "RIGHT D-PAD LEFT": "RIGHT D-PAD LEFT",
- "RIGHT D-PAD RIGHT": "RIGHT D-PAD RIGHT",
- "C": "C",
- "MODE": "MODE",
- "FIRE": "FIRE",
- "RESET": "RESET",
- "LEFT DIFFICULTY A": "LEFT DIFFICULTY A",
- "LEFT DIFFICULTY B": "LEFT DIFFICULTY B",
- "RIGHT DIFFICULTY A": "RIGHT DIFFICULTY A",
- "RIGHT DIFFICULTY B": "RIGHT DIFFICULTY B",
- "COLOR": "COLOR",
- "B/W": "B/W",
- "PAUSE": "PAUSE",
- "OPTION": "OPTION",
- "OPTION 1": "OPTION 1",
- "OPTION 2": "OPTION 2",
- "L2": "L2",
- "R2": "R2",
- "L3": "L3",
- "R3": "R3",
- "L STICK UP": "L STICK UP",
- "L STICK DOWN": "L STICK DOWN",
- "L STICK LEFT": "L STICK LEFT",
- "L STICK RIGHT": "L STICK RIGHT",
- "R STICK UP": "R STICK UP",
- "R STICK DOWN": "R STICK DOWN",
- "R STICK LEFT": "R STICK LEFT",
- "R STICK RIGHT": "R STICK RIGHT",
- "Start": "Start",
- "Select": "Select",
- "Fast": "Fast",
- "Slow": "Slow",
- "a": "a",
- "b": "b",
- "c": "c",
- "d": "d",
- "e": "e",
- "f": "f",
- "g": "g",
- "h": "h",
- "i": "i",
- "j": "j",
- "k": "k",
- "l": "l",
- "m": "m",
- "n": "n",
- "o": "o",
- "p": "p",
- "q": "q",
- "r": "r",
- "s": "s",
- "t": "t",
- "u": "u",
- "v": "v",
- "w": "w",
- "x": "x",
- "y": "y",
- "z": "z",
- "enter": "enter",
- "escape": "escape",
- "space": "space",
- "tab": "tab",
- "backspace": "backspace",
- "delete": "delete",
- "arrowup": "arrowup",
- "arrowdown": "arrowdown",
- "arrowleft": "arrowleft",
- "arrowright": "arrowright",
- "f1": "f1",
- "f2": "f2",
- "f3": "f3",
- "f4": "f4",
- "f5": "f5",
- "f6": "f6",
- "f7": "f7",
- "f8": "f8",
- "f9": "f9",
- "f10": "f10",
- "f11": "f11",
- "f12": "f12",
- "shift": "shift",
- "control": "control",
- "alt": "alt",
- "meta": "meta",
- "capslock": "capslock",
- "insert": "insert",
- "home": "home",
- "end": "end",
- "pageup": "pageup",
- "pagedown": "pagedown",
- "!": "!",
- "@": "@",
- "#": "#",
- "$": "$",
- "%": "%",
- "^": "^",
- "&": "&",
- "*": "*",
- "(": "(",
- ")": ")",
- "-": "-",
- "_": "_",
- "+": "+",
- "=": "=",
- "[": "[",
- "]": "]",
- "{": "{",
- "}": "}",
- ";": ";",
- ":": ":",
- "'": "'",
- "\"": "\"",
- ",": ",",
- ".": ".",
- "<": "<",
- ">": ">",
- "/": "/",
- "?": "?",
- "LEFT_STICK_X": "LEFT_STICK_X",
- "LEFT_STICK_Y": "LEFT_STICK_Y",
- "RIGHT_STICK_X": "RIGHT_STICK_X",
- "RIGHT_STICK_Y": "RIGHT_STICK_Y",
- "LEFT_TRIGGER": "LEFT_TRIGGER",
- "RIGHT_TRIGGER": "RIGHT_TRIGGER",
- "A_BUTTON": "A_BUTTON",
- "B_BUTTON": "B_BUTTON",
- "X_BUTTON": "X_BUTTON",
- "Y_BUTTON": "Y_BUTTON",
- "START_BUTTON": "START_BUTTON",
- "SELECT_BUTTON": "SELECT_BUTTON",
- "L1_BUTTON": "L1_BUTTON",
- "R1_BUTTON": "R1_BUTTON",
- "L2_BUTTON": "L2_BUTTON",
- "R2_BUTTON": "R2_BUTTON",
- "LEFT_THUMB_BUTTON": "LEFT_THUMB_BUTTON",
- "RIGHT_THUMB_BUTTON": "RIGHT_THUMB_BUTTON",
- "DPAD_UP": "DPAD_UP",
- "DPAD_DOWN": "DPAD_DOWN",
- "DPAD_LEFT": "DPAD_LEFT",
- "DPAD_RIGHT": "DPAD_RIGHT",
- "Autofire": "Autofire"
-}
+{
+ "0": "0",
+ "1": "1",
+ "2": "2",
+ "3": "3",
+ "4": "4",
+ "5": "5",
+ "6": "6",
+ "7": "7",
+ "8": "8",
+ "9": "9",
+ "Restart": "Chạy lại",
+ "Pause": "Tạm dừng",
+ "Play": "Chơi",
+ "Save State": "Lưu State",
+ "Load State": "Nạp State",
+ "Control Settings": "Cài đặt điều khiển",
+ "Cheats": "Gian lận xíu",
+ "Cache Manager": "Bộ nhớ đệm",
+ "Export Save File": "Xuất tệp lưu",
+ "Import Save File": "Nhập tệp lưu ",
+ "Netplay": "Chơi qua mạng",
+ "Mute": "Tắt âm",
+ "Unmute": "Mở âm",
+ "Settings": "Cài đặt",
+ "Enter Fullscreen": "Toàn màn hình",
+ "Exit Fullscreen": "Thoát toàn màn hình",
+ "Context Menu": "Menu chuột phải",
+ "Reset": "Đặt lại",
+ "Clear": "Xoá",
+ "Close": "Đóng",
+ "QUICK SAVE STATE": "LƯU NHANH",
+ "QUICK LOAD STATE": "NẠP NHANH",
+ "CHANGE STATE SLOT": "ĐỔI NHANH",
+ "FAST FORWARD": "TIẾN NHANH ",
+ "Player": "Người chơi",
+ "Connected Gamepad": "Bảng điều khiển đã kết nối",
+ "Gamepad": "Bảng điều khiển ",
+ "Keyboard": "Bàn phím",
+ "Set": "Đặt",
+ "Add Cheat": "Thêm mật mã",
+ "Create a Room": "Tạo phòng",
+ "Rooms": "Các phòng",
+ "Start Game": "Bắt đầu chơi",
+ "Loading...": "Đang nạp...",
+ "Download Game Core": "Tải xuống nhân trò chơi",
+ "Decompress Game Core": "Giải nén nhân trò chơi",
+ "Download Game Data": "Tải xuống dữ liệu trò chơi",
+ "Decompress Game Data": "Giải nén dữ liệu trò chơi ",
+ "Shaders": "Shaders",
+ "Disabled": "Vô hiệu",
+ "2xScaleHQ": "2xScaleHQ",
+ "4xScaleHQ": "4xScaleHQ",
+ "CRT easymode": "CRT chế độ dễ",
+ "CRT aperture": "CRT aperture",
+ "CRT geom": "CRT geom",
+ "CRT mattias": "CRT mattias",
+ "FPS": "FPS",
+ "show": "hiện",
+ "hide": "ẩn",
+ "Fast Forward Ratio": "Tỷ lệ tiến nhanh",
+ "Fast Forward": "Tiến nhanh",
+ "Enabled": "Cho phép",
+ "Save State Slot": "Lưu trạng thái thẻ",
+ "Save State Location": "Lưu trạng thái vị trí",
+ "Download": "Tải về",
+ "Keep in Browser": "Giữ ở trình duyệt",
+ "Auto": "Auto",
+ "NTSC": "NTSC",
+ "PAL": "PAL",
+ "Dendy": "Dendy",
+ "8:7 PAR": "8:7 PAR",
+ "4:3": "4:3",
+ "Low": "Thấp",
+ "High": "Cao",
+ "Very High": "Rất cao",
+ "None": "Không gì",
+ "Player 1": "Game thủ 1",
+ "Player 2": "Game thủ 2",
+ "Both": "Cả hai",
+ "SAVED STATE TO SLOT": "SAVED STATE TO SLOT",
+ "LOADED STATE FROM SLOT": "LOADED STATE FROM SLOT",
+ "SET SAVE STATE SLOT TO": "SET SAVE STATE SLOT TO",
+ "Network Error": "Mạng bị lỗi",
+ "Submit": "Gửi đi",
+ "Description": "Mô tả",
+ "Code": "Mã",
+ "Add Cheat Code": "Thêm mã gian lận",
+ "Leave Room": "Rời phòng",
+ "Password": "Mật khẩu",
+ "Password (optional)": "Mật khẩu (tùy chọn)",
+ "Max Players": "Người chơi tối đa",
+ "Room Name": "Tên phòng",
+ "Join": "Tham gia",
+ "Player Name": "Tên người chơi",
+ "Set Player Name": "Đặt tên người chơi",
+ "Left Handed Mode": "Chế độ tay trái",
+ "Virtual Gamepad": "Bàn phím ảo",
+ "Disk": "Đĩa",
+ "Press Keyboard": "Bàn phím",
+ "INSERT COIN": "THÊM XU",
+ "Remove": "Loại bỏ",
+ "LOADED STATE FROM BROWSER": "TRẠNG THÁI ĐÃ TẢI TỪ TRÌNH DUYỆT",
+ "SAVED STATE TO BROWSER": "TRẠNG THÁI ĐÃ LƯU VÀO TRÌNH DUYỆT",
+ "Join the discord": "Tham gia thảo luận",
+ "View on GitHub": "Xem trên GitHub",
+ "Failed to start game": "Thất bại khởi động game",
+ "Download Game BIOS": "Tải Game BIOS",
+ "Decompress Game BIOS": "Giải nén Game BIOS",
+ "Download Game Parent": "Tải Game cha",
+ "Decompress Game Parent": "Giải nén Game cha",
+ "Download Game Patch": "Tải vá Game ",
+ "Decompress Game Patch": "Giải nén Game vá",
+ "Download Game State": "Tải trạng thái Game",
+ "Check console": "Kiểm tra log console",
+ "Error for site owner": "Lỗi sở hữu trang chủ",
+ "EmulatorJS": "EmulatorJS",
+ "Clear All": "Xóa hết",
+ "Take Screenshot": "Chụp màn hình",
+ "Quick Save": "Lưu nhanh",
+ "Quick Load": "Nạp nhanh",
+ "REWIND": "REWIND",
+ "Rewind Enabled (requires restart)": "Cho phép quay lui (cần khởi động lại)",
+ "Rewind Granularity": "Rewind Granularity",
+ "Slow Motion Ratio": "Tỷ lệ chuyển động chậm",
+ "Slow Motion": "chuyển động chậm",
+ "Home": "Nhà",
+ "EmulatorJS License": "Giấy phép EmulatorJS",
+ "RetroArch License": "Giấy phép RetroArch ",
+ "SLOW MOTION": "CHUYỂN ĐỘNG CHẬM",
+ "A": "A",
+ "B": "B",
+ "SELECT": "SELECT",
+ "START": "START",
+ "UP": "UP",
+ "DOWN": "DOWN",
+ "LEFT": "LEFT",
+ "RIGHT": "RIGHT",
+ "X": "X",
+ "Y": "Y",
+ "L": "L",
+ "R": "R",
+ "Z": "Z",
+ "STICK UP": "STICK UP",
+ "STICK DOWN": "STICK DOWN",
+ "STICK LEFT": "STICK LEFT",
+ "STICK RIGHT": "STICK RIGHT",
+ "C-PAD UP": "C-PAD UP",
+ "C-PAD DOWN": "C-PAD DOWN",
+ "C-PAD LEFT": "C-PAD LEFT",
+ "C-PAD RIGHT": "C-PAD RIGHT",
+ "MICROPHONE": "MICROPHONE",
+ "BUTTON 1 / START": "BUTTON 1 / START",
+ "BUTTON 2": "BUTTON 2",
+ "BUTTON": "BUTTON",
+ "LEFT D-PAD UP": "LEFT D-PAD UP",
+ "LEFT D-PAD DOWN": "LEFT D-PAD DOWN",
+ "LEFT D-PAD LEFT": "LEFT D-PAD LEFT",
+ "LEFT D-PAD RIGHT": "LEFT D-PAD RIGHT",
+ "RIGHT D-PAD UP": "RIGHT D-PAD UP",
+ "RIGHT D-PAD DOWN": "RIGHT D-PAD DOWN",
+ "RIGHT D-PAD LEFT": "RIGHT D-PAD LEFT",
+ "RIGHT D-PAD RIGHT": "RIGHT D-PAD RIGHT",
+ "C": "C",
+ "MODE": "MODE",
+ "FIRE": "FIRE",
+ "RESET": "RESET",
+ "LEFT DIFFICULTY A": "LEFT DIFFICULTY A",
+ "LEFT DIFFICULTY B": "LEFT DIFFICULTY B",
+ "RIGHT DIFFICULTY A": "RIGHT DIFFICULTY A",
+ "RIGHT DIFFICULTY B": "RIGHT DIFFICULTY B",
+ "COLOR": "COLOR",
+ "B/W": "B/W",
+ "PAUSE": "PAUSE",
+ "OPTION": "OPTION",
+ "OPTION 1": "OPTION 1",
+ "OPTION 2": "OPTION 2",
+ "L2": "L2",
+ "R2": "R2",
+ "L3": "L3",
+ "R3": "R3",
+ "L STICK UP": "L STICK UP",
+ "L STICK DOWN": "L STICK DOWN",
+ "L STICK LEFT": "L STICK LEFT",
+ "L STICK RIGHT": "L STICK RIGHT",
+ "R STICK UP": "R STICK UP",
+ "R STICK DOWN": "R STICK DOWN",
+ "R STICK LEFT": "R STICK LEFT",
+ "R STICK RIGHT": "R STICK RIGHT",
+ "Start": "Start",
+ "Select": "Select",
+ "Fast": "Fast",
+ "Slow": "Slow",
+ "a": "a",
+ "b": "b",
+ "c": "c",
+ "d": "d",
+ "e": "e",
+ "f": "f",
+ "g": "g",
+ "h": "h",
+ "i": "i",
+ "j": "j",
+ "k": "k",
+ "l": "l",
+ "m": "m",
+ "n": "n",
+ "o": "o",
+ "p": "p",
+ "q": "q",
+ "r": "r",
+ "s": "s",
+ "t": "t",
+ "u": "u",
+ "v": "v",
+ "w": "w",
+ "x": "x",
+ "y": "y",
+ "z": "z",
+ "enter": "enter",
+ "escape": "escape",
+ "space": "space",
+ "tab": "tab",
+ "backspace": "backspace",
+ "delete": "delete",
+ "arrowup": "arrowup",
+ "arrowdown": "arrowdown",
+ "arrowleft": "arrowleft",
+ "arrowright": "arrowright",
+ "f1": "f1",
+ "f2": "f2",
+ "f3": "f3",
+ "f4": "f4",
+ "f5": "f5",
+ "f6": "f6",
+ "f7": "f7",
+ "f8": "f8",
+ "f9": "f9",
+ "f10": "f10",
+ "f11": "f11",
+ "f12": "f12",
+ "shift": "shift",
+ "control": "control",
+ "alt": "alt",
+ "meta": "meta",
+ "capslock": "capslock",
+ "insert": "insert",
+ "home": "home",
+ "end": "end",
+ "pageup": "pageup",
+ "pagedown": "pagedown",
+ "!": "!",
+ "@": "@",
+ "#": "#",
+ "$": "$",
+ "%": "%",
+ "^": "^",
+ "&": "&",
+ "*": "*",
+ "(": "(",
+ ")": ")",
+ "-": "-",
+ "_": "_",
+ "+": "+",
+ "=": "=",
+ "[": "[",
+ "]": "]",
+ "{": "{",
+ "}": "}",
+ ";": ";",
+ ":": ":",
+ "'": "'",
+ "\"": "\"",
+ ",": ",",
+ ".": ".",
+ "<": "<",
+ ">": ">",
+ "/": "/",
+ "?": "?",
+ "LEFT_STICK_X": "LEFT_STICK_X",
+ "LEFT_STICK_Y": "LEFT_STICK_Y",
+ "RIGHT_STICK_X": "RIGHT_STICK_X",
+ "RIGHT_STICK_Y": "RIGHT_STICK_Y",
+ "LEFT_TRIGGER": "LEFT_TRIGGER",
+ "RIGHT_TRIGGER": "RIGHT_TRIGGER",
+ "A_BUTTON": "A_BUTTON",
+ "B_BUTTON": "B_BUTTON",
+ "X_BUTTON": "X_BUTTON",
+ "Y_BUTTON": "Y_BUTTON",
+ "START_BUTTON": "START_BUTTON",
+ "SELECT_BUTTON": "SELECT_BUTTON",
+ "L1_BUTTON": "L1_BUTTON",
+ "R1_BUTTON": "R1_BUTTON",
+ "L2_BUTTON": "L2_BUTTON",
+ "R2_BUTTON": "R2_BUTTON",
+ "LEFT_THUMB_BUTTON": "LEFT_THUMB_BUTTON",
+ "RIGHT_THUMB_BUTTON": "RIGHT_THUMB_BUTTON",
+ "DPAD_UP": "DPAD_UP",
+ "DPAD_DOWN": "DPAD_DOWN",
+ "DPAD_LEFT": "DPAD_LEFT",
+ "DPAD_RIGHT": "DPAD_RIGHT",
+ "Autofire": "Autofire"
+}
diff --git a/data/src/consts.js b/data/src/consts.js
index 499d6341e..f660c9fdd 100644
--- a/data/src/consts.js
+++ b/data/src/consts.js
@@ -1,45 +1,45 @@
-export const version = "4.3.0-beta";
-
-export const cores = {
- "atari5200": ["a5200"],
- "vb": ["beetle_vb"],
- "nds": ["melonds", "desmume", "desmume2015"],
- "arcade": ["fbneo", "fbalpha2012_cps1", "fbalpha2012_cps2", "same_cdi"],
- "nes": ["fceumm", "nestopia"],
- "gb": ["gambatte"],
- "coleco": ["gearcoleco"],
- "segaMS": ["smsplus", "genesis_plus_gx", "genesis_plus_gx_wide", "picodrive"],
- "segaMD": ["genesis_plus_gx", "genesis_plus_gx_wide", "picodrive"],
- "segaGG": ["genesis_plus_gx", "genesis_plus_gx_wide"],
- "segaCD": ["genesis_plus_gx", "genesis_plus_gx_wide", "picodrive"],
- "sega32x": ["picodrive"],
- "sega": ["genesis_plus_gx", "genesis_plus_gx_wide", "picodrive"],
- "lynx": ["handy"],
- "mame": ["mame2003_plus", "mame2003"],
- "ngp": ["mednafen_ngp"],
- "pce": ["mednafen_pce"],
- "pcfx": ["mednafen_pcfx"],
- "psx": ["pcsx_rearmed", "mednafen_psx_hw"],
- "ws": ["mednafen_wswan"],
- "gba": ["mgba"],
- "n64": ["mupen64plus_next", "parallel_n64"],
- "3do": ["opera"],
- "psp": ["ppsspp"],
- "atari7800": ["prosystem"],
- "snes": ["snes9x", "bsnes"],
- "atari2600": ["stella2014"],
- "jaguar": ["virtualjaguar"],
- "segaSaturn": ["yabause"],
- "amiga": ["puae"],
- "c64": ["vice_x64sc"],
- "c128": ["vice_x128"],
- "pet": ["vice_xpet"],
- "plus4": ["vice_xplus4"],
- "vic20": ["vice_xvic"],
- "dos": ["dosbox_pure"],
- "intv": ["freeintv"]
-};
-
-export const requiresThreads = ["ppsspp", "dosbox_pure"];
-
-export const requiresWebGL2 = ["ppsspp"];
+export const version = "4.3.0-beta";
+
+export const cores = {
+ "atari5200": ["a5200"],
+ "vb": ["beetle_vb"],
+ "nds": ["melonds", "desmume", "desmume2015"],
+ "arcade": ["fbneo", "fbalpha2012_cps1", "fbalpha2012_cps2", "same_cdi"],
+ "nes": ["fceumm", "nestopia"],
+ "gb": ["gambatte"],
+ "coleco": ["gearcoleco"],
+ "segaMS": ["smsplus", "genesis_plus_gx", "genesis_plus_gx_wide", "picodrive"],
+ "segaMD": ["genesis_plus_gx", "genesis_plus_gx_wide", "picodrive"],
+ "segaGG": ["genesis_plus_gx", "genesis_plus_gx_wide"],
+ "segaCD": ["genesis_plus_gx", "genesis_plus_gx_wide", "picodrive"],
+ "sega32x": ["picodrive"],
+ "sega": ["genesis_plus_gx", "genesis_plus_gx_wide", "picodrive"],
+ "lynx": ["handy"],
+ "mame": ["mame2003_plus", "mame2003"],
+ "ngp": ["mednafen_ngp"],
+ "pce": ["mednafen_pce"],
+ "pcfx": ["mednafen_pcfx"],
+ "psx": ["pcsx_rearmed", "mednafen_psx_hw"],
+ "ws": ["mednafen_wswan"],
+ "gba": ["mgba"],
+ "n64": ["mupen64plus_next", "parallel_n64"],
+ "3do": ["opera"],
+ "psp": ["ppsspp"],
+ "atari7800": ["prosystem"],
+ "snes": ["snes9x", "bsnes"],
+ "atari2600": ["stella2014"],
+ "jaguar": ["virtualjaguar"],
+ "segaSaturn": ["yabause"],
+ "amiga": ["puae"],
+ "c64": ["vice_x64sc"],
+ "c128": ["vice_x128"],
+ "pet": ["vice_xpet"],
+ "plus4": ["vice_xplus4"],
+ "vic20": ["vice_xvic"],
+ "dos": ["dosbox_pure"],
+ "intv": ["freeintv"]
+};
+
+export const requiresThreads = ["ppsspp", "dosbox_pure"];
+
+export const requiresWebGL2 = ["ppsspp"];
diff --git a/data/src/emulator.js b/data/src/emulator.js
index 7f95db066..869a0d422 100644
--- a/data/src/emulator.js
+++ b/data/src/emulator.js
@@ -1,10 +1,16 @@
-import { EJS_Cache, EJS_CacheItem, EJS_FileItem, EJS_Download } from "./cache.js";
+import {
+ EJS_Cache,
+ EJS_CacheItem,
+ EJS_FileItem,
+ EJS_Download,
+} from "./cache.js";
import { EJS_COMPRESSION } from "./compression.js";
import { EJS_GameManager } from "./GameManager.js";
import { GamepadHandler } from "./gamepad.js";
import { EJS_STORAGE, EJS_DUMMYSTORAGE } from "./storage.js";
import { EJS_UTILS } from "./utils.js";
import { EJS_SETUP } from "./setup.js";
+import { netplayMethods } from "./netplay.js";
import { EJS_license } from "./license.js";
import * as CONSTS from "./consts.js";
@@ -37,7 +43,10 @@ class EmulatorJS {
return core;
}
const gen = this.getCore(true);
- if (cores[gen] && cores[gen].includes(this.preGetSetting("retroarch_core"))) {
+ if (
+ cores[gen] &&
+ cores[gen].includes(this.preGetSetting("retroarch_core"))
+ ) {
return this.preGetSetting("retroarch_core");
}
if (cores[core]) {
@@ -53,7 +62,11 @@ class EmulatorJS {
let rv = [];
for (let i = 0; i < listeners.length; i++) {
element.addEventListener(listeners[i], callback);
- const data = { cb: callback, elem: element, listener: listeners[i] };
+ const data = {
+ cb: callback,
+ elem: element,
+ listener: listeners[i],
+ };
rv.push(data);
}
return rv;
@@ -76,10 +89,20 @@ class EmulatorJS {
* @param {boolean} dontCache If true, the downloaded file will not be cached (default is false).
* @returns A promise that resolves with the downloaded file data.
*/
- downloadFile(path, type, progress, notWithPath, opts, forceExtract = false, dontCache = false) {
+ downloadFile(
+ path,
+ type,
+ progress,
+ notWithPath,
+ opts,
+ forceExtract = false,
+ dontCache = false,
+ ) {
+ opts = opts || {};
+ if (!opts.method) opts.method = "GET";
+
if (this.debug) console.log("[EJS " + type + "] Downloading " + path);
return new Promise(async (resolve) => {
- // Handle direct data objects (ArrayBuffer, Uint8Array, Blob)
const data = this.toData(path);
if (data) {
data.then((game) => {
@@ -95,18 +118,31 @@ class EmulatorJS {
// Construct the full path/URL
const basePath = notWithPath ? "" : this.config.dataPath;
let fullPath = basePath + path;
- if (!notWithPath && this.config.filePaths && typeof this.config.filePaths[path.split("/").pop()] === "string") {
+ if (
+ !notWithPath &&
+ this.config.filePaths &&
+ typeof this.config.filePaths[path.split("/").pop()] === "string"
+ ) {
fullPath = this.config.filePaths[path.split("/").pop()];
}
// Delegate all URL downloads (http, https, blob, data, etc.) to EJS_Download
try {
- const onProgress = progress instanceof Function ? (status, percentage, loaded, total) => {
- if (status === "downloading") {
- const progressText = total ? " " + Math.floor(percentage).toString() + "%" : " " + (loaded / 1048576).toFixed(2) + "MB";
- progress(progressText);
- }
- } : null;
+ const onProgress =
+ progress instanceof Function
+ ? (status, percentage, loaded, total) => {
+ if (status === "downloading") {
+ const progressText = total
+ ? " " +
+ Math.floor(percentage).toString() +
+ "%"
+ : " " +
+ (loaded / 1048576).toFixed(2) +
+ "MB";
+ progress(progressText);
+ }
+ }
+ : null;
const onComplete = (success, result) => {
if (!success) {
@@ -130,7 +166,7 @@ class EmulatorJS {
timeout,
responseType,
forceExtract,
- dontCache
+ dontCache,
);
// Handle HEAD requests (returns null)
@@ -147,38 +183,52 @@ class EmulatorJS {
resolve({
data: cacheItem,
headers: {
- "content-length": cacheItem.files.reduce((sum, f) => sum + (f.bytes.byteLength || 0), 0)
- }
+ "content-length": cacheItem.files.reduce(
+ (sum, f) => sum + (f.bytes.byteLength || 0),
+ 0,
+ ),
+ },
});
} else {
let data = cacheItem.files[0].bytes;
-
+
// Convert to appropriate format based on responseType
- if (responseType === "text" || (opts.type && opts.type.toLowerCase() === "text")) {
+ if (
+ responseType === "text" ||
+ (opts.type && opts.type.toLowerCase() === "text")
+ ) {
const decoder = new TextDecoder();
data = decoder.decode(data);
- try { data = JSON.parse(data) } catch(e) {}
+ try {
+ data = JSON.parse(data);
+ } catch (e) {}
}
resolve({
data: data,
headers: {
- "content-length": data.byteLength || data.length
- }
+ "content-length":
+ data.byteLength || data.length,
+ },
});
}
} else {
console.error("Invalid cache item returned:", cacheItem);
resolve(-1);
}
- } catch(error) {
+ } catch (error) {
console.error("Download error:", error);
resolve(-1);
}
});
}
toData(data, rv) {
- if (!(data instanceof ArrayBuffer) && !(data instanceof Uint8Array) && !(data instanceof Blob)) return null;
+ if (
+ !(data instanceof ArrayBuffer) &&
+ !(data instanceof Uint8Array) &&
+ !(data instanceof Blob)
+ )
+ return null;
if (rv) return true;
return new Promise(async (resolve) => {
if (data instanceof ArrayBuffer) {
@@ -189,23 +239,32 @@ class EmulatorJS {
resolve(new Uint8Array(await data.arrayBuffer()));
}
resolve();
- })
+ });
}
checkForUpdates() {
if (this.ejs_version.endsWith("-beta")) {
- console.warn("Using EmulatorJS beta. Not checking for updates. This instance may be out of date. Using stable is highly recommended unless you build and ship your own cores.");
+ console.warn(
+ "Using EmulatorJS beta. Not checking for updates. This instance may be out of date. Using stable is highly recommended unless you build and ship your own cores.",
+ );
return;
}
- fetch("https://cdn.emulatorjs.org/stable/data/version.json").then(response => {
- if (response.ok) {
- response.text().then(body => {
- let version = JSON.parse(body);
- if (this.versionAsInt(this.ejs_version) < this.versionAsInt(version.version)) {
- console.log(`Using EmulatorJS version ${this.ejs_version} but the newest version is ${version.current_version}\nopen https://github.com/EmulatorJS/EmulatorJS to update`);
- }
- })
- }
- })
+ fetch("https://cdn.emulatorjs.org/stable/data/version.json").then(
+ (response) => {
+ if (response.ok) {
+ response.text().then((body) => {
+ let version = JSON.parse(body);
+ if (
+ this.versionAsInt(this.ejs_version) <
+ this.versionAsInt(version.version)
+ ) {
+ console.log(
+ `Using EmulatorJS version ${this.ejs_version} but the newest version is ${version.current_version}\nopen https://github.com/EmulatorJS/EmulatorJS to update`,
+ );
+ }
+ });
+ }
+ },
+ );
}
versionAsInt(ver) {
if (ver.endsWith("-beta")) {
@@ -223,7 +282,11 @@ class EmulatorJS {
this.allSettings = {};
this.initControlVars();
this.debug = config.debug;
- if (this.debug || (window.location && ["localhost", "127.0.0.1"].includes(location.hostname))) {
+ if (
+ this.debug ||
+ (window.location &&
+ ["localhost", "127.0.0.1"].includes(location.hostname))
+ ) {
this.checkForUpdates();
}
this.netplayEnabled = true;
@@ -235,8 +298,10 @@ class EmulatorJS {
this.setup.cacheDefaults();
this.setup.browserMode();
this.setup.shaders();
-
- this.config.buttonOpts = this.buildButtonOptions(this.config.buttonOpts);
+
+ this.config.buttonOpts = this.buildButtonOptions(
+ this.config.buttonOpts,
+ );
this.config.settingsLanguage = window.EJS_settingsLanguage || false;
this.currentPopup = null;
@@ -247,16 +312,20 @@ class EmulatorJS {
this.touch = false;
this.cheats = [];
this.started = false;
- this.volume = (typeof this.config.volume === "number") ? this.config.volume : 0.5;
+ this.volume =
+ typeof this.config.volume === "number" ? this.config.volume : 0.5;
if (this.config.defaultControllers) {
// Merge user config with defaults instead of replacing
- for (const [player, buttons] of Object.entries(this.config.defaultControllers)) {
- this.defaultControllers[player] = this.defaultControllers[player] || {};
+ for (const [player, buttons] of Object.entries(
+ this.config.defaultControllers,
+ )) {
+ this.defaultControllers[player] =
+ this.defaultControllers[player] || {};
for (const [button, config] of Object.entries(buttons)) {
this.defaultControllers[player][button] = {
...(this.defaultControllers[player][button] || {}),
- ...config
+ ...config,
};
}
}
@@ -268,11 +337,23 @@ class EmulatorJS {
this.missingLang = [];
this.setElements(element);
this.setColor(this.config.color || "");
- this.config.alignStartButton = (typeof this.config.alignStartButton === "string") ? this.config.alignStartButton : "bottom";
- this.config.backgroundColor = (typeof this.config.backgroundColor === "string") ? this.config.backgroundColor : "rgb(51, 51, 51)";
+ this.config.alignStartButton =
+ typeof this.config.alignStartButton === "string"
+ ? this.config.alignStartButton
+ : "bottom";
+ this.config.backgroundColor =
+ typeof this.config.backgroundColor === "string"
+ ? this.config.backgroundColor
+ : "rgb(51, 51, 51)";
if (this.config.adUrl) {
- this.config.adSize = (Array.isArray(this.config.adSize)) ? this.config.adSize : ["300px", "250px"];
- this.setupAds(this.config.adUrl, this.config.adSize[0], this.config.adSize[1]);
+ this.config.adSize = Array.isArray(this.config.adSize)
+ ? this.config.adSize
+ : ["300px", "250px"];
+ this.setupAds(
+ this.config.adUrl,
+ this.config.adSize[0],
+ this.config.adSize[1],
+ );
}
this.isMobile = (() => {
// browserMode can be either a 1 (force mobile), 2 (force desktop) or undefined (auto detect)
@@ -284,18 +365,31 @@ class EmulatorJS {
}
let check = false;
- (function (a) { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) check = true; })(navigator.userAgent || navigator.vendor || window.opera);
+ (function (a) {
+ if (
+ /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(
+ a,
+ ) ||
+ /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
+ a.substr(0, 4),
+ )
+ )
+ check = true;
+ })(navigator.userAgent || navigator.vendor || window.opera);
return check;
})();
- this.hasTouchScreen = (function() {
- if (window.PointerEvent && ("maxTouchPoints" in navigator)) {
+ this.hasTouchScreen = (function () {
+ if (window.PointerEvent && "maxTouchPoints" in navigator) {
if (navigator.maxTouchPoints > 0) {
return true;
}
} else {
- if (window.matchMedia && window.matchMedia("(any-pointer:coarse)").matches) {
+ if (
+ window.matchMedia &&
+ window.matchMedia("(any-pointer:coarse)").matches
+ ) {
return true;
- } else if (window.TouchEvent || ("ontouchstart" in window)) {
+ } else if (window.TouchEvent || "ontouchstart" in window) {
return true;
}
}
@@ -303,34 +397,69 @@ class EmulatorJS {
})();
this.canvas = this.createElement("canvas");
this.canvas.classList.add("ejs_canvas");
- this.videoRotation = ([0, 1, 2, 3].includes(this.config.videoRotation)) ? this.config.videoRotation : this.preGetSetting("videoRotation") || 0;
+ this.videoRotation = [0, 1, 2, 3].includes(this.config.videoRotation)
+ ? this.config.videoRotation
+ : this.preGetSetting("videoRotation") || 0;
this.videoRotationChanged = false;
this.capture = this.capture || {};
this.capture.photo = this.capture.photo || {};
- this.capture.photo.source = ["canvas", "retroarch"].includes(this.capture.photo.source) ? this.capture.photo.source : "canvas";
- this.capture.photo.format = (typeof this.capture.photo.format === "string") ? this.capture.photo.format : "png";
- this.capture.photo.upscale = (typeof this.capture.photo.upscale === "number") ? this.capture.photo.upscale : 1;
+ this.capture.photo.source = ["canvas", "retroarch"].includes(
+ this.capture.photo.source,
+ )
+ ? this.capture.photo.source
+ : "canvas";
+ this.capture.photo.format =
+ typeof this.capture.photo.format === "string"
+ ? this.capture.photo.format
+ : "png";
+ this.capture.photo.upscale =
+ typeof this.capture.photo.upscale === "number"
+ ? this.capture.photo.upscale
+ : 1;
this.capture.video = this.capture.video || {};
- this.capture.video.format = (typeof this.capture.video.format === "string") ? this.capture.video.format : "detect";
- this.capture.video.upscale = (typeof this.capture.video.upscale === "number") ? this.capture.video.upscale : 1;
- this.capture.video.fps = (typeof this.capture.video.fps === "number") ? this.capture.video.fps : 30;
- this.capture.video.videoBitrate = (typeof this.capture.video.videoBitrate === "number") ? this.capture.video.videoBitrate : 2.5 * 1024 * 1024;
- this.capture.video.audioBitrate = (typeof this.capture.video.audioBitrate === "number") ? this.capture.video.audioBitrate : 192 * 1024;
+ this.capture.video.format =
+ typeof this.capture.video.format === "string"
+ ? this.capture.video.format
+ : "detect";
+ this.capture.video.upscale =
+ typeof this.capture.video.upscale === "number"
+ ? this.capture.video.upscale
+ : 1;
+ this.capture.video.fps =
+ typeof this.capture.video.fps === "number"
+ ? this.capture.video.fps
+ : 30;
+ this.capture.video.videoBitrate =
+ typeof this.capture.video.videoBitrate === "number"
+ ? this.capture.video.videoBitrate
+ : 2.5 * 1024 * 1024;
+ this.capture.video.audioBitrate =
+ typeof this.capture.video.audioBitrate === "number"
+ ? this.capture.video.audioBitrate
+ : 192 * 1024;
this.bindListeners();
// Additions for Netplay
- this.netplayCanvas = null;
+ this.netplayCanvas = null;
this.netplayShowTurnWarning = false;
this.netplayWarningShown = false;
if (this.netplayEnabled) {
- const iceServers = this.config.netplayICEServers || window.EJS_netplayICEServers || [];
- const hasTurnServer = iceServers.some(server =>
- server && typeof server.urls === 'string' && server.urls.startsWith('turn:')
+ const iceServers =
+ this.config.netplayICEServers ||
+ window.EJS_netplayICEServers ||
+ [];
+ const hasTurnServer = iceServers.some(
+ (server) =>
+ server &&
+ typeof server.urls === "string" &&
+ server.urls.startsWith("turn:"),
);
if (!hasTurnServer) {
this.netplayShowTurnWarning = true;
}
if (this.netplayShowTurnWarning && this.debug) {
- console.warn("WARNING: No TURN addresses are configured! Many clients may fail to connect!");
+ console.warn(
+ "WARNING: No TURN addresses are configured! Many clients may fail to connect!",
+ );
}
}
@@ -341,7 +470,9 @@ class EmulatorJS {
this.fullscreen = false;
this.enableMouseLock = false;
- this.supportsWebgl2 = !!document.createElement("canvas").getContext("webgl2") && (this.config.forceLegacyCores !== true);
+ this.supportsWebgl2 =
+ !!document.createElement("canvas").getContext("webgl2") &&
+ this.config.forceLegacyCores !== true;
this.webgl2Enabled = (() => {
let setting = this.preGetSetting("webgl2Enabled");
if (setting === "disabled" || !this.supportsWebgl2) {
@@ -351,26 +482,28 @@ class EmulatorJS {
}
return null;
})();
- this.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
+ this.isSafari = /^((?!chrome|android).)*safari/i.test(
+ navigator.userAgent,
+ );
+
+ this.storage = {};
- this.storage = {}
-
if (this.config.disableDatabases === true) {
this.config.cacheConfig.enabled = false;
}
-
+
// Populate downloadTypes
this.downloadType = {
- "rom": { "name": "ROM", "dontCache": false },
- "core": { "name": "Core", "dontCache": false },
- "bios": { "name": "BIOS", "dontCache": false },
- "parent": { "name": "Parent", "dontCache": false },
- "patch": { "name": "Patch", "dontCache": false },
- "reports": { "name": "Reports", "dontCache": true },
- "states": { "name": "States", "dontCache": true },
- "support": { "name": "Support", "dontCache": true },
- "unknown": { "name": "Unknown", "dontCache": true }
- }
+ rom: { name: "ROM", dontCache: false },
+ core: { name: "Core", dontCache: false },
+ bios: { name: "BIOS", dontCache: false },
+ parent: { name: "Parent", dontCache: false },
+ patch: { name: "Patch", dontCache: false },
+ reports: { name: "Reports", dontCache: true },
+ states: { name: "States", dontCache: true },
+ support: { name: "Support", dontCache: true },
+ unknown: { name: "Unknown", dontCache: true },
+ };
// Initialize storage cache
this.storageCache = new EJS_Cache(
@@ -378,26 +511,34 @@ class EmulatorJS {
"EmulatorJS-Cache",
this.config.cacheConfig.cacheMaxSizeMB,
this.config.cacheConfig.cacheMaxAgeMins || 7200,
- this.debug
+ this.debug,
);
// Initialize downloader with cache
this.downloader = new EJS_Download(this.storageCache, this);
-
+
// This is not cache. This is save data
this.storage.states = new EJS_STORAGE("EmulatorJS-states", "states");
this.game.classList.add("ejs_game");
if (typeof this.config.backgroundImg === "string") {
this.game.classList.add("ejs_game_background");
- if (this.config.backgroundBlur) this.game.classList.add("ejs_game_background_blur");
- this.game.setAttribute("style", `--ejs-background-image: url("${this.config.backgroundImg}"); --ejs-background-color: ${this.config.backgroundColor};`);
+ if (this.config.backgroundBlur)
+ this.game.classList.add("ejs_game_background_blur");
+ this.game.setAttribute(
+ "style",
+ `--ejs-background-image: url("${this.config.backgroundImg}"); --ejs-background-color: ${this.config.backgroundColor};`,
+ );
this.on("start", () => {
this.game.classList.remove("ejs_game_background");
- if (this.config.backgroundBlur) this.game.classList.remove("ejs_game_background_blur");
- })
+ if (this.config.backgroundBlur)
+ this.game.classList.remove("ejs_game_background_blur");
+ });
} else {
- this.game.setAttribute("style", "--ejs-background-color: " + this.config.backgroundColor + ";");
+ this.game.setAttribute(
+ "style",
+ "--ejs-background-color: " + this.config.backgroundColor + ";",
+ );
}
if (Array.isArray(this.config.cheats)) {
@@ -408,8 +549,8 @@ class EmulatorJS {
desc: cheat[0],
checked: false,
code: cheat[1],
- is_permanent: true
- })
+ is_permanent: true,
+ });
}
}
}
@@ -438,7 +579,7 @@ class EmulatorJS {
setColor(color) {
if (typeof color !== "string") color = "";
- let getColor = function(color) {
+ let getColor = function (color) {
color = color.toLowerCase();
if (color && /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(color)) {
if (color.length === 4) {
@@ -455,16 +596,27 @@ class EmulatorJS {
return rv.join(", ");
}
return null;
- }
+ };
if (!color || getColor(color) === null) {
- this.elements.parent.setAttribute("style", "--ejs-primary-color: 26,175,255;");
+ this.elements.parent.setAttribute(
+ "style",
+ "--ejs-primary-color: 26,175,255;",
+ );
return;
}
- this.elements.parent.setAttribute("style", "--ejs-primary-color:" + getColor(color) + ";");
+ this.elements.parent.setAttribute(
+ "style",
+ "--ejs-primary-color:" + getColor(color) + ";",
+ );
}
setupAds(ads, width, height) {
const div = this.createElement("div");
- const time = (typeof this.config.adMode === "number" && this.config.adMode > -1 && this.config.adMode < 3) ? this.config.adMode : 2;
+ const time =
+ typeof this.config.adMode === "number" &&
+ this.config.adMode > -1 &&
+ this.config.adMode < 3
+ ? this.config.adMode
+ : 2;
div.classList.add("ejs_ad_iframe");
const frame = this.createElement("iframe");
frame.src = ads;
@@ -484,25 +636,28 @@ class EmulatorJS {
}
this.addEventListener(closeButton, "click", () => {
div.remove();
- })
+ });
this.on("start-clicked", () => {
if (this.config.adMode === 0) div.remove();
if (this.config.adMode === 1) {
this.elements.parent.appendChild(div);
}
- })
+ });
this.on("start", () => {
closeParent.removeAttribute("hidden");
- const time = (typeof this.config.adTimer === "number" && this.config.adTimer > 0) ? this.config.adTimer : 10000;
+ const time =
+ typeof this.config.adTimer === "number" &&
+ this.config.adTimer > 0
+ ? this.config.adTimer
+ : 10000;
if (this.config.adTimer === -1) div.remove();
if (this.config.adTimer === 0) return;
setTimeout(() => {
div.remove();
}, time);
- })
-
+ });
}
adBlocked(url, del) {
if (del) {
@@ -510,9 +665,13 @@ class EmulatorJS {
} else {
try {
document.querySelector('div[class="ejs_ad_iframe"]').remove();
- } catch(e) {}
+ } catch (e) {}
this.config.adUrl = url;
- this.setupAds(this.config.adUrl, this.config.adSize[0], this.config.adSize[1]);
+ this.setupAds(
+ this.config.adUrl,
+ this.config.adSize[0],
+ this.config.adSize[1],
+ );
}
}
on(event, func) {
@@ -523,7 +682,7 @@ class EmulatorJS {
callEvent(event, data) {
if (!this.functions) this.functions = {};
if (!Array.isArray(this.functions[event])) return 0;
- this.functions[event].forEach(e => e(data));
+ this.functions[event].forEach((e) => e(data));
return this.functions[event].length;
}
setElements(element) {
@@ -535,8 +694,8 @@ class EmulatorJS {
this.elements = {
main: this.game,
- parent: elem
- }
+ parent: elem,
+ };
this.elements.parent.classList.add("ejs_parent");
this.elements.parent.setAttribute("tabindex", -1);
}
@@ -549,7 +708,10 @@ class EmulatorJS {
button.classList.add("ejs_start_button_border");
border = 1;
}
- button.innerText = (typeof this.config.startBtnName === "string") ? this.config.startBtnName : this.localization("Start Game");
+ button.innerText =
+ typeof this.config.startBtnName === "string"
+ ? this.config.startBtnName
+ : this.localization("Start Game");
if (this.config.alignStartButton == "top") {
button.style.bottom = "calc(100% - 20px)";
} else if (this.config.alignStartButton == "center") {
@@ -558,8 +720,12 @@ class EmulatorJS {
this.elements.parent.appendChild(button);
this.addEventListener(button, "touchstart", () => {
this.touch = true;
- })
- this.addEventListener(button, "click", this.startButtonClicked.bind(this));
+ });
+ this.addEventListener(
+ button,
+ "click",
+ this.startButtonClicked.bind(this),
+ );
if (this.config.startOnLoad === true) {
this.startButtonClicked(button);
}
@@ -585,7 +751,8 @@ class EmulatorJS {
createText() {
this.textElem = this.createElement("div");
this.textElem.classList.add("ejs_loading_text");
- if (typeof this.config.backgroundImg === "string") this.textElem.classList.add("ejs_loading_text_glow");
+ if (typeof this.config.backgroundImg === "string")
+ this.textElem.classList.add("ejs_loading_text_glow");
this.textElem.innerText = this.localization("Loading...");
this.elements.parent.appendChild(this.textElem);
}
@@ -596,17 +763,29 @@ class EmulatorJS {
if (this.config.langJson) {
if (typeof log === "undefined") log = true;
if (!this.config.langJson[text] && log) {
- if (!this.missingLang.includes(text)) this.missingLang.push(text);
- if (this.debug) console.log(`Translation not found for '${text}'. Language set to '${this.config.language}'`);
+ if (!this.missingLang.includes(text))
+ this.missingLang.push(text);
+ if (this.debug)
+ console.log(
+ `Translation not found for '${text}'. Language set to '${this.config.language}'`,
+ );
}
return this.config.langJson[text] || text;
}
return text;
}
checkCoreCompatibility(version) {
- if (this.versionAsInt(version.minimumEJSVersion) > this.versionAsInt(this.ejs_version)) {
- this.startGameError(this.localization("Outdated EmulatorJS version"));
- throw new Error("Core requires minimum EmulatorJS version of " + version.minimumEJSVersion);
+ if (
+ this.versionAsInt(version.minimumEJSVersion) >
+ this.versionAsInt(this.ejs_version)
+ ) {
+ this.startGameError(
+ this.localization("Outdated EmulatorJS version"),
+ );
+ throw new Error(
+ "Core requires minimum EmulatorJS version of " +
+ version.minimumEJSVersion,
+ );
}
}
startGameError(message) {
@@ -624,27 +803,44 @@ class EmulatorJS {
downloadGameCore() {
this.textElem.innerText = this.localization("Download Game Core");
if (!this.config.threads && this.requiresThreads(this.getCore())) {
- this.startGameError(this.localization("Error for site owner") + "\n" + this.localization("Check console"));
- console.warn("This core requires threads, but EJS_threads is not set!");
+ this.startGameError(
+ this.localization("Error for site owner") +
+ "\n" +
+ this.localization("Check console"),
+ );
+ console.warn(
+ "This core requires threads, but EJS_threads is not set!",
+ );
return;
}
if (!this.supportsWebgl2 && this.requiresWebGL2(this.getCore())) {
this.startGameError(this.localization("Outdated graphics driver"));
return;
}
- if (this.config.threads && typeof window.SharedArrayBuffer !== "function") {
- this.startGameError(this.localization("Error for site owner") + "\n" + this.localization("Check console"));
- console.warn("Threads is set to true, but the SharedArrayBuffer function is not exposed. Threads requires 2 headers to be set when sending you html page. See https://stackoverflow.com/a/68630724");
+ if (
+ this.config.threads &&
+ typeof window.SharedArrayBuffer !== "function"
+ ) {
+ this.startGameError(
+ this.localization("Error for site owner") +
+ "\n" +
+ this.localization("Check console"),
+ );
+ console.warn(
+ "Threads is set to true, but the SharedArrayBuffer function is not exposed. Threads requires 2 headers to be set when sending you html page. See https://stackoverflow.com/a/68630724",
+ );
return;
}
const gotCore = (data) => {
this.defaultCoreOpts = {};
-
+
let decompressedData = {};
-
+
// Check if data is already a cache item with extracted files
if (data && data.files && Array.isArray(data.files)) {
- console.log("[EJS Core] Data is already decompressed cache item");
+ console.log(
+ "[EJS Core] Data is already decompressed cache item",
+ );
// Convert cache item files array to object keyed by filename
for (const file of data.files) {
decompressedData[file.filename] = file.bytes;
@@ -656,19 +852,33 @@ class EmulatorJS {
if (!this.compression) {
this.compression = new EJS_COMPRESSION(this);
}
-
- this.textElem.innerText = this.localization("Decompress Game Core");
-
- this.compression.decompress(new Uint8Array(data), (m, appendMsg) => {
- this.textElem.innerText = appendMsg ? (this.localization("Decompress Game Core") + m) : m;
- }, null).then(async (decompressedData) => {
- this.processCore(decompressedData);
- });
+
+ this.textElem.innerText = this.localization(
+ "Decompress Game Core",
+ );
+
+ this.compression
+ .decompress(
+ new Uint8Array(data),
+ (m, appendMsg) => {
+ this.textElem.innerText = appendMsg
+ ? this.localization("Decompress Game Core") + m
+ : m;
+ },
+ null,
+ )
+ .then(async (decompressedData) => {
+ this.processCore(decompressedData);
+ });
}
- }
-
+ };
+
this.processCore = (decompressedData) => {
- if (this.debug) console.log("[EJS Core] Decompressed files:", Object.keys(decompressedData));
+ if (this.debug)
+ console.log(
+ "[EJS Core] Decompressed files:",
+ Object.keys(decompressedData),
+ );
let js, thread, wasm;
for (let k in decompressedData) {
if (k.endsWith(".wasm")) {
@@ -678,9 +888,15 @@ class EmulatorJS {
} else if (k.endsWith(".js")) {
js = decompressedData[k];
} else if (k === "build.json") {
- this.checkCoreCompatibility(JSON.parse(new TextDecoder().decode(decompressedData[k])));
+ this.checkCoreCompatibility(
+ JSON.parse(
+ new TextDecoder().decode(decompressedData[k]),
+ ),
+ );
} else if (k === "core.json") {
- let core = JSON.parse(new TextDecoder().decode(decompressedData[k]));
+ let core = JSON.parse(
+ new TextDecoder().decode(decompressedData[k]),
+ );
this.extensions = core.extensions;
this.coreName = core.name;
this.repository = core.repo;
@@ -689,7 +905,9 @@ class EmulatorJS {
this.retroarchOpts = core.retroarchOpts;
this.saveFileExt = core.save;
} else if (k === "license.txt") {
- this.license = new TextDecoder().decode(decompressedData[k]);
+ this.license = new TextDecoder().decode(
+ decompressedData[k],
+ );
}
}
@@ -698,11 +916,20 @@ class EmulatorJS {
this.elements.bottomBar.loadSavFiles[0].style.display = "none";
}
- if (this.debug) console.log("[EJS Core] Core decompression complete");
- if (this.debug) console.log("[EJS Core] js size:", js?.byteLength, "wasm size:", wasm?.byteLength, "thread size:", thread?.byteLength);
+ if (this.debug)
+ console.log("[EJS Core] Core decompression complete");
+ if (this.debug)
+ console.log(
+ "[EJS Core] js size:",
+ js?.byteLength,
+ "wasm size:",
+ wasm?.byteLength,
+ "thread size:",
+ thread?.byteLength,
+ );
this.initGameCore(js, wasm, thread);
- }
+ };
const report = "cores/reports/" + this.getCore() + ".json";
// Add cache-busting parameter periodically to ensure we get updated build versions
@@ -711,18 +938,36 @@ class EmulatorJS {
const cacheBustParam = Math.floor(Date.now() / cacheBustInterval);
const reportUrl = `${report}?v=${cacheBustParam}`;
- this.downloadFile(reportUrl, this.downloadType.reports.name, null, false, { responseType: "text", method: "GET" }, false, this.downloadType.reports.dontCache).then(async rep => {
- if (rep === -1 || typeof rep === "string" || typeof rep.data === "string") {
+ this.downloadFile(
+ reportUrl,
+ this.downloadType.reports.name,
+ null,
+ false,
+ { responseType: "text", method: "GET" },
+ false,
+ this.downloadType.reports.dontCache,
+ ).then(async (rep) => {
+ if (
+ rep === -1 ||
+ typeof rep === "string" ||
+ typeof rep.data === "string"
+ ) {
rep = {};
} else {
rep = rep.data;
}
if (!rep.buildStart) {
- console.warn("Could not fetch core report JSON at " + reportUrl + "! Core caching will be disabled!");
+ console.warn(
+ "Could not fetch core report JSON at " +
+ reportUrl +
+ "! Core caching will be disabled!",
+ );
rep.buildStart = Math.random() * 100;
}
if (this.webgl2Enabled === null) {
- this.webgl2Enabled = rep.options ? rep.options.defaultWebGL2 : false;
+ this.webgl2Enabled = rep.options
+ ? rep.options.defaultWebGL2
+ : false;
}
if (this.requiresWebGL2(this.getCore())) {
this.webgl2Enabled = true;
@@ -731,37 +976,75 @@ class EmulatorJS {
if (typeof window.SharedArrayBuffer === "function") {
const opt = this.preGetSetting("ejs_threads");
if (opt) {
- threads = (opt === "enabled");
+ threads = opt === "enabled";
} else {
threads = this.config.threads;
}
}
- let legacy = (this.supportsWebgl2 && this.webgl2Enabled ? "" : "-legacy");
- let filename = this.getCore() + (threads ? "-thread" : "") + legacy + "-wasm.data";
+ let legacy =
+ this.supportsWebgl2 && this.webgl2Enabled ? "" : "-legacy";
+ let filename =
+ this.getCore() +
+ (threads ? "-thread" : "") +
+ legacy +
+ "-wasm.data";
// Download the core
console.log("[EJS Core] Downloading core:", filename);
const corePath = "cores/" + filename;
- let res = await this.downloadFile(corePath, this.downloadType.core.name, (progress) => {
- this.textElem.innerText = this.localization("Download Game Core") + progress;
- }, false, { responseType: "arraybuffer", method: "GET" }, true, this.downloadType.core.dontCache);
+ let res = await this.downloadFile(
+ corePath,
+ this.downloadType.core.name,
+ (progress) => {
+ this.textElem.innerText =
+ this.localization("Download Game Core") + progress;
+ },
+ false,
+ { responseType: "arraybuffer", method: "GET" },
+ true,
+ this.downloadType.core.dontCache,
+ );
if (res === -1) {
- console.log("File not found, attemping to fetch from emulatorjs cdn.");
- console.error("**THIS METHOD IS A FAILSAFE, AND NOT OFFICIALLY SUPPORTED. USE AT YOUR OWN RISK**");
- let version = this.ejs_version.endsWith("-beta") ? "nightly" : this.ejs_version;
- res = await this.downloadFile(`https://cdn.emulatorjs.org/${version}/data/${corePath}`, this.downloadType.core.name, (progress) => {
- this.textElem.innerText = this.localization("Download Game Core") + progress;
- }, true, { responseType: "arraybuffer", method: "GET" }, true, this.downloadType.core.dontCache);
+ console.log(
+ "File not found, attemping to fetch from emulatorjs cdn.",
+ );
+ console.error(
+ "**THIS METHOD IS A FAILSAFE, AND NOT OFFICIALLY SUPPORTED. USE AT YOUR OWN RISK**",
+ );
+ let version = this.ejs_version.endsWith("-beta")
+ ? "nightly"
+ : this.ejs_version;
+ res = await this.downloadFile(
+ `https://cdn.emulatorjs.org/${version}/data/${corePath}`,
+ this.downloadType.core.name,
+ (progress) => {
+ this.textElem.innerText =
+ this.localization("Download Game Core") + progress;
+ },
+ true,
+ { responseType: "arraybuffer", method: "GET" },
+ true,
+ this.downloadType.core.dontCache,
+ );
if (res === -1) {
if (!this.supportsWebgl2) {
- this.startGameError(this.localization("Outdated graphics driver"));
+ this.startGameError(
+ this.localization("Outdated graphics driver"),
+ );
} else {
- this.startGameError(this.localization("Error downloading core") + " (" + filename + ")");
+ this.startGameError(
+ this.localization("Error downloading core") +
+ " (" +
+ filename +
+ ")",
+ );
}
return;
}
- console.warn("File was not found locally, but was found on the emulatorjs cdn.\nIt is recommended to download the stable release from here: https://cdn.emulatorjs.org/releases/");
+ console.warn(
+ "File was not found locally, but was found on the emulatorjs cdn.\nIt is recommended to download the stable release from here: https://cdn.emulatorjs.org/releases/",
+ );
}
// Core download and caching handled by EJS_Download
@@ -770,7 +1053,9 @@ class EmulatorJS {
}
initGameCore(js, wasm, thread) {
let script = this.createElement("script");
- script.src = URL.createObjectURL(new Blob([js], { type: "application/javascript" }));
+ script.src = URL.createObjectURL(
+ new Blob([js], { type: "application/javascript" }),
+ );
script.addEventListener("load", () => {
this.initModule(wasm, thread);
});
@@ -779,12 +1064,22 @@ class EmulatorJS {
getBaseFileName(force) {
//Only once game and core is loaded
if (!this.started && !force) return null;
- if (force && this.config.gameUrl !== "game" && !this.config.gameUrl.startsWith("blob:")) {
- return this.config.gameUrl.split("/").pop().split("#")[0].split("?")[0];
+ if (
+ force &&
+ this.config.gameUrl !== "game" &&
+ !this.config.gameUrl.startsWith("blob:")
+ ) {
+ return this.config.gameUrl
+ .split("/")
+ .pop()
+ .split("#")[0]
+ .split("?")[0];
}
if (typeof this.config.gameName === "string") {
- const invalidCharacters = /[#<$+%>!`&*'|{}/\\?"=@:^\r\n]/ig;
- const name = this.config.gameName.replace(invalidCharacters, "").trim();
+ const invalidCharacters = /[#<$+%>!`&*'|{}/\\?"=@:^\r\n]/gi;
+ const name = this.config.gameName
+ .replace(invalidCharacters, "")
+ .trim();
if (name) return name;
}
if (!this.fileName) return "game";
@@ -793,7 +1088,11 @@ class EmulatorJS {
return parts.join(".");
}
saveInBrowserSupported() {
- return !!window.indexedDB && (typeof this.config.gameName === "string" || !this.config.gameUrl.startsWith("blob:"));
+ return (
+ !!window.indexedDB &&
+ (typeof this.config.gameName === "string" ||
+ !this.config.gameUrl.startsWith("blob:"))
+ );
}
displayMessage(message, time) {
if (!this.msgElem) {
@@ -803,45 +1102,67 @@ class EmulatorJS {
this.elements.parent.appendChild(this.msgElem);
}
clearTimeout(this.msgTimeout);
- this.msgTimeout = setTimeout(() => {
- this.msgElem.innerText = "";
- }, (typeof time === "number" && time > 0) ? time : 3000)
+ this.msgTimeout = setTimeout(
+ () => {
+ this.msgElem.innerText = "";
+ },
+ typeof time === "number" && time > 0 ? time : 3000,
+ );
this.msgElem.innerText = message;
}
downloadStartState() {
return new Promise((resolve, reject) => {
- if (typeof this.config.loadState !== "string" && !this.toData(this.config.loadState, true)) {
+ if (
+ typeof this.config.loadState !== "string" &&
+ !this.toData(this.config.loadState, true)
+ ) {
resolve();
return;
}
this.textElem.innerText = this.localization("Download Game State");
- this.downloadFile(this.config.loadState, this.downloadType.states.name, (progress) => {
- this.textElem.innerText = this.localization("Download Game State") + progress;
- }, true, { responseType: "arraybuffer", method: "GET" }, false, this.downloadType.states.dontCache).then((res) => {
+ this.downloadFile(
+ this.config.loadState,
+ this.downloadType.states.name,
+ (progress) => {
+ this.textElem.innerText =
+ this.localization("Download Game State") + progress;
+ },
+ true,
+ { responseType: "arraybuffer", method: "GET" },
+ false,
+ this.downloadType.states.dontCache,
+ ).then((res) => {
if (res === -1) {
- this.startGameError(this.localization("Error downloading game state"));
+ this.startGameError(
+ this.localization("Error downloading game state"),
+ );
return;
}
this.on("start", () => {
setTimeout(() => {
this.gameManager.loadState(new Uint8Array(res.data));
}, 10);
- })
+ });
resolve();
});
- })
+ });
}
/**
* Download a file, with caching and File object support
* @param {*} url The URL or File object to download
* @param {*} type The download type (from this.downloadType)
- * @returns
+ * @returns
*/
download(url, type) {
if (url === undefined || url === null || url === "") {
- if (this.debug) console.log("[EJS " + type.name.toUpperCase() + "] No URL provided, skipping download.");
+ if (this.debug)
+ console.log(
+ "[EJS " +
+ type.name.toUpperCase() +
+ "] No URL provided, skipping download.",
+ );
return new Promise((resolve) => {
resolve(url);
});
@@ -856,7 +1177,13 @@ class EmulatorJS {
// check if url is a file object, and if so convert it to an EJS_CacheItem
if (typeof url === "object" && url instanceof File) {
- if (this.debug) console.log("[EJS " + type.name.toUpperCase() + "] Requested download for File object " + url.name);
+ if (this.debug)
+ console.log(
+ "[EJS " +
+ type.name.toUpperCase() +
+ "] Requested download for File object " +
+ url.name,
+ );
// Convert File to Uint8Array
const arrayBuffer = await url.arrayBuffer();
@@ -866,34 +1193,53 @@ class EmulatorJS {
let key = this.storageCache.generateCacheKey(inData);
let cachedItem = await this.storageCache.get(key);
if (cachedItem) {
- if (this.debug) console.log("[EJS " + type.name.toUpperCase() + "] Using cached content for " + url.name);
+ if (this.debug)
+ console.log(
+ "[EJS " +
+ type.name.toUpperCase() +
+ "] Using cached content for " +
+ url.name,
+ );
returnData = cachedItem;
} else {
// Not in cache - decompress
let files = [];
- const decompressedData = await this.compression.decompress(inData, (m, appendMsg) => {
- this.textElem.innerText = appendMsg ? (this.localization("Decompress Game Core") + m) : m;
- }, (fileName, fileData) => {
- // Use file callback to collect files during decompression
- let bytes;
- if (fileData instanceof Uint8Array) {
- bytes = fileData;
- } else if (fileData instanceof ArrayBuffer) {
- bytes = new Uint8Array(fileData);
- } else if (fileData && typeof fileData === 'object') {
- // Handle case where it might be an object with numeric keys
- bytes = new Uint8Array(Object.values(fileData));
- } else {
- console.error("Unknown file data type:", typeof fileData, fileData);
- return;
- }
+ const decompressedData = await this.compression.decompress(
+ inData,
+ (m, appendMsg) => {
+ this.textElem.innerText = appendMsg
+ ? this.localization("Decompress Game Core") + m
+ : m;
+ },
+ (fileName, fileData) => {
+ // Use file callback to collect files during decompression
+ let bytes;
+ if (fileData instanceof Uint8Array) {
+ bytes = fileData;
+ } else if (fileData instanceof ArrayBuffer) {
+ bytes = new Uint8Array(fileData);
+ } else if (
+ fileData &&
+ typeof fileData === "object"
+ ) {
+ // Handle case where it might be an object with numeric keys
+ bytes = new Uint8Array(Object.values(fileData));
+ } else {
+ console.error(
+ "Unknown file data type:",
+ typeof fileData,
+ fileData,
+ );
+ return;
+ }
- if (fileName === "!!notCompressedData") {
- files.push(new EJS_FileItem(url.name, bytes));
- } else if (!fileName.endsWith("/")) {
- files.push(new EJS_FileItem(fileName, bytes));
- }
- });
+ if (fileName === "!!notCompressedData") {
+ files.push(new EJS_FileItem(url.name, bytes));
+ } else if (!fileName.endsWith("/")) {
+ files.push(new EJS_FileItem(fileName, bytes));
+ }
+ },
+ );
// construct EJS_CacheItem
let data = new EJS_CacheItem(
@@ -904,7 +1250,7 @@ class EmulatorJS {
"arraybuffer",
url.name,
url.name,
- Date.now() + 5 * 24 * 60 * 60 * 1000 // 5 days expiration
+ Date.now() + 5 * 24 * 60 * 60 * 1000, // 5 days expiration
);
this.storageCache.put(data);
@@ -913,18 +1259,25 @@ class EmulatorJS {
}
} else {
// download using a url
- if (this.debug) console.log("[EJS " + type.name.toUpperCase() + "] Requested download for " + url);
+ if (this.debug)
+ console.log(
+ "[EJS " +
+ type.name.toUpperCase() +
+ "] Requested download for " +
+ url,
+ );
// download the content
const data = await this.downloadFile(
url,
type.name,
(progress) => {
- this.textElem.innerText = this.localization("Download Game Data") + progress;
+ this.textElem.innerText =
+ this.localization("Download Game Data") + progress;
},
true,
{ responseType: "arraybuffer", method: "GET" },
false,
- type.dontCache
+ type.dontCache,
);
// check for error
if (data === -1) {
@@ -941,7 +1294,11 @@ class EmulatorJS {
returnData = data.data;
}
- if (this.debug) console.log("[EJS " + type.name.toUpperCase() + "] Downloaded content:", returnData);
+ if (this.debug)
+ console.log(
+ "[EJS " + type.name.toUpperCase() + "] Downloaded content:",
+ returnData,
+ );
const writeFilesToFS = (fileName, fileData) => {
if (fileName.includes("/")) {
@@ -966,7 +1323,10 @@ class EmulatorJS {
// extract to the file system
if (returnData && returnData.files) {
for (let i = 0; i < returnData.files.length; i++) {
- writeFilesToFS(returnData.files[i].filename, returnData.files[i].bytes)
+ writeFilesToFS(
+ returnData.files[i].filename,
+ returnData.files[i].bytes,
+ );
}
}
@@ -990,10 +1350,25 @@ class EmulatorJS {
* Determine CUE file handling settings based on core type and configuration
*/
determineCueSettings() {
- const coresThatNeedCueHandling = ["pcsx_rearmed", "genesis_plus_gx", "picodrive", "mednafen_pce", "smsplus", "vice_x64", "vice_x64sc", "vice_x128", "vice_xvic", "vice_xpet", "puae"];
+ const coresThatNeedCueHandling = [
+ "pcsx_rearmed",
+ "genesis_plus_gx",
+ "picodrive",
+ "mednafen_pce",
+ "smsplus",
+ "vice_x64",
+ "vice_x64sc",
+ "vice_x128",
+ "vice_xvic",
+ "vice_xpet",
+ "puae",
+ ];
let disableCue = false;
- if (coresThatNeedCueHandling.includes(this.getCore()) && this.config.disableCue === undefined) {
+ if (
+ coresThatNeedCueHandling.includes(this.getCore()) &&
+ this.config.disableCue === undefined
+ ) {
disableCue = true;
} else {
disableCue = this.config.disableCue;
@@ -1027,16 +1402,22 @@ class EmulatorJS {
let supportedFile = null;
let cueFile = null;
- fileNames.forEach(fileName => {
+ fileNames.forEach((fileName) => {
const ext = fileName.split(".").pop().toLowerCase();
if (supportedFile === null && this.supportsExtension(ext)) {
supportedFile = fileName;
}
- if (isoFile === null && ["iso", "cso", "chd", "elf"].includes(ext)) {
+ if (
+ isoFile === null &&
+ ["iso", "cso", "chd", "elf"].includes(ext)
+ ) {
isoFile = fileName;
}
if (prioritizeExtensions.includes(ext)) {
- const currentCueExt = (cueFile === null) ? null : cueFile.split(".").pop().toLowerCase();
+ const currentCueExt =
+ cueFile === null
+ ? null
+ : cueFile.split(".").pop().toLowerCase();
if (coreName === "psx") {
// Always prefer m3u files for psx cores
if (currentCueExt !== "m3u") {
@@ -1045,7 +1426,7 @@ class EmulatorJS {
}
}
} else {
- const priority = ["cue", "ccd"]
+ const priority = ["cue", "ccd"];
// Prefer cue or ccd files over toc or m3u
if (!priority.includes(currentCueExt)) {
if (cueFile === null || priority.includes(ext)) {
@@ -1064,14 +1445,24 @@ class EmulatorJS {
}
// ISO files take priority if supported
- if (isoFile !== null && this.supportsExtension(isoFile.split(".").pop().toLowerCase())) {
+ if (
+ isoFile !== null &&
+ this.supportsExtension(isoFile.split(".").pop().toLowerCase())
+ ) {
this.fileName = isoFile;
}
// CUE/CCD files take priority if supported, or create a CUE file if needed
- if (cueFile !== null && this.supportsExtension(cueFile.split(".").pop().toLowerCase())) {
+ if (
+ cueFile !== null &&
+ this.supportsExtension(cueFile.split(".").pop().toLowerCase())
+ ) {
this.fileName = cueFile;
- } else if (createCueFile && this.supportsExtension("m3u") && this.supportsExtension("cue")) {
+ } else if (
+ createCueFile &&
+ this.supportsExtension("m3u") &&
+ this.supportsExtension("cue")
+ ) {
this.fileName = this.gameManager.createCueFile(fileNames);
}
@@ -1102,12 +1493,21 @@ class EmulatorJS {
downloadFiles() {
(async () => {
await this.initializeGameManager();
-
- const romData = await this.download(this.config.gameUrl, this.downloadType.rom);
+
+ const romData = await this.download(
+ this.config.gameUrl,
+ this.downloadType.rom,
+ );
await this.download(this.config.biosUrl, this.downloadType.bios);
await this.downloadStartState();
- await this.download(this.config.gameParentUrl, this.downloadType.parent);
- await this.download(this.config.gamePatchUrl, this.downloadType.patch);
+ await this.download(
+ this.config.gameParentUrl,
+ this.downloadType.parent,
+ );
+ await this.download(
+ this.config.gamePatchUrl,
+ this.downloadType.patch,
+ );
this.determineCueSettings();
this.startGameFromDownload(romData);
@@ -1116,50 +1516,61 @@ class EmulatorJS {
initModule(wasmData, threadData) {
if (typeof window.EJS_Runtime !== "function") {
console.warn("EJS_Runtime is not defined!");
- this.startGameError(this.localization("Error loading EmulatorJS runtime"));
+ this.startGameError(
+ this.localization("Error loading EmulatorJS runtime"),
+ );
throw new Error("EJS_Runtime is not defined!");
}
- window.EJS_Runtime({
- noInitialRun: true,
- onRuntimeInitialized: null,
- arguments: [],
- preRun: [],
- postRun: [],
- canvas: this.canvas,
- callbacks: {},
- parent: this.elements.parent,
- print: (msg) => {
- if (this.debug) {
- console.log(msg);
- }
- },
- printErr: (msg) => {
- if (this.debug) {
- console.log(msg);
- }
- },
- totalDependencies: 0,
- locateFile: function (fileName) {
- if (this.debug) console.log(fileName);
- if (fileName.endsWith(".wasm")) {
- return URL.createObjectURL(new Blob([wasmData], { type: "application/wasm" }));
- } else if (fileName.endsWith(".worker.js")) {
- return URL.createObjectURL(new Blob([threadData], { type: "application/javascript" }));
- }
- },
- getSavExt: () => {
- if (this.saveFileExt) {
- return "." + this.saveFileExt;
- }
- return ".srm";
- }
- }).then(module => {
- this.Module = module;
- this.downloadFiles();
- }).catch(e => {
- console.warn(e);
- this.startGameError(this.localization("Failed to start game"));
- });
+ window
+ .EJS_Runtime({
+ noInitialRun: true,
+ onRuntimeInitialized: null,
+ arguments: [],
+ preRun: [],
+ postRun: [],
+ canvas: this.canvas,
+ callbacks: {},
+ parent: this.elements.parent,
+ print: (msg) => {
+ if (this.debug) {
+ console.log(msg);
+ }
+ },
+ printErr: (msg) => {
+ if (this.debug) {
+ console.log(msg);
+ }
+ },
+ totalDependencies: 0,
+ locateFile: function (fileName) {
+ if (this.debug) console.log(fileName);
+ if (fileName.endsWith(".wasm")) {
+ return URL.createObjectURL(
+ new Blob([wasmData], { type: "application/wasm" }),
+ );
+ } else if (fileName.endsWith(".worker.js")) {
+ return URL.createObjectURL(
+ new Blob([threadData], {
+ type: "application/javascript",
+ }),
+ );
+ }
+ },
+ getSavExt: () => {
+ if (this.saveFileExt) {
+ return "." + this.saveFileExt;
+ }
+ return ".srm";
+ },
+ })
+ .then((module) => {
+ this.Module = module;
+ this.downloadFiles();
+ })
+ .catch((e) => {
+ console.warn(e);
+ this.startGameError(this.localization("Failed to start game"));
+ });
}
startGame() {
try {
@@ -1168,7 +1579,10 @@ class EmulatorJS {
args.push("/" + this.fileName);
if (this.debug) console.log(args);
this.Module.callMain(args);
- if (typeof this.config.softLoad === "number" && this.config.softLoad > 0) {
+ if (
+ typeof this.config.softLoad === "number" &&
+ this.config.softLoad > 0
+ ) {
this.resetTimeout = setTimeout(() => {
this.gameManager.restart();
}, this.config.softLoad * 1000);
@@ -1201,8 +1615,9 @@ class EmulatorJS {
if (this.config.fullscreenOnLoad) {
try {
this.toggleFullscreen(true);
- } catch(e) {
- if (this.debug) console.warn("Could not fullscreen on load");
+ } catch (e) {
+ if (this.debug)
+ console.warn("Could not fullscreen on load");
}
}
this.menu.open();
@@ -1216,7 +1631,7 @@ class EmulatorJS {
console.log("File system directory");
this.gameManager.listDir("/");
}
- } catch(e) {
+ } catch (e) {
console.warn("Failed to start game", e);
this.startGameError(this.localization("Failed to start game"));
this.callEvent("exit");
@@ -1226,19 +1641,21 @@ class EmulatorJS {
}
checkStarted() {
(async () => {
- let sleep = (ms) => new Promise(r => setTimeout(r, ms));
+ let sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let state = "suspended";
let popup;
while (state === "suspended") {
if (!this.Module.AL) return;
- this.Module.AL.currentCtx.sources.forEach(ctx => {
+ this.Module.AL.currentCtx.sources.forEach((ctx) => {
state = ctx.gain.context.state;
});
if (state !== "suspended") break;
if (!popup) {
popup = this.createPopup("", {});
const button = this.createElement("button");
- button.innerText = this.localization("Click to resume Emulator");
+ button.innerText = this.localization(
+ "Click to resume Emulator",
+ );
button.classList.add("ejs_menu_button");
button.style.width = "25%";
button.style.height = "25%";
@@ -1255,35 +1672,54 @@ class EmulatorJS {
this.createContextMenu();
this.createBottomMenuBar();
this.createControlSettingMenu();
- this.createCheatsMenu()
+ this.createCheatsMenu();
this.createNetplayMenu();
this.setVirtualGamepad();
- this.addEventListener(this.elements.parent, "keydown keyup", this.keyChange.bind(this));
- this.addEventListener(this.elements.parent, "mousedown touchstart", (e) => {
- if (document.activeElement !== this.elements.parent && this.config.noAutoFocus !== true) this.elements.parent.focus();
- })
+ this.addEventListener(
+ this.elements.parent,
+ "keydown keyup",
+ this.keyChange.bind(this),
+ );
+ this.addEventListener(
+ this.elements.parent,
+ "mousedown touchstart",
+ (e) => {
+ if (
+ document.activeElement !== this.elements.parent &&
+ this.config.noAutoFocus !== true
+ )
+ this.elements.parent.focus();
+ },
+ );
this.addEventListener(window, "resize", this.handleResize.bind(this));
this.addEventListener(window, "blur", () => this.stopAllAutofire());
let counter = 0;
this.elements.statePopupPanel = this.createPopup("", {}, true);
- this.elements.statePopupPanel.innerText = this.localization("Drop save state here to load");
+ this.elements.statePopupPanel.innerText = this.localization(
+ "Drop save state here to load",
+ );
this.elements.statePopupPanel.style["text-align"] = "center";
this.elements.statePopupPanel.style["font-size"] = "28px";
//to fix a funny apple bug
- this.addEventListener(window, "webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange", () => {
- setTimeout(() => {
- this.handleResize.bind(this);
- if (this.config.noAutoFocus !== true) this.elements.parent.focus();
- }, 0);
- });
+ this.addEventListener(
+ window,
+ "webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange",
+ () => {
+ setTimeout(() => {
+ this.handleResize.bind(this);
+ if (this.config.noAutoFocus !== true)
+ this.elements.parent.focus();
+ }, 0);
+ },
+ );
this.addEventListener(window, "beforeunload", (e) => {
if (this.config.disableAutoUnload) {
e.preventDefault();
e.returnValue = "";
- return
- }
+ return;
+ }
if (!this.started) return;
this.callEvent("exit");
});
@@ -1301,7 +1737,8 @@ class EmulatorJS {
if (!this.started) return;
counter--;
if (counter === 0) {
- this.elements.statePopupPanel.parentElement.style.display = "none";
+ this.elements.statePopupPanel.parentElement.style.display =
+ "none";
}
});
this.addEventListener(this.elements.parent, "dragend", (e) => {
@@ -1325,9 +1762,9 @@ class EmulatorJS {
}
if (!file) return;
const fileHandle = file.getAsFile();
- fileHandle.arrayBuffer().then(data => {
+ fileHandle.arrayBuffer().then((data) => {
this.gameManager.loadState(new Uint8Array(data));
- })
+ });
});
this.gamepad = new GamepadHandler(); //https://github.com/ethanaobrien/Gamepad
@@ -1335,22 +1772,30 @@ class EmulatorJS {
if (!this.gamepadLabels) return;
for (let i = 0; i < this.gamepadSelection.length; i++) {
if (this.gamepadSelection[i] === "") {
- this.gamepadSelection[i] = this.gamepad.gamepads[e.gamepadIndex].id + "_" + this.gamepad.gamepads[e.gamepadIndex].index;
+ this.gamepadSelection[i] =
+ this.gamepad.gamepads[e.gamepadIndex].id +
+ "_" +
+ this.gamepad.gamepads[e.gamepadIndex].index;
break;
}
}
this.updateGamepadLabels();
- })
+ });
this.gamepad.on("disconnected", (e) => {
- const gamepadIndex = this.gamepad.gamepads.indexOf(this.gamepad.gamepads.find(f => f.index == e.gamepadIndex));
- const gamepadSelection = this.gamepad.gamepads[gamepadIndex].id + "_" + this.gamepad.gamepads[gamepadIndex].index;
+ const gamepadIndex = this.gamepad.gamepads.indexOf(
+ this.gamepad.gamepads.find((f) => f.index == e.gamepadIndex),
+ );
+ const gamepadSelection =
+ this.gamepad.gamepads[gamepadIndex].id +
+ "_" +
+ this.gamepad.gamepads[gamepadIndex].index;
for (let i = 0; i < this.gamepadSelection.length; i++) {
if (this.gamepadSelection[i] === gamepadSelection) {
this.gamepadSelection[i] = "";
}
}
setTimeout(this.updateGamepadLabels.bind(this), 10);
- })
+ });
this.gamepad.on("axischanged", this.gamepadEvent.bind(this));
this.gamepad.on("buttondown", this.gamepadEvent.bind(this));
this.gamepad.on("buttonup", this.gamepadEvent.bind(this));
@@ -1363,24 +1808,37 @@ class EmulatorJS {
this.elements.contextMenu.save.style.display = "none";
this.elements.contextMenu.load.style.display = "none";
}
- if (typeof this.config.gameId !== "number" || !this.config.netplayUrl || this.netplayEnabled === false) {
+ if (
+ typeof this.config.gameId !== "number" ||
+ !this.config.netplayUrl ||
+ this.netplayEnabled === false
+ ) {
this.elements.bottomBar.netplay[0].style.display = "none";
}
}
updateGamepadLabels() {
for (let i = 0; i < this.gamepadLabels.length; i++) {
- this.gamepadLabels[i].innerHTML = ""
+ this.gamepadLabels[i].innerHTML = "";
const def = this.createElement("option");
def.setAttribute("value", "notconnected");
def.innerText = "Not Connected";
this.gamepadLabels[i].appendChild(def);
for (let j = 0; j < this.gamepad.gamepads.length; j++) {
const opt = this.createElement("option");
- opt.setAttribute("value", this.gamepad.gamepads[j].id + "_" + this.gamepad.gamepads[j].index);
- opt.innerText = this.gamepad.gamepads[j].id + "_" + this.gamepad.gamepads[j].index;
+ opt.setAttribute(
+ "value",
+ this.gamepad.gamepads[j].id +
+ "_" +
+ this.gamepad.gamepads[j].index,
+ );
+ opt.innerText =
+ this.gamepad.gamepads[j].id +
+ "_" +
+ this.gamepad.gamepads[j].index;
this.gamepadLabels[i].appendChild(opt);
}
- this.gamepadLabels[i].value = this.gamepadSelection[i] || "notconnected";
+ this.gamepadLabels[i].value =
+ this.gamepadSelection[i] || "notconnected";
}
}
createLink(elem, link, text, useP) {
@@ -1401,126 +1859,126 @@ class EmulatorJS {
playPause: {
visible: true,
icon: "play",
- displayName: "Play/Pause"
+ displayName: "Play/Pause",
},
play: {
visible: true,
icon: '',
- displayName: "Play"
+ displayName: "Play",
},
pause: {
visible: true,
icon: '',
- displayName: "Pause"
+ displayName: "Pause",
},
restart: {
visible: true,
icon: '',
- displayName: "Restart"
+ displayName: "Restart",
},
mute: {
visible: true,
icon: '',
- displayName: "Mute"
+ displayName: "Mute",
},
unmute: {
visible: true,
icon: '',
- displayName: "Unmute"
+ displayName: "Unmute",
},
settings: {
visible: true,
icon: '',
- displayName: "Settings"
+ displayName: "Settings",
},
fullscreen: {
visible: true,
icon: "fullscreen",
- displayName: "Fullscreen"
+ displayName: "Fullscreen",
},
enterFullscreen: {
visible: true,
icon: '',
- displayName: "Enter Fullscreen"
+ displayName: "Enter Fullscreen",
},
exitFullscreen: {
visible: true,
icon: '',
- displayName: "Exit Fullscreen"
+ displayName: "Exit Fullscreen",
},
saveState: {
visible: true,
icon: '',
- displayName: "Save State"
+ displayName: "Save State",
},
loadState: {
visible: true,
icon: '',
- displayName: "Load State"
+ displayName: "Load State",
},
screenRecord: {
- visible: true
+ visible: true,
},
gamepad: {
visible: true,
icon: '',
- displayName: "Control Settings"
+ displayName: "Control Settings",
},
cheat: {
visible: true,
icon: '',
- displayName: "Cheats"
+ displayName: "Cheats",
},
volumeSlider: {
- visible: true
+ visible: true,
},
saveSavFiles: {
visible: true,
icon: '',
- displayName: "Export Save File"
+ displayName: "Export Save File",
},
loadSavFiles: {
visible: true,
icon: '',
- displayName: "Import Save File"
+ displayName: "Import Save File",
},
quickSave: {
- visible: true
+ visible: true,
},
quickLoad: {
- visible: true
+ visible: true,
},
screenshot: {
- visible: true
+ visible: true,
},
cacheManager: {
visible: true,
icon: '',
- displayName: "Cache Manager"
+ displayName: "Cache Manager",
},
exitEmulation: {
visible: true,
icon: '',
- displayName: "Exit Emulation"
+ displayName: "Exit Emulation",
},
netplay: {
visible: true,
icon: '',
- displayName: "Netplay"
+ displayName: "Netplay",
},
diskButton: {
visible: true,
icon: '',
- displayName: "Disks"
+ displayName: "Disks",
},
contextMenu: {
visible: true,
icon: '',
- displayName: "Context Menu"
- }
+ displayName: "Context Menu",
+ },
};
this.defaultButtonAliases = {
- volume: "volumeSlider"
+ volume: "volumeSlider",
};
let mergedButtonOptions = this.defaultButtonOptions;
@@ -1541,67 +1999,94 @@ class EmulatorJS {
if (!mergedButtonOptions[searchKey]) {
// If the button does not exist in the default buttons, create a custom button
// Custom buttons must have a displayName, icon, and callback property
- if (!buttonUserOpts[searchKey] || !buttonUserOpts[searchKey].displayName || !buttonUserOpts[searchKey].icon || !buttonUserOpts[searchKey].callback) {
- if (this.debug) console.warn(`Custom button "${searchKey}" is missing required properties`);
+ if (
+ !buttonUserOpts[searchKey] ||
+ !buttonUserOpts[searchKey].displayName ||
+ !buttonUserOpts[searchKey].icon ||
+ !buttonUserOpts[searchKey].callback
+ ) {
+ if (this.debug)
+ console.warn(
+ `Custom button "${searchKey}" is missing required properties`,
+ );
continue;
}
mergedButtonOptions[searchKey] = {
visible: true,
- displayName: buttonUserOpts[searchKey].displayName || searchKey,
+ displayName:
+ buttonUserOpts[searchKey].displayName || searchKey,
icon: buttonUserOpts[searchKey].icon || "",
- callback: buttonUserOpts[searchKey].callback || (() => { }),
- custom: true
+ callback:
+ buttonUserOpts[searchKey].callback || (() => {}),
+ custom: true,
};
}
// if the value is a boolean, set the visible property to the value
if (typeof buttonUserOpts[searchKey] === "boolean") {
- mergedButtonOptions[searchKey].visible = buttonUserOpts[searchKey];
+ mergedButtonOptions[searchKey].visible =
+ buttonUserOpts[searchKey];
} else if (typeof buttonUserOpts[searchKey] === "object") {
// If the value is an object, merge it with the default button properties
-
+
// if the button is the contextMenu, only allow the visible property to be set
if (searchKey === "contextMenu") {
- mergedButtonOptions[searchKey].visible = buttonUserOpts[searchKey].visible !== undefined ? buttonUserOpts[searchKey].visible : true;
+ mergedButtonOptions[searchKey].visible =
+ buttonUserOpts[searchKey].visible !== undefined
+ ? buttonUserOpts[searchKey].visible
+ : true;
} else if (this.defaultButtonOptions[searchKey]) {
// copy properties from the button definition if they aren't null
for (const prop in buttonUserOpts[searchKey]) {
if (buttonUserOpts[searchKey][prop] !== null) {
- mergedButtonOptions[searchKey][prop] = buttonUserOpts[searchKey][prop];
+ mergedButtonOptions[searchKey][prop] =
+ buttonUserOpts[searchKey][prop];
}
}
} else {
// button was not in the default buttons list and is therefore a custom button
// verify that the value has a displayName, icon, and callback property
- if (buttonUserOpts[searchKey].displayName && buttonUserOpts[searchKey].icon && buttonUserOpts[searchKey].callback) {
+ if (
+ buttonUserOpts[searchKey].displayName &&
+ buttonUserOpts[searchKey].icon &&
+ buttonUserOpts[searchKey].callback
+ ) {
mergedButtonOptions[searchKey] = {
visible: true,
- displayName: buttonUserOpts[searchKey].displayName,
+ displayName:
+ buttonUserOpts[searchKey].displayName,
icon: buttonUserOpts[searchKey].icon,
callback: buttonUserOpts[searchKey].callback,
- custom: true
+ custom: true,
};
} else if (this.debug) {
- console.warn(`Custom button "${searchKey}" is missing required properties`);
+ console.warn(
+ `Custom button "${searchKey}" is missing required properties`,
+ );
}
}
}
-
+
// behaviour exceptions
switch (searchKey) {
case "playPause":
- mergedButtonOptions.play.visible = mergedButtonOptions.playPause.visible;
- mergedButtonOptions.pause.visible = mergedButtonOptions.playPause.visible;
+ mergedButtonOptions.play.visible =
+ mergedButtonOptions.playPause.visible;
+ mergedButtonOptions.pause.visible =
+ mergedButtonOptions.playPause.visible;
break;
-
+
case "mute":
- mergedButtonOptions.unmute.visible = mergedButtonOptions.mute.visible;
+ mergedButtonOptions.unmute.visible =
+ mergedButtonOptions.mute.visible;
break;
-
+
case "fullscreen":
- mergedButtonOptions.enterFullscreen.visible = mergedButtonOptions.fullscreen.visible;
- mergedButtonOptions.exitFullscreen.visible = mergedButtonOptions.fullscreen.visible;
+ mergedButtonOptions.enterFullscreen.visible =
+ mergedButtonOptions.fullscreen.visible;
+ mergedButtonOptions.exitFullscreen.visible =
+ mergedButtonOptions.fullscreen.visible;
break;
}
}
@@ -1614,20 +2099,31 @@ class EmulatorJS {
this.elements.contextmenu.classList.add("ejs_context_menu");
this.addEventListener(this.game, "contextmenu", (e) => {
e.preventDefault();
- if ((this.config.buttonOpts && this.config.buttonOpts.rightClick === false) || !this.started) return;
+ if (
+ (this.config.buttonOpts &&
+ this.config.buttonOpts.rightClick === false) ||
+ !this.started
+ )
+ return;
const parentRect = this.elements.parent.getBoundingClientRect();
this.elements.contextmenu.style.display = "block";
const rect = this.elements.contextmenu.getBoundingClientRect();
const up = e.offsetY + rect.height > parentRect.height - 25;
const left = e.offsetX + rect.width > parentRect.width - 5;
- this.elements.contextmenu.style.left = (e.offsetX - (left ? rect.width : 0)) + "px";
- this.elements.contextmenu.style.top = (e.offsetY - (up ? rect.height : 0)) + "px";
- })
+ this.elements.contextmenu.style.left =
+ e.offsetX - (left ? rect.width : 0) + "px";
+ this.elements.contextmenu.style.top =
+ e.offsetY - (up ? rect.height : 0) + "px";
+ });
const hideMenu = () => {
this.elements.contextmenu.style.display = "none";
- }
- this.addEventListener(this.elements.contextmenu, "contextmenu", (e) => e.preventDefault());
- this.addEventListener(this.elements.parent, "contextmenu", (e) => e.preventDefault());
+ };
+ this.addEventListener(this.elements.contextmenu, "contextmenu", (e) =>
+ e.preventDefault(),
+ );
+ this.addEventListener(this.elements.parent, "contextmenu", (e) =>
+ e.preventDefault(),
+ );
this.addEventListener(this.game, "mousedown touchend", hideMenu);
const parent = this.createElement("ul");
const addButton = (title, hidden, functi0n) => {
@@ -1648,12 +2144,19 @@ class EmulatorJS {
parent.appendChild(li);
hideMenu();
return li;
- }
+ };
let screenshotUrl;
const screenshot = addButton("Take Screenshot", false, () => {
if (screenshotUrl) URL.revokeObjectURL(screenshotUrl);
const date = new Date();
- const fileName = this.getBaseFileName() + "-" + date.getMonth() + "-" + date.getDate() + "-" + date.getFullYear();
+ const fileName =
+ this.getBaseFileName() +
+ "-" +
+ date.getMonth() +
+ "-" +
+ date.getDate() +
+ "-" +
+ date.getFullYear();
this.screenshot((blob, format) => {
screenshotUrl = URL.createObjectURL(blob);
const a = this.createElement("a");
@@ -1665,38 +2168,54 @@ class EmulatorJS {
});
let screenMediaRecorder = null;
- const startScreenRecording = addButton("Start Screen Recording", false, () => {
- if (screenMediaRecorder !== null) {
- screenMediaRecorder.stop();
- }
- screenMediaRecorder = this.screenRecord();
- startScreenRecording.setAttribute("hidden", "hidden");
- stopScreenRecording.removeAttribute("hidden");
- hideMenu();
- });
- const stopScreenRecording = addButton("Stop Screen Recording", true, () => {
- if (screenMediaRecorder !== null) {
- screenMediaRecorder.stop();
- screenMediaRecorder = null;
- }
- startScreenRecording.removeAttribute("hidden");
- stopScreenRecording.setAttribute("hidden", "hidden");
- hideMenu();
- });
+ const startScreenRecording = addButton(
+ "Start Screen Recording",
+ false,
+ () => {
+ if (screenMediaRecorder !== null) {
+ screenMediaRecorder.stop();
+ }
+ screenMediaRecorder = this.screenRecord();
+ startScreenRecording.setAttribute("hidden", "hidden");
+ stopScreenRecording.removeAttribute("hidden");
+ hideMenu();
+ },
+ );
+ const stopScreenRecording = addButton(
+ "Stop Screen Recording",
+ true,
+ () => {
+ if (screenMediaRecorder !== null) {
+ screenMediaRecorder.stop();
+ screenMediaRecorder = null;
+ }
+ startScreenRecording.removeAttribute("hidden");
+ stopScreenRecording.setAttribute("hidden", "hidden");
+ hideMenu();
+ },
+ );
const qSave = addButton("Quick Save", false, () => {
- const slot = this.getSettingValue("save-state-slot") ? this.getSettingValue("save-state-slot") : "1";
+ const slot = this.getSettingValue("save-state-slot")
+ ? this.getSettingValue("save-state-slot")
+ : "1";
if (this.gameManager.quickSave(slot)) {
- this.displayMessage(this.localization("SAVED STATE TO SLOT") + " " + slot);
+ this.displayMessage(
+ this.localization("SAVED STATE TO SLOT") + " " + slot,
+ );
} else {
this.displayMessage(this.localization("FAILED TO SAVE STATE"));
}
hideMenu();
});
const qLoad = addButton("Quick Load", false, () => {
- const slot = this.getSettingValue("save-state-slot") ? this.getSettingValue("save-state-slot") : "1";
+ const slot = this.getSettingValue("save-state-slot")
+ ? this.getSettingValue("save-state-slot")
+ : "1";
this.gameManager.quickLoad(slot);
- this.displayMessage(this.localization("LOADED STATE FROM SLOT") + " " + slot);
+ this.displayMessage(
+ this.localization("LOADED STATE FROM SLOT") + " " + slot,
+ );
hideMenu();
});
this.elements.contextMenu = {
@@ -1704,14 +2223,14 @@ class EmulatorJS {
startScreenRecording: startScreenRecording,
stopScreenRecording: stopScreenRecording,
save: qSave,
- load: qLoad
- }
+ load: qLoad,
+ };
addButton("EmulatorJS v" + this.ejs_version, false, () => {
hideMenu();
const body = this.createPopup("EmulatorJS", {
- "Close": () => {
+ Close: () => {
this.closePopup();
- }
+ },
});
body.style.display = "flex";
@@ -1737,7 +2256,7 @@ class EmulatorJS {
parent.appendChild(li);
hideMenu();
return li;
- }
+ };
//body.style["padding-left"] = "20%";
const home = this.createElement("div");
const license = this.createElement("div");
@@ -1760,16 +2279,30 @@ class EmulatorJS {
retroarch.classList.add("ejs_context_menu_tab");
coreLicense.classList.add("ejs_context_menu_tab");
- this.createLink(home, "https://github.com/EmulatorJS/EmulatorJS", "View on GitHub", true);
+ this.createLink(
+ home,
+ "https://github.com/EmulatorJS/EmulatorJS",
+ "View on GitHub",
+ true,
+ );
- this.createLink(home, "https://discord.gg/6akryGkETU", "Join the discord", true);
+ this.createLink(
+ home,
+ "https://discord.gg/6akryGkETU",
+ "Join the discord",
+ true,
+ );
const info = this.createElement("div");
this.createLink(info, "https://emulatorjs.org", "EmulatorJS");
// I do not like using innerHTML, though this should be "safe"
info.innerHTML += " is powered by ";
- this.createLink(info, "https://github.com/libretro/RetroArch/", "RetroArch");
+ this.createLink(
+ info,
+ "https://github.com/libretro/RetroArch/",
+ "RetroArch",
+ );
if (this.repository && this.coreName) {
info.innerHTML += ". This core is powered by ";
this.createLink(info, this.repository, this.coreName);
@@ -1779,7 +2312,6 @@ class EmulatorJS {
}
home.appendChild(info);
-
home.appendChild(this.createElement("br"));
menu.appendChild(parent);
let current = home;
@@ -1788,14 +2320,16 @@ class EmulatorJS {
if (current) {
current.style.display = "none";
}
- let activeLi = li.parentElement.querySelector(".ejs_active_list_element");
+ let activeLi = li.parentElement.querySelector(
+ ".ejs_active_list_element",
+ );
if (activeLi) {
activeLi.classList.remove("ejs_active_list_element");
}
li.classList.add("ejs_active_list_element");
current = element;
element.style.display = "";
- }
+ };
addButton("Home", false, (li) => {
setElem(home, li);
}).classList.add("ejs_active_list_element");
@@ -1808,12 +2342,13 @@ class EmulatorJS {
if (this.coreName && this.license) {
addButton(this.coreName + " License", false, (li) => {
setElem(coreLicense, li);
- })
+ });
coreLicense.innerText = this.license;
}
//Todo - Contributors.
- retroarch.innerText = this.localization("This project is powered by") + " ";
+ retroarch.innerText =
+ this.localization("This project is powered by") + " ";
const a = this.createElement("a");
a.href = "https://github.com/libretro/RetroArch";
a.target = "_blank";
@@ -1821,8 +2356,11 @@ class EmulatorJS {
retroarch.appendChild(a);
const licenseLink = this.createElement("a");
licenseLink.target = "_blank";
- licenseLink.href = "https://github.com/libretro/RetroArch/blob/master/COPYING";
- licenseLink.innerText = this.localization("View the RetroArch license here");
+ licenseLink.href =
+ "https://github.com/libretro/RetroArch/blob/master/COPYING";
+ licenseLink.innerText = this.localization(
+ "View the RetroArch license here",
+ );
a.appendChild(this.createElement("br"));
a.appendChild(licenseLink);
@@ -1830,10 +2368,14 @@ class EmulatorJS {
});
if (this.config.buttonOpts) {
- if (this.config.buttonOpts.screenshot.visible === false) screenshot.setAttribute("hidden", "");
- if (this.config.buttonOpts.screenRecord.visible === false) startScreenRecording.setAttribute("hidden", "");
- if (this.config.buttonOpts.quickSave.visible === false) qSave.setAttribute("hidden", "");
- if (this.config.buttonOpts.quickLoad.visible === false) qLoad.setAttribute("hidden", "");
+ if (this.config.buttonOpts.screenshot.visible === false)
+ screenshot.setAttribute("hidden", "");
+ if (this.config.buttonOpts.screenRecord.visible === false)
+ startScreenRecording.setAttribute("hidden", "");
+ if (this.config.buttonOpts.quickSave.visible === false)
+ qSave.setAttribute("hidden", "");
+ if (this.config.buttonOpts.quickLoad.visible === false)
+ qLoad.setAttribute("hidden", "");
}
this.elements.contextmenu.appendChild(parent);
@@ -1844,7 +2386,7 @@ class EmulatorJS {
if (this.currentPopup !== null) {
try {
this.currentPopup.remove();
- } catch(e) {}
+ } catch (e) {}
this.currentPopup = null;
}
}
@@ -1892,12 +2434,17 @@ class EmulatorJS {
file.type = "file";
this.addEventListener(file, "change", (e) => {
resolve(e.target.files[0]);
- })
+ });
file.click();
- })
+ });
}
isPopupOpen() {
- return this.cheatMenu.style.display !== "none" || this.netplayMenu.style.display !== "none" || this.controlMenu.style.display !== "none" || this.currentPopup !== null;
+ return (
+ this.cheatMenu.style.display !== "none" ||
+ this.netplayMenu.style.display !== "none" ||
+ this.controlMenu.style.display !== "none" ||
+ this.currentPopup !== null
+ );
}
isChild(first, second) {
if (!first || !second) return false;
@@ -1909,7 +2456,10 @@ class EmulatorJS {
return adown.contains(second);
}
- return first.compareDocumentPosition && first.compareDocumentPosition(second) & 16;
+ return (
+ first.compareDocumentPosition &&
+ first.compareDocumentPosition(second) & 16
+ );
}
createBottomMenuBar() {
this.elements.menu = this.createElement("div");
@@ -1918,22 +2468,23 @@ class EmulatorJS {
this.elements.menu.style.opacity = 0;
this.on("start", (e) => {
this.elements.menu.style.opacity = "";
- })
+ });
this.elements.menu.classList.add("ejs_menu_bar");
this.elements.menu.classList.add("ejs_menu_bar_hidden");
let timeout = null;
let ignoreEvents = false;
const hide = () => {
- if (this.paused || this.settingsMenuOpen || this.disksMenuOpen) return;
+ if (this.paused || this.settingsMenuOpen || this.disksMenuOpen)
+ return;
this.elements.menu.classList.add("ejs_menu_bar_hidden");
- }
+ };
const show = () => {
clearTimeout(timeout);
timeout = setTimeout(hide, 3000);
this.elements.menu.classList.remove("ejs_menu_bar_hidden");
- }
+ };
this.menu = {
close: () => {
@@ -1949,22 +2500,34 @@ class EmulatorJS {
toggle: () => {
if (!this.started) return;
clearTimeout(timeout);
- if (this.elements.menu.classList.contains("ejs_menu_bar_hidden")) {
+ if (
+ this.elements.menu.classList.contains("ejs_menu_bar_hidden")
+ ) {
timeout = setTimeout(hide, 3000);
}
this.elements.menu.classList.toggle("ejs_menu_bar_hidden");
- }
- }
+ },
+ };
this.createBottomMenuBarListeners = () => {
const clickListener = (e) => {
if (e.pointerType === "touch") return;
- if (!this.started || ignoreEvents || document.pointerLockElement === this.canvas) return;
+ if (
+ !this.started ||
+ ignoreEvents ||
+ document.pointerLockElement === this.canvas
+ )
+ return;
if (this.isPopupOpen()) return;
show();
- }
+ };
const mouseListener = (e) => {
- if (!this.started || ignoreEvents || document.pointerLockElement === this.canvas) return;
+ if (
+ !this.started ||
+ ignoreEvents ||
+ document.pointerLockElement === this.canvas
+ )
+ return;
if (this.isPopupOpen()) return;
const deltaX = e.movementX;
const deltaY = e.movementY;
@@ -1979,41 +2542,72 @@ class EmulatorJS {
if (angle < 0) angle += 360;
if (angle < 85 || angle > 95) return;
show();
- }
- if (this.menu.mousemoveListener) this.removeEventListener(this.menu.mousemoveListener);
-
- if ((this.preGetSetting("menubarBehavior") || "downward") === "downward") {
- this.menu.mousemoveListener = this.addEventListener(this.elements.parent, "mousemove", mouseListener);
+ };
+ if (this.menu.mousemoveListener)
+ this.removeEventListener(this.menu.mousemoveListener);
+
+ if (
+ (this.preGetSetting("menubarBehavior") || "downward") ===
+ "downward"
+ ) {
+ this.menu.mousemoveListener = this.addEventListener(
+ this.elements.parent,
+ "mousemove",
+ mouseListener,
+ );
} else {
- this.menu.mousemoveListener = this.addEventListener(this.elements.parent, "mousemove", clickListener);
+ this.menu.mousemoveListener = this.addEventListener(
+ this.elements.parent,
+ "mousemove",
+ clickListener,
+ );
}
this.addEventListener(this.elements.parent, "click", clickListener);
- }
+ };
this.createBottomMenuBarListeners();
this.elements.parent.appendChild(this.elements.menu);
let tmout;
- this.addEventListener(this.elements.parent, "mousedown touchstart", (e) => {
- if (this.isChild(this.elements.menu, e.target) || this.isChild(this.elements.menuToggle, e.target)) return;
- if (!this.started || this.elements.menu.classList.contains("ejs_menu_bar_hidden") || this.isPopupOpen()) return;
- const width = this.elements.parent.getBoundingClientRect().width;
- if (width > 575) return;
- clearTimeout(tmout);
- tmout = setTimeout(() => {
- ignoreEvents = false;
- }, 2000)
- ignoreEvents = true;
- this.menu.close();
- })
+ this.addEventListener(
+ this.elements.parent,
+ "mousedown touchstart",
+ (e) => {
+ if (
+ this.isChild(this.elements.menu, e.target) ||
+ this.isChild(this.elements.menuToggle, e.target)
+ )
+ return;
+ if (
+ !this.started ||
+ this.elements.menu.classList.contains(
+ "ejs_menu_bar_hidden",
+ ) ||
+ this.isPopupOpen()
+ )
+ return;
+ const width =
+ this.elements.parent.getBoundingClientRect().width;
+ if (width > 575) return;
+ clearTimeout(tmout);
+ tmout = setTimeout(() => {
+ ignoreEvents = false;
+ }, 2000);
+ ignoreEvents = true;
+ this.menu.close();
+ },
+ );
let paddingSet = false;
//Now add buttons
const addButton = (buttonConfig, callback, element, both) => {
const button = this.createElement("button");
button.type = "button";
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ const svg = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "svg",
+ );
svg.setAttribute("role", "presentation");
svg.setAttribute("focusable", "false");
svg.innerHTML = buttonConfig.icon;
@@ -2038,8 +2632,8 @@ class EmulatorJS {
this.addEventListener(button, "click", buttonConfig.callback);
}
return both ? [button, svg, text] : button;
- }
-
+ };
+
const restartButton = addButton(this.config.buttonOpts.restart, () => {
if (this.isNetplay && this.netplay.owner) {
this.gameManager.restart();
@@ -2089,57 +2683,84 @@ class EmulatorJS {
this.canvas.mozExitPointerLock();
}
}
- }
+ };
this.play = (dontUpdate) => {
if (this.paused) this.togglePlaying(dontUpdate);
- }
+ };
this.pause = (dontUpdate) => {
if (!this.paused) this.togglePlaying(dontUpdate);
- }
+ };
let stateUrl;
- const saveState = addButton(this.config.buttonOpts.saveState, async () => {
- let state;
- try {
- state = this.gameManager.getState();
- } catch(e) {
- this.displayMessage(this.localization("FAILED TO SAVE STATE"));
- return;
- }
- const { screenshot, format } = await this.takeScreenshot(this.capture.photo.source, this.capture.photo.format, this.capture.photo.upscale);
- const called = this.callEvent("saveState", {
- screenshot: screenshot,
- format: format,
- state: state
- });
- if (called > 0) return;
- if (stateUrl) URL.revokeObjectURL(stateUrl);
- if (this.getSettingValue("save-state-location") === "browser" && this.saveInBrowserSupported()) {
- this.storage.states.put(this.getBaseFileName() + ".state", state);
- this.displayMessage(this.localization("SAVED STATE TO BROWSER"));
- } else {
- const blob = new Blob([state]);
- stateUrl = URL.createObjectURL(blob);
- const a = this.createElement("a");
- a.href = stateUrl;
- a.download = this.getBaseFileName() + ".state";
- a.click();
- }
- });
- const loadState = addButton(this.config.buttonOpts.loadState, async () => {
- const called = this.callEvent("loadState");
- if (called > 0) return;
- if (this.getSettingValue("save-state-location") === "browser" && this.saveInBrowserSupported()) {
- this.storage.states.get(this.getBaseFileName() + ".state").then(e => {
- this.gameManager.loadState(e);
- this.displayMessage(this.localization("LOADED STATE FROM BROWSER"));
- })
- } else {
- const file = await this.selectFile();
- const state = new Uint8Array(await file.arrayBuffer());
- this.gameManager.loadState(state);
- }
- });
+ const saveState = addButton(
+ this.config.buttonOpts.saveState,
+ async () => {
+ let state;
+ try {
+ state = this.gameManager.getState();
+ } catch (e) {
+ this.displayMessage(
+ this.localization("FAILED TO SAVE STATE"),
+ );
+ return;
+ }
+ const { screenshot, format } = await this.takeScreenshot(
+ this.capture.photo.source,
+ this.capture.photo.format,
+ this.capture.photo.upscale,
+ );
+ const called = this.callEvent("saveState", {
+ screenshot: screenshot,
+ format: format,
+ state: state,
+ });
+ if (called > 0) return;
+ if (stateUrl) URL.revokeObjectURL(stateUrl);
+ if (
+ this.getSettingValue("save-state-location") === "browser" &&
+ this.saveInBrowserSupported()
+ ) {
+ this.storage.states.put(
+ this.getBaseFileName() + ".state",
+ state,
+ );
+ this.displayMessage(
+ this.localization("SAVED STATE TO BROWSER"),
+ );
+ } else {
+ const blob = new Blob([state]);
+ stateUrl = URL.createObjectURL(blob);
+ const a = this.createElement("a");
+ a.href = stateUrl;
+ a.download = this.getBaseFileName() + ".state";
+ a.click();
+ }
+ },
+ );
+ const loadState = addButton(
+ this.config.buttonOpts.loadState,
+ async () => {
+ const called = this.callEvent("loadState");
+ if (called > 0) return;
+ if (
+ this.getSettingValue("save-state-location") === "browser" &&
+ this.saveInBrowserSupported()
+ ) {
+ this.storage.states
+ .get(this.getBaseFileName() + ".state")
+ .then((e) => {
+ this.gameManager.loadState(e);
+ this.displayMessage(
+ this.localization("LOADED STATE FROM BROWSER"),
+ );
+ });
+ } else {
+ const file = await this.selectFile();
+ const state = new Uint8Array(await file.arrayBuffer());
+ this.gameManager.loadState(state);
+ }
+ },
+ );
const controlMenu = addButton(this.config.buttonOpts.gamepad, () => {
this.controlMenu.style.display = "";
});
@@ -2151,47 +2772,63 @@ class EmulatorJS {
this.openCacheMenu();
});
- if (this.config.cacheConfig.enabled === false) cache.style.display = "none";
+ if (this.config.cacheConfig.enabled === false)
+ cache.style.display = "none";
let savUrl;
- const saveSavFiles = addButton(this.config.buttonOpts.saveSavFiles, async () => {
- const file = await this.gameManager.getSaveFile();
- const { screenshot, format } = await this.takeScreenshot(this.capture.photo.source, this.capture.photo.format, this.capture.photo.upscale);
- const called = this.callEvent("saveSave", {
- screenshot: screenshot,
- format: format,
- save: file
- });
- if (called > 0) return;
- const blob = new Blob([file]);
- savUrl = URL.createObjectURL(blob);
- const a = this.createElement("a");
- a.href = savUrl;
- a.download = this.gameManager.getSaveFilePath().split("/").pop();
- a.click();
- });
- const loadSavFiles = addButton(this.config.buttonOpts.loadSavFiles, async () => {
- const called = this.callEvent("loadSave");
- if (called > 0) return;
- const file = await this.selectFile();
- const sav = new Uint8Array(await file.arrayBuffer());
- const path = this.gameManager.getSaveFilePath();
- const paths = path.split("/");
- let cp = "";
- for (let i = 0; i < paths.length - 1; i++) {
- if (paths[i] === "") continue;
- cp += "/" + paths[i];
- if (!this.gameManager.FS.analyzePath(cp).exists) this.gameManager.FS.mkdir(cp);
- }
- if (this.gameManager.FS.analyzePath(path).exists) this.gameManager.FS.unlink(path);
- this.gameManager.FS.writeFile(path, sav);
- this.gameManager.loadSaveFiles();
- });
+ const saveSavFiles = addButton(
+ this.config.buttonOpts.saveSavFiles,
+ async () => {
+ const file = await this.gameManager.getSaveFile();
+ const { screenshot, format } = await this.takeScreenshot(
+ this.capture.photo.source,
+ this.capture.photo.format,
+ this.capture.photo.upscale,
+ );
+ const called = this.callEvent("saveSave", {
+ screenshot: screenshot,
+ format: format,
+ save: file,
+ });
+ if (called > 0) return;
+ const blob = new Blob([file]);
+ savUrl = URL.createObjectURL(blob);
+ const a = this.createElement("a");
+ a.href = savUrl;
+ a.download = this.gameManager
+ .getSaveFilePath()
+ .split("/")
+ .pop();
+ a.click();
+ },
+ );
+ const loadSavFiles = addButton(
+ this.config.buttonOpts.loadSavFiles,
+ async () => {
+ const called = this.callEvent("loadSave");
+ if (called > 0) return;
+ const file = await this.selectFile();
+ const sav = new Uint8Array(await file.arrayBuffer());
+ const path = this.gameManager.getSaveFilePath();
+ const paths = path.split("/");
+ let cp = "";
+ for (let i = 0; i < paths.length - 1; i++) {
+ if (paths[i] === "") continue;
+ cp += "/" + paths[i];
+ if (!this.gameManager.FS.analyzePath(cp).exists)
+ this.gameManager.FS.mkdir(cp);
+ }
+ if (this.gameManager.FS.analyzePath(path).exists)
+ this.gameManager.FS.unlink(path);
+ this.gameManager.FS.writeFile(path, sav);
+ this.gameManager.loadSaveFiles();
+ },
+ );
const netplay = addButton(this.config.buttonOpts.netplay, async () => {
this.openNetplayMenu();
});
-
+
// add custom buttons
// get all elements from this.config.buttonOpts with custom: true
if (this.config.buttonOpts) {
@@ -2209,19 +2846,27 @@ class EmulatorJS {
const volumeSettings = this.createElement("div");
volumeSettings.classList.add("ejs_volume_parent");
- const muteButton = addButton(this.config.buttonOpts.mute, () => {
- muteButton.style.display = "none";
- unmuteButton.style.display = "";
- this.muted = true;
- this.setVolume(0);
- }, volumeSettings);
- const unmuteButton = addButton(this.config.buttonOpts.unmute, () => {
- if (this.volume === 0) this.volume = 0.5;
- muteButton.style.display = "";
- unmuteButton.style.display = "none";
- this.muted = false;
- this.setVolume(this.volume);
- }, volumeSettings);
+ const muteButton = addButton(
+ this.config.buttonOpts.mute,
+ () => {
+ muteButton.style.display = "none";
+ unmuteButton.style.display = "";
+ this.muted = true;
+ this.setVolume(0);
+ },
+ volumeSettings,
+ );
+ const unmuteButton = addButton(
+ this.config.buttonOpts.unmute,
+ () => {
+ if (this.volume === 0) this.volume = 0.5;
+ muteButton.style.display = "";
+ unmuteButton.style.display = "none";
+ this.muted = false;
+ this.setVolume(this.volume);
+ },
+ volumeSettings,
+ );
unmuteButton.style.display = "none";
const volumeSlider = this.createElement("input");
@@ -2238,94 +2883,233 @@ class EmulatorJS {
this.setVolume = (volume) => {
this.saveSettings();
- this.muted = (volume === 0);
+ this.muted = volume === 0;
volumeSlider.value = volume;
volumeSlider.setAttribute("aria-valuenow", volume * 100);
- volumeSlider.setAttribute("aria-valuetext", (volume * 100).toFixed(1) + "%");
- volumeSlider.setAttribute("style", "--value: " + volume * 100 + "%;margin-left: 5px;position: relative;z-index: 2;");
- if (this.Module.AL && this.Module.AL.currentCtx && this.Module.AL.currentCtx.sources) {
- this.Module.AL.currentCtx.sources.forEach(e => {
- e.gain.gain.value = volume;
- })
+ volumeSlider.setAttribute(
+ "aria-valuetext",
+ (volume * 100).toFixed(1) + "%",
+ );
+ volumeSlider.setAttribute(
+ "style",
+ "--value: " +
+ volume * 100 +
+ "%;margin-left: 5px;position: relative;z-index: 2;",
+ );
+
+ const isNetplayGuest =
+ this.isNetplay && this.netplay && !this.netplay.owner;
+
+ if (isNetplayGuest) {
+ if (this.netplay.remoteGainNode) {
+ this.netplay.remoteGainNode.gain.value = volume;
+ }
+
+ const audioElements = document.querySelectorAll(
+ 'audio[id^="ejs-remote-audio-"]',
+ );
+ audioElements.forEach(function (el) {
+ el.volume = Math.max(0, Math.min(1, volume));
+ el.muted = volume === 0;
+ });
+ } else {
+ if (
+ this.Module &&
+ this.Module.AL &&
+ this.Module.AL.currentCtx
+ ) {
+ const ctx = this.Module.AL.currentCtx;
+
+ if (ctx.gain && ctx.gain.gain) {
+ ctx.gain.gain.value = volume;
+ }
+
+ const sources = ctx.sources || {};
+ for (const k in sources) {
+ const s = sources[k];
+ if (s && s.gain && s.gain.gain) {
+ s.gain.gain.value = volume;
+ }
+ }
+ }
+
+ if (
+ this.isNetplay &&
+ this.netplay &&
+ this.netplay.owner &&
+ this.netplay.streamCompensationGain
+ ) {
+ const compensation = volume > 0.01 ? 1.0 / volume : 1.0;
+ this.netplay.streamCompensationGain.gain.value = Math.min(
+ compensation,
+ 20,
+ );
+ if (this.debug)
+ console.log(
+ "Stream compensation adjusted: " +
+ this.netplay.streamCompensationGain.gain.value,
+ );
+ }
}
- if (!this.config.buttonOpts || this.config.buttonOpts.mute !== false) {
- unmuteButton.style.display = (volume === 0) ? "" : "none";
- muteButton.style.display = (volume === 0) ? "none" : "";
+
+ if (
+ !this.config.buttonOpts ||
+ this.config.buttonOpts.mute !== false
+ ) {
+ unmuteButton.style.display = volume === 0 ? "" : "none";
+ muteButton.style.display = volume === 0 ? "none" : "";
}
- }
+ };
- this.addEventListener(volumeSlider, "change mousemove touchmove mousedown touchstart mouseup", (e) => {
- setTimeout(() => {
- const newVal = parseFloat(volumeSlider.value);
- if (newVal === 0 && this.muted) return;
- this.volume = newVal;
- this.setVolume(this.volume);
- }, 5);
- })
+ this.addEventListener(
+ volumeSlider,
+ "change mousemove touchmove mousedown touchstart mouseup",
+ (e) => {
+ setTimeout(() => {
+ const newVal = parseFloat(volumeSlider.value);
+ if (newVal === 0 && this.muted) return;
+ this.volume = newVal;
+ this.setVolume(this.volume);
+ }, 5);
+ },
+ );
- if (!this.config.buttonOpts || this.config.buttonOpts.volume !== false) {
+ if (
+ !this.config.buttonOpts ||
+ this.config.buttonOpts.volume !== false
+ ) {
volumeSettings.appendChild(volumeSlider);
}
this.elements.menu.appendChild(volumeSettings);
- const contextMenuButton = addButton(this.config.buttonOpts.contextMenu, () => {
- if (this.elements.contextmenu.style.display === "none") {
- this.elements.contextmenu.style.display = "block";
- this.elements.contextmenu.style.left = (getComputedStyle(this.elements.parent).width.split("px")[0] / 2 - getComputedStyle(this.elements.contextmenu).width.split("px")[0] / 2) + "px";
- this.elements.contextmenu.style.top = (getComputedStyle(this.elements.parent).height.split("px")[0] / 2 - getComputedStyle(this.elements.contextmenu).height.split("px")[0] / 2) + "px";
- setTimeout(this.menu.close.bind(this), 20);
- } else {
- this.elements.contextmenu.style.display = "none";
- }
- });
+ const contextMenuButton = addButton(
+ this.config.buttonOpts.contextMenu,
+ () => {
+ if (this.elements.contextmenu.style.display === "none") {
+ this.elements.contextmenu.style.display = "block";
+ this.elements.contextmenu.style.left =
+ getComputedStyle(this.elements.parent).width.split(
+ "px",
+ )[0] /
+ 2 -
+ getComputedStyle(this.elements.contextmenu).width.split(
+ "px",
+ )[0] /
+ 2 +
+ "px";
+ this.elements.contextmenu.style.top =
+ getComputedStyle(this.elements.parent).height.split(
+ "px",
+ )[0] /
+ 2 -
+ getComputedStyle(
+ this.elements.contextmenu,
+ ).height.split("px")[0] /
+ 2 +
+ "px";
+ setTimeout(this.menu.close.bind(this), 20);
+ } else {
+ this.elements.contextmenu.style.display = "none";
+ }
+ },
+ );
this.diskParent = this.createElement("div");
this.diskParent.id = "ejs_disksMenu";
this.disksMenuOpen = false;
- const diskButton = addButton(this.config.buttonOpts.diskButton, () => {
- this.disksMenuOpen = !this.disksMenuOpen;
- diskButton[1].classList.toggle("ejs_svg_rotate", this.disksMenuOpen);
- this.disksMenu.style.display = this.disksMenuOpen ? "" : "none";
- diskButton[2].classList.toggle("ejs_disks_text", this.disksMenuOpen);
- }, this.diskParent, true);
+ const diskButton = addButton(
+ this.config.buttonOpts.diskButton,
+ () => {
+ this.disksMenuOpen = !this.disksMenuOpen;
+ diskButton[1].classList.toggle(
+ "ejs_svg_rotate",
+ this.disksMenuOpen,
+ );
+ this.disksMenu.style.display = this.disksMenuOpen ? "" : "none";
+ diskButton[2].classList.toggle(
+ "ejs_disks_text",
+ this.disksMenuOpen,
+ );
+ },
+ this.diskParent,
+ true,
+ );
this.elements.menu.appendChild(this.diskParent);
this.closeDisksMenu = () => {
if (!this.disksMenu) return;
this.disksMenuOpen = false;
- diskButton[1].classList.toggle("ejs_svg_rotate", this.disksMenuOpen);
- diskButton[2].classList.toggle("ejs_disks_text", this.disksMenuOpen);
+ diskButton[1].classList.toggle(
+ "ejs_svg_rotate",
+ this.disksMenuOpen,
+ );
+ diskButton[2].classList.toggle(
+ "ejs_disks_text",
+ this.disksMenuOpen,
+ );
this.disksMenu.style.display = "none";
- }
- this.addEventListener(this.elements.parent, "mousedown touchstart", (e) => {
- if (this.isChild(this.disksMenu, e.target)) return;
- if (e.pointerType === "touch") return;
- if (e.target === diskButton[0] || e.target === diskButton[2]) return;
- this.closeDisksMenu();
- })
+ };
+ this.addEventListener(
+ this.elements.parent,
+ "mousedown touchstart",
+ (e) => {
+ if (this.isChild(this.disksMenu, e.target)) return;
+ if (e.pointerType === "touch") return;
+ if (e.target === diskButton[0] || e.target === diskButton[2])
+ return;
+ this.closeDisksMenu();
+ },
+ );
this.settingParent = this.createElement("div");
this.settingsMenuOpen = false;
- const settingButton = addButton(this.config.buttonOpts.settings, () => {
- this.settingsMenuOpen = !this.settingsMenuOpen;
- settingButton[1].classList.toggle("ejs_svg_rotate", this.settingsMenuOpen);
- this.settingsMenu.style.display = this.settingsMenuOpen ? "" : "none";
- settingButton[2].classList.toggle("ejs_settings_text", this.settingsMenuOpen);
- }, this.settingParent, true);
+ const settingButton = addButton(
+ this.config.buttonOpts.settings,
+ () => {
+ this.settingsMenuOpen = !this.settingsMenuOpen;
+ settingButton[1].classList.toggle(
+ "ejs_svg_rotate",
+ this.settingsMenuOpen,
+ );
+ this.settingsMenu.style.display = this.settingsMenuOpen
+ ? ""
+ : "none";
+ settingButton[2].classList.toggle(
+ "ejs_settings_text",
+ this.settingsMenuOpen,
+ );
+ },
+ this.settingParent,
+ true,
+ );
this.elements.menu.appendChild(this.settingParent);
this.closeSettingsMenu = () => {
if (!this.settingsMenu) return;
this.settingsMenuOpen = false;
- settingButton[1].classList.toggle("ejs_svg_rotate", this.settingsMenuOpen);
- settingButton[2].classList.toggle("ejs_settings_text", this.settingsMenuOpen);
+ settingButton[1].classList.toggle(
+ "ejs_svg_rotate",
+ this.settingsMenuOpen,
+ );
+ settingButton[2].classList.toggle(
+ "ejs_settings_text",
+ this.settingsMenuOpen,
+ );
this.settingsMenu.style.display = "none";
- }
- this.addEventListener(this.elements.parent, "mousedown touchstart", (e) => {
- if (this.isChild(this.settingsMenu, e.target)) return;
- if (e.pointerType === "touch") return;
- if (e.target === settingButton[0] || e.target === settingButton[2]) return;
- this.closeSettingsMenu();
- })
+ };
+ this.addEventListener(
+ this.elements.parent,
+ "mousedown touchstart",
+ (e) => {
+ if (this.isChild(this.settingsMenu, e.target)) return;
+ if (e.pointerType === "touch") return;
+ if (
+ e.target === settingButton[0] ||
+ e.target === settingButton[2]
+ )
+ return;
+ this.closeSettingsMenu();
+ },
+ );
this.addEventListener(this.canvas, "click", (e) => {
if (e.pointerType === "touch") return;
@@ -2337,7 +3121,7 @@ class EmulatorJS {
}
this.menu.close();
}
- })
+ });
const enter = addButton(this.config.buttonOpts.enterFullscreen, () => {
this.toggleFullscreen(true);
@@ -2362,8 +3146,14 @@ class EmulatorJS {
enter.style.display = "none";
if (this.isMobile) {
try {
- screen.orientation.lock(this.getCore(true) === "nds" ? "portrait" : "landscape").catch(e => {});
- } catch(e) {}
+ screen.orientation
+ .lock(
+ this.getCore(true) === "nds"
+ ? "portrait"
+ : "landscape",
+ )
+ .catch((e) => {});
+ } catch (e) {}
}
} else {
if (document.exitFullscreen) {
@@ -2380,79 +3170,94 @@ class EmulatorJS {
if (this.isMobile) {
try {
screen.orientation.unlock();
- } catch(e) {}
+ } catch (e) {}
}
}
- }
+ };
let exitMenuIsOpen = false;
- const exitEmulation = addButton(this.config.buttonOpts.exitEmulation, async () => {
- if (exitMenuIsOpen) return;
- exitMenuIsOpen = true;
- const popups = this.createSubPopup();
- this.game.appendChild(popups[0]);
- popups[1].classList.add("ejs_cheat_parent");
- popups[1].style.width = "100%";
- const popup = popups[1];
- const header = this.createElement("div");
- header.classList.add("ejs_cheat_header");
- const title = this.createElement("h2");
- title.innerText = this.localization("Are you sure you want to exit?");
- title.classList.add("ejs_cheat_heading");
- const close = this.createElement("button");
- close.classList.add("ejs_cheat_close");
- header.appendChild(title);
- header.appendChild(close);
- popup.appendChild(header);
- this.addEventListener(close, "click", (e) => {
- exitMenuIsOpen = false
- popups[0].remove();
- })
- popup.appendChild(this.createElement("br"));
-
- const footer = this.createElement("footer");
- const submit = this.createElement("button");
- const closeButton = this.createElement("button");
- submit.innerText = this.localization("Exit");
- closeButton.innerText = this.localization("Cancel");
- submit.classList.add("ejs_button_button");
- closeButton.classList.add("ejs_button_button");
- submit.classList.add("ejs_popup_submit");
- closeButton.classList.add("ejs_popup_submit");
- submit.style["background-color"] = "rgba(var(--ejs-primary-color),1)";
- footer.appendChild(submit);
- const span = this.createElement("span");
- span.innerText = " ";
- footer.appendChild(span);
- footer.appendChild(closeButton);
- popup.appendChild(footer);
-
- this.addEventListener(closeButton, "click", (e) => {
- popups[0].remove();
- exitMenuIsOpen = false
- })
+ const exitEmulation = addButton(
+ this.config.buttonOpts.exitEmulation,
+ async () => {
+ if (exitMenuIsOpen) return;
+ exitMenuIsOpen = true;
+ const popups = this.createSubPopup();
+ this.game.appendChild(popups[0]);
+ popups[1].classList.add("ejs_cheat_parent");
+ popups[1].style.width = "100%";
+ const popup = popups[1];
+ const header = this.createElement("div");
+ header.classList.add("ejs_cheat_header");
+ const title = this.createElement("h2");
+ title.innerText = this.localization(
+ "Are you sure you want to exit?",
+ );
+ title.classList.add("ejs_cheat_heading");
+ const close = this.createElement("button");
+ close.classList.add("ejs_cheat_close");
+ header.appendChild(title);
+ header.appendChild(close);
+ popup.appendChild(header);
+ this.addEventListener(close, "click", (e) => {
+ exitMenuIsOpen = false;
+ popups[0].remove();
+ });
+ popup.appendChild(this.createElement("br"));
- this.addEventListener(submit, "click", (e) => {
- popups[0].remove();
- const body = this.createPopup("EmulatorJS has exited", {});
- this.callEvent("exit");
- })
- setTimeout(this.menu.close.bind(this), 20);
- });
+ const footer = this.createElement("footer");
+ const submit = this.createElement("button");
+ const closeButton = this.createElement("button");
+ submit.innerText = this.localization("Exit");
+ closeButton.innerText = this.localization("Cancel");
+ submit.classList.add("ejs_button_button");
+ closeButton.classList.add("ejs_button_button");
+ submit.classList.add("ejs_popup_submit");
+ closeButton.classList.add("ejs_popup_submit");
+ submit.style["background-color"] =
+ "rgba(var(--ejs-primary-color),1)";
+ footer.appendChild(submit);
+ const span = this.createElement("span");
+ span.innerText = " ";
+ footer.appendChild(span);
+ footer.appendChild(closeButton);
+ popup.appendChild(footer);
- this.addEventListener(document, "webkitfullscreenchange mozfullscreenchange fullscreenchange", (e) => {
- if (e.target !== this.elements.parent) return;
- if (document.fullscreenElement === null) {
- exit.style.display = "none";
- enter.style.display = "";
- } else {
- //not sure if this is possible, lets put it here anyways
- exit.style.display = "";
- enter.style.display = "none";
- }
- })
+ this.addEventListener(closeButton, "click", (e) => {
+ popups[0].remove();
+ exitMenuIsOpen = false;
+ });
+
+ this.addEventListener(submit, "click", (e) => {
+ popups[0].remove();
+ const body = this.createPopup("EmulatorJS has exited", {});
+ this.callEvent("exit");
+ });
+ setTimeout(this.menu.close.bind(this), 20);
+ },
+ );
+
+ this.addEventListener(
+ document,
+ "webkitfullscreenchange mozfullscreenchange fullscreenchange",
+ (e) => {
+ if (e.target !== this.elements.parent) return;
+ if (document.fullscreenElement === null) {
+ exit.style.display = "none";
+ enter.style.display = "";
+ } else {
+ //not sure if this is possible, lets put it here anyways
+ exit.style.display = "";
+ enter.style.display = "none";
+ }
+ },
+ );
- const hasFullscreen = !!(this.elements.parent.requestFullscreen || this.elements.parent.mozRequestFullScreen || this.elements.parent.webkitRequestFullscreen || this.elements.parent.msRequestFullscreen);
+ const hasFullscreen = !!(
+ this.elements.parent.requestFullscreen ||
+ this.elements.parent.mozRequestFullScreen ||
+ this.elements.parent.webkitRequestFullscreen ||
+ this.elements.parent.msRequestFullscreen
+ );
if (!hasFullscreen) {
exit.style.display = "none";
@@ -2473,8 +3278,8 @@ class EmulatorJS {
saveSavFiles: [saveSavFiles],
loadSavFiles: [loadSavFiles],
netplay: [netplay],
- exit: [exitEmulation]
- }
+ exit: [exitEmulation],
+ };
if (this.config.buttonOpts) {
if (this.debug) console.log(this.config.buttonOpts);
@@ -2482,9 +3287,16 @@ class EmulatorJS {
pauseButton.style.display = "none";
playButton.style.display = "none";
}
- if (this.config.buttonOpts.contextMenu.visible === false && this.config.buttonOpts.rightClick !== false && this.isMobile === false) contextMenuButton.style.display = "none"
- if (this.config.buttonOpts.restart.visible === false) restartButton.style.display = "none"
- if (this.config.buttonOpts.settings.visible === false) settingButton[0].style.display = "none"
+ if (
+ this.config.buttonOpts.contextMenu.visible === false &&
+ this.config.buttonOpts.rightClick !== false &&
+ this.isMobile === false
+ )
+ contextMenuButton.style.display = "none";
+ if (this.config.buttonOpts.restart.visible === false)
+ restartButton.style.display = "none";
+ if (this.config.buttonOpts.settings.visible === false)
+ settingButton[0].style.display = "none";
if (this.config.buttonOpts.fullscreen.visible === false) {
enter.style.display = "none";
exit.style.display = "none";
@@ -2493,17 +3305,28 @@ class EmulatorJS {
muteButton.style.display = "none";
unmuteButton.style.display = "none";
}
- if (this.config.buttonOpts.saveState.visible === false) saveState.style.display = "none";
- if (this.config.buttonOpts.loadState.visible === false) loadState.style.display = "none";
- if (this.config.buttonOpts.saveSavFiles.visible === false) saveSavFiles.style.display = "none";
- if (this.config.buttonOpts.loadSavFiles.visible === false) loadSavFiles.style.display = "none";
- if (this.config.buttonOpts.gamepad.visible === false) controlMenu.style.display = "none";
- if (this.config.buttonOpts.cheat.visible === false) cheatMenu.style.display = "none";
- if (this.config.buttonOpts.cacheManager.visible === false) cache.style.display = "none";
- if (this.config.buttonOpts.netplay.visible === false) netplay.style.display = "none";
- if (this.config.buttonOpts.diskButton.visible === false) diskButton[0].style.display = "none";
- if (this.config.buttonOpts.volumeSlider.visible === false) volumeSlider.style.display = "none";
- if (this.config.buttonOpts.exitEmulation.visible === false) exitEmulation.style.display = "none";
+ if (this.config.buttonOpts.saveState.visible === false)
+ saveState.style.display = "none";
+ if (this.config.buttonOpts.loadState.visible === false)
+ loadState.style.display = "none";
+ if (this.config.buttonOpts.saveSavFiles.visible === false)
+ saveSavFiles.style.display = "none";
+ if (this.config.buttonOpts.loadSavFiles.visible === false)
+ loadSavFiles.style.display = "none";
+ if (this.config.buttonOpts.gamepad.visible === false)
+ controlMenu.style.display = "none";
+ if (this.config.buttonOpts.cheat.visible === false)
+ cheatMenu.style.display = "none";
+ if (this.config.buttonOpts.cacheManager.visible === false)
+ cache.style.display = "none";
+ if (this.config.buttonOpts.netplay.visible === false)
+ netplay.style.display = "none";
+ if (this.config.buttonOpts.diskButton.visible === false)
+ diskButton[0].style.display = "none";
+ if (this.config.buttonOpts.volumeSlider.visible === false)
+ volumeSlider.style.display = "none";
+ if (this.config.buttonOpts.exitEmulation.visible === false)
+ exitEmulation.style.display = "none";
}
this.menu.failedToStart = () => {
@@ -2538,7 +3361,7 @@ class EmulatorJS {
this.virtualGamepad.style.display = "none";
settingButton[0].classList.add("shadow");
this.menu.open(true);
- }
+ };
}
openCacheMenu() {
(async () => {
@@ -2578,27 +3401,28 @@ class EmulatorJS {
const body = this.createPopup("Cache Manager", {
"Cleanup Now": async () => {
- const cleanupBtn = document.querySelector('.ejs_popup_button');
- if (cleanupBtn) cleanupBtn.textContent = 'Cleaning...';
+ const cleanupBtn =
+ document.querySelector(".ejs_popup_button");
+ if (cleanupBtn) cleanupBtn.textContent = "Cleaning...";
await this.storageCache.cleanup();
tbody.innerHTML = "";
// Refresh the cache list
await this.populateCacheList(tbody, getSize, getTypeName);
- if (cleanupBtn) cleanupBtn.textContent = 'Cleanup Now';
+ if (cleanupBtn) cleanupBtn.textContent = "Cleanup Now";
},
"Clear All": async () => {
await this.storageCache.clear();
tbody.innerHTML = "";
},
- "Close": () => {
+ Close: () => {
this.closePopup();
- }
+ },
});
-
+
list.style.width = "100%";
list.style["padding-left"] = "10px";
list.style["text-align"] = "left";
-
+
list.appendChild(thead);
list.appendChild(tbody);
body.appendChild(list);
@@ -2606,21 +3430,25 @@ class EmulatorJS {
const getSize = function (size) {
let i = -1;
do {
- size /= 1024, i++;
+ ((size /= 1024), i++);
} while (size > 1024);
- return Math.max(size, 0.1).toFixed(1) + [" kB", " MB", " GB", " TB", "PB", "EB", "ZB", "YB"][i];
- }
+ return (
+ Math.max(size, 0.1).toFixed(1) +
+ [" kB", " MB", " GB", " TB", "PB", "EB", "ZB", "YB"][i]
+ );
+ };
const getTypeName = function (key) {
- if (key.startsWith('compression_')) return 'Decompressed Content';
- if (key.startsWith('core_decompressed_')) return 'Core';
+ if (key.startsWith("compression_"))
+ return "Decompressed Content";
+ if (key.startsWith("core_decompressed_")) return "Core";
// Additional fallback logic for other types
- if (key.includes('core')) return 'Core';
- if (key.includes('bios')) return 'BIOS';
- if (key.includes('rom')) return 'ROM';
- if (key.includes('asset')) return 'Asset';
- return 'Unknown';
- }
+ if (key.includes("core")) return "Core";
+ if (key.includes("bios")) return "BIOS";
+ if (key.includes("rom")) return "ROM";
+ if (key.includes("asset")) return "Asset";
+ return "Unknown";
+ };
await this.populateCacheList(tbody, getSize, getTypeName);
})();
@@ -2646,7 +3474,9 @@ class EmulatorJS {
// Use filename if available, otherwise fall back to key
const displayName = item.filename || item.key;
- name.innerText = displayName.substring(0, 50) + (displayName.length > 50 ? '...' : '');
+ name.innerText =
+ displayName.substring(0, 50) +
+ (displayName.length > 50 ? "..." : "");
// Use the stored type if available, otherwise fall back to getTypeName
const itemType = item.type || getTypeName(item.key);
@@ -2654,7 +3484,8 @@ class EmulatorJS {
size.innerText = getSize(totalSize);
// Format last accessed time
- const lastAccessedTime = item.lastAccessed || item.added || Date.now();
+ const lastAccessedTime =
+ item.lastAccessed || item.added || Date.now();
const formatDate = (timestamp) => {
const date = new Date(timestamp);
const now = new Date();
@@ -2663,7 +3494,7 @@ class EmulatorJS {
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
- if (diffMins < 1) return 'Just now';
+ if (diffMins < 1) return "Just now";
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
@@ -2678,7 +3509,7 @@ class EmulatorJS {
this.addEventListener(remove, "click", async () => {
await this.storageCache.delete(item.key);
line.remove();
- })
+ });
remove.appendChild(a);
line.appendChild(name);
@@ -2691,7 +3522,10 @@ class EmulatorJS {
}
getControlScheme() {
- if (this.config.controlScheme && typeof this.config.controlScheme === "string") {
+ if (
+ this.config.controlScheme &&
+ typeof this.config.controlScheme === "string"
+ ) {
return this.config.controlScheme;
} else {
return this.getCore(true);
@@ -2699,29 +3533,36 @@ class EmulatorJS {
}
createControlSettingMenu() {
let buttonListeners = [];
- this.checkGamepadInputs = () => buttonListeners.forEach(elem => elem());
+ this.checkGamepadInputs = () =>
+ buttonListeners.forEach((elem) => elem());
this.gamepadLabels = [];
this.gamepadSelection = [];
this.controls = JSON.parse(JSON.stringify(this.defaultControllers));
- const body = this.createPopup("Control Settings", {
- "Reset": () => {
- this.stopAllAutofire();
- this.controls = JSON.parse(JSON.stringify(this.defaultControllers));
- this.setupKeys();
- this.checkGamepadInputs();
- this.saveSettings();
- },
- "Clear": () => {
- this.stopAllAutofire();
- this.controls = { 0: {}, 1: {}, 2: {}, 3: {} };
- this.setupKeys();
- this.checkGamepadInputs();
- this.saveSettings();
+ const body = this.createPopup(
+ "Control Settings",
+ {
+ Reset: () => {
+ this.stopAllAutofire();
+ this.controls = JSON.parse(
+ JSON.stringify(this.defaultControllers),
+ );
+ this.setupKeys();
+ this.checkGamepadInputs();
+ this.saveSettings();
+ },
+ Clear: () => {
+ this.stopAllAutofire();
+ this.controls = { 0: {}, 1: {}, 2: {}, 3: {} };
+ this.setupKeys();
+ this.checkGamepadInputs();
+ this.saveSettings();
+ },
+ Close: () => {
+ this.controlMenu.style.display = "none";
+ },
},
- "Close": () => {
- this.controlMenu.style.display = "none";
- }
- }, true);
+ true,
+ );
this.setupKeys();
this.controlMenu = body.parentElement;
body.classList.add("ejs_control_body");
@@ -2750,10 +3591,19 @@ class EmulatorJS {
{ id: 7, label: this.localization("RIGHT") },
];
if (this.getCore() === "nestopia") {
- buttons.push({ id: 10, label: this.localization("SWAP DISKS") });
+ buttons.push({
+ id: 10,
+ label: this.localization("SWAP DISKS"),
+ });
} else {
- buttons.push({ id: 10, label: this.localization("SWAP DISKS") });
- buttons.push({ id: 11, label: this.localization("EJECT/INSERT DISK") });
+ buttons.push({
+ id: 10,
+ label: this.localization("SWAP DISKS"),
+ });
+ buttons.push({
+ id: 11,
+ label: this.localization("EJECT/INSERT DISK"),
+ });
}
} else if ("snes" === this.getControlScheme()) {
buttons = [
@@ -2837,7 +3687,9 @@ class EmulatorJS {
{ id: 17, label: this.localization("RIGHT D-PAD LEFT") },
{ id: 16, label: this.localization("RIGHT D-PAD RIGHT") },
];
- } else if (["segaMD", "segaCD", "sega32x"].includes(this.getControlScheme())) {
+ } else if (
+ ["segaMD", "segaCD", "sega32x"].includes(this.getControlScheme())
+ ) {
buttons = [
{ id: 1, label: this.localization("A") },
{ id: 0, label: this.localization("B") },
@@ -3111,7 +3963,7 @@ class EmulatorJS {
{ id: 26, label: this.localization("CHANGE STATE SLOT") },
{ id: 27, label: this.localization("FAST FORWARD") },
{ id: 29, label: this.localization("SLOW MOTION") },
- { id: 28, label: this.localization("REWIND") }
+ { id: 28, label: this.localization("REWIND") },
);
let nums = [];
for (let i = 0; i < buttons.length; i++) {
@@ -3153,12 +4005,14 @@ class EmulatorJS {
player.id = "controls-" + (i - 1) + "-label";
this.addEventListener(player, "click", (e) => {
e.preventDefault();
- players[selectedPlayer].classList.remove("ejs_control_selected");
+ players[selectedPlayer].classList.remove(
+ "ejs_control_selected",
+ );
playerDivs[selectedPlayer].setAttribute("hidden", "");
selectedPlayer = i - 1;
players[i - 1].classList.add("ejs_control_selected");
playerDivs[i - 1].removeAttribute("hidden");
- })
+ });
playerContainer.appendChild(player);
playerSelect.appendChild(playerContainer);
players.push(playerContainer);
@@ -3172,7 +4026,8 @@ class EmulatorJS {
const playerTitle = this.createElement("div");
const gamepadTitle = this.createElement("div");
- gamepadTitle.innerText = this.localization("Connected Gamepad") + ": ";
+ gamepadTitle.innerText =
+ this.localization("Connected Gamepad") + ": ";
const gamepadName = this.createElement("select");
gamepadName.classList.add("ejs_gamepad_dropdown");
@@ -3180,7 +4035,7 @@ class EmulatorJS {
gamepadName.setAttribute("index", i);
this.gamepadLabels.push(gamepadName);
this.gamepadSelection.push("");
- this.addEventListener(gamepadName, "change", e => {
+ this.addEventListener(gamepadName, "change", (e) => {
const controller = e.target.value;
const player = parseInt(e.target.getAttribute("index"));
if (controller === "notconnected") {
@@ -3219,11 +4074,13 @@ class EmulatorJS {
aboutParent.appendChild(keyboard);
const setHeader = this.createElement("div");
- setHeader.style = "font-size:12px;width:15%;float:left;text-align:center;";
+ setHeader.style =
+ "font-size:12px;width:15%;float:left;text-align:center;";
setHeader.innerHTML = " ";
const autofireHeader = this.createElement("div");
- autofireHeader.style = "font-size:12px;width:20%;float:left;text-align:center;";
+ autofireHeader.style =
+ "font-size:12px;width:20%;float:left;text-align:center;";
autofireHeader.innerText = this.localization("Autofire");
const headingPadding = this.createElement("div");
@@ -3237,7 +4094,8 @@ class EmulatorJS {
if ((this.touch || this.hasTouchScreen) && i === 0) {
const vgp = this.createElement("div");
- vgp.style = "width:25%;float:right;clear:none;padding:0;font-size: 11px;padding-left: 2.25rem;";
+ vgp.style =
+ "width:25%;float:right;clear:none;padding:0;font-size: 11px;padding-left: 2.25rem;";
vgp.classList.add("ejs_control_row");
vgp.classList.add("ejs_cheat_row");
const input = this.createElement("input");
@@ -3252,13 +4110,18 @@ class EmulatorJS {
vgp.appendChild(label);
label.addEventListener("click", (e) => {
input.checked = !input.checked;
- this.changeSettingOption("virtual-gamepad", input.checked ? "enabled" : "disabled");
- })
+ this.changeSettingOption(
+ "virtual-gamepad",
+ input.checked ? "enabled" : "disabled",
+ );
+ });
this.on("start", (e) => {
- if (this.getSettingValue("virtual-gamepad") === "disabled") {
+ if (
+ this.getSettingValue("virtual-gamepad") === "disabled"
+ ) {
input.checked = false;
}
- })
+ });
playerTitle.appendChild(vgp);
}
@@ -3307,24 +4170,37 @@ class EmulatorJS {
buttonListeners.push(() => {
textBox2.value = "";
textBox1.value = "";
- if (this.controls[i][k] && this.controls[i][k].value !== undefined) {
+ if (
+ this.controls[i][k] &&
+ this.controls[i][k].value !== undefined
+ ) {
let value = this.keyMap[this.controls[i][k].value];
value = this.localization(value);
textBox2.value = value;
}
- if (this.controls[i][k] && this.controls[i][k].value2 !== undefined && this.controls[i][k].value2 !== "") {
+ if (
+ this.controls[i][k] &&
+ this.controls[i][k].value2 !== undefined &&
+ this.controls[i][k].value2 !== ""
+ ) {
let value2 = this.controls[i][k].value2.toString();
if (value2.includes(":")) {
value2 = value2.split(":");
- value2 = this.localization(value2[0]) + ":" + this.localization(value2[1])
+ value2 =
+ this.localization(value2[0]) +
+ ":" +
+ this.localization(value2[1]);
} else if (!isNaN(value2)) {
- value2 = this.localization("BUTTON") + " " + this.localization(value2);
+ value2 =
+ this.localization("BUTTON") +
+ " " +
+ this.localization(value2);
} else {
value2 = this.localization(value2);
}
textBox1.value = value2;
}
- })
+ });
if (this.controls[i][k] && this.controls[i][k].value) {
let value = this.keyMap[this.controls[i][k].value];
@@ -3335,9 +4211,15 @@ class EmulatorJS {
let value2 = this.controls[i][k].value2.toString();
if (value2.includes(":")) {
value2 = value2.split(":");
- value2 = this.localization(value2[0]) + ":" + this.localization(value2[1])
+ value2 =
+ this.localization(value2[0]) +
+ ":" +
+ this.localization(value2[1]);
} else if (!isNaN(value2)) {
- value2 = this.localization("BUTTON") + " " + this.localization(value2);
+ value2 =
+ this.localization("BUTTON") +
+ " " +
+ this.localization(value2);
} else {
value2 = this.localization(value2);
}
@@ -3360,29 +4242,39 @@ class EmulatorJS {
// Autofire checkbox - not available for analog stick axes
const autofireColumn = this.createElement("div");
- autofireColumn.style = "width:20%;float:left;text-align:center;";
+ autofireColumn.style =
+ "width:20%;float:left;text-align:center;";
if (!this.analogAxes.includes(k)) {
const autofireCheckbox = this.createElement("input");
autofireCheckbox.type = "checkbox";
autofireCheckbox.style = "cursor:pointer;";
- autofireCheckbox.checked = this.controls[i][k] && this.controls[i][k].autofire === true;
+ autofireCheckbox.checked =
+ this.controls[i][k] &&
+ this.controls[i][k].autofire === true;
autofireCheckbox.setAttribute("data-player", i);
autofireCheckbox.setAttribute("data-button", k);
// Update checkbox state when controls change
buttonListeners.push(() => {
- autofireCheckbox.checked = this.controls[i][k] && this.controls[i][k].autofire === true;
+ autofireCheckbox.checked =
+ this.controls[i][k] &&
+ this.controls[i][k].autofire === true;
});
this.addEventListener(autofireCheckbox, "change", (e) => {
e.stopPropagation();
- const playerIdx = parseInt(e.target.getAttribute("data-player"));
- const buttonIdx = parseInt(e.target.getAttribute("data-button"));
+ const playerIdx = parseInt(
+ e.target.getAttribute("data-player"),
+ );
+ const buttonIdx = parseInt(
+ e.target.getAttribute("data-button"),
+ );
if (!this.controls[playerIdx][buttonIdx]) {
this.controls[playerIdx][buttonIdx] = {};
}
- this.controls[playerIdx][buttonIdx].autofire = e.target.checked;
+ this.controls[playerIdx][buttonIdx].autofire =
+ e.target.checked;
// Stop any active autofire if unchecked
if (!e.target.checked) {
this.stopAutofire(playerIdx, buttonIdx);
@@ -3406,15 +4298,24 @@ class EmulatorJS {
this.addEventListener(buttonText, "mousedown", (e) => {
// Don't open popup when clicking on the autofire checkbox
- if (e.target.tagName === "INPUT" && e.target.type === "checkbox") {
+ if (
+ e.target.tagName === "INPUT" &&
+ e.target.type === "checkbox"
+ ) {
return;
}
e.preventDefault();
- this.controlPopup.parentElement.parentElement.removeAttribute("hidden");
- this.controlPopup.innerText = "[ " + controlLabel + " ]\n" + this.localization("Press Keyboard");
+ this.controlPopup.parentElement.parentElement.removeAttribute(
+ "hidden",
+ );
+ this.controlPopup.innerText =
+ "[ " +
+ controlLabel +
+ " ]\n" +
+ this.localization("Press Keyboard");
this.controlPopup.setAttribute("button-num", k);
this.controlPopup.setAttribute("player-num", i);
- })
+ });
}
controls.appendChild(player);
player.setAttribute("hidden", "");
@@ -3431,8 +4332,11 @@ class EmulatorJS {
const popupMsg = this.createElement("div");
this.addEventListener(popup, "mousedown click touchstart", (e) => {
if (this.isChild(popupMsg, e.target)) return;
- this.controlPopup.parentElement.parentElement.setAttribute("hidden", "");
- })
+ this.controlPopup.parentElement.parentElement.setAttribute(
+ "hidden",
+ "",
+ );
+ });
const btn = this.createElement("a");
btn.classList.add("ejs_control_set_button");
btn.innerText = this.localization("Clear");
@@ -3444,10 +4348,13 @@ class EmulatorJS {
}
this.controls[player][num].value = 0;
this.controls[player][num].value2 = "";
- this.controlPopup.parentElement.parentElement.setAttribute("hidden", "");
+ this.controlPopup.parentElement.parentElement.setAttribute(
+ "hidden",
+ "",
+ );
this.checkGamepadInputs();
this.saveSettings();
- })
+ });
popupMsg.classList.add("ejs_popup_box");
popupMsg.innerText = "";
popup.setAttribute("hidden", "");
@@ -3463,109 +4370,109 @@ class EmulatorJS {
this.defaultControllers = {
0: {
0: {
- "value": "x",
- "value2": "BUTTON_2"
+ value: "x",
+ value2: "BUTTON_2",
},
1: {
- "value": "s",
- "value2": "BUTTON_4"
+ value: "s",
+ value2: "BUTTON_4",
},
2: {
- "value": "v",
- "value2": "SELECT"
+ value: "v",
+ value2: "SELECT",
},
3: {
- "value": "enter",
- "value2": "START"
+ value: "enter",
+ value2: "START",
},
4: {
- "value": "up arrow",
- "value2": "DPAD_UP"
+ value: "up arrow",
+ value2: "DPAD_UP",
},
5: {
- "value": "down arrow",
- "value2": "DPAD_DOWN"
+ value: "down arrow",
+ value2: "DPAD_DOWN",
},
6: {
- "value": "left arrow",
- "value2": "DPAD_LEFT"
+ value: "left arrow",
+ value2: "DPAD_LEFT",
},
7: {
- "value": "right arrow",
- "value2": "DPAD_RIGHT"
+ value: "right arrow",
+ value2: "DPAD_RIGHT",
},
8: {
- "value": "z",
- "value2": "BUTTON_1"
+ value: "z",
+ value2: "BUTTON_1",
},
9: {
- "value": "a",
- "value2": "BUTTON_3"
+ value: "a",
+ value2: "BUTTON_3",
},
10: {
- "value": "q",
- "value2": "LEFT_TOP_SHOULDER"
+ value: "q",
+ value2: "LEFT_TOP_SHOULDER",
},
11: {
- "value": "e",
- "value2": "RIGHT_TOP_SHOULDER"
+ value: "e",
+ value2: "RIGHT_TOP_SHOULDER",
},
12: {
- "value": "tab",
- "value2": "LEFT_BOTTOM_SHOULDER"
+ value: "tab",
+ value2: "LEFT_BOTTOM_SHOULDER",
},
13: {
- "value": "r",
- "value2": "RIGHT_BOTTOM_SHOULDER"
+ value: "r",
+ value2: "RIGHT_BOTTOM_SHOULDER",
},
14: {
- "value": "",
- "value2": "LEFT_STICK",
+ value: "",
+ value2: "LEFT_STICK",
},
15: {
- "value": "",
- "value2": "RIGHT_STICK",
+ value: "",
+ value2: "RIGHT_STICK",
},
16: {
- "value": "h",
- "value2": "LEFT_STICK_X:+1"
+ value: "h",
+ value2: "LEFT_STICK_X:+1",
},
17: {
- "value": "f",
- "value2": "LEFT_STICK_X:-1"
+ value: "f",
+ value2: "LEFT_STICK_X:-1",
},
18: {
- "value": "g",
- "value2": "LEFT_STICK_Y:+1"
+ value: "g",
+ value2: "LEFT_STICK_Y:+1",
},
19: {
- "value": "t",
- "value2": "LEFT_STICK_Y:-1"
+ value: "t",
+ value2: "LEFT_STICK_Y:-1",
},
20: {
- "value": "l",
- "value2": "RIGHT_STICK_X:+1"
+ value: "l",
+ value2: "RIGHT_STICK_X:+1",
},
21: {
- "value": "j",
- "value2": "RIGHT_STICK_X:-1"
+ value: "j",
+ value2: "RIGHT_STICK_X:-1",
},
22: {
- "value": "k",
- "value2": "RIGHT_STICK_Y:+1"
+ value: "k",
+ value2: "RIGHT_STICK_Y:+1",
},
23: {
- "value": "i",
- "value2": "RIGHT_STICK_Y:-1"
+ value: "i",
+ value2: "RIGHT_STICK_Y:-1",
},
24: {
- "value": "1"
+ value: "1",
},
25: {
- "value": "2"
+ value: "2",
},
26: {
- "value": "3"
+ value: "3",
},
27: {},
28: {},
@@ -3573,8 +4480,8 @@ class EmulatorJS {
},
1: {},
2: {},
- 3: {}
- }
+ 3: {},
+ };
// Analog stick axes - these use 0x7fff values and don't support autofire
this.analogAxes = [16, 17, 18, 19, 20, 21, 22, 23];
this.keyMap = {
@@ -3677,17 +4584,22 @@ class EmulatorJS {
219: "open bracket",
220: "back slash",
221: "close braket",
- 222: "single quote"
- }
+ 222: "single quote",
+ };
}
setupKeys() {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 30; j++) {
if (this.controls[i][j]) {
- this.controls[i][j].value = parseInt(this.keyLookup(this.controls[i][j].value));
+ this.controls[i][j].value = parseInt(
+ this.keyLookup(this.controls[i][j].value),
+ );
if (this.controls[i][j].value === -1 && this.debug) {
delete this.controls[i][j].value;
- if (this.debug) console.warn("Invalid key for control " + j + " player " + i);
+ if (this.debug)
+ console.warn(
+ "Invalid key for control " + j + " player " + i,
+ );
}
}
}
@@ -3696,7 +4608,7 @@ class EmulatorJS {
keyLookup(controllerkey) {
if (controllerkey === undefined) return 0;
if (typeof controllerkey === "number") return controllerkey;
- controllerkey = controllerkey.toString().toLowerCase()
+ controllerkey = controllerkey.toString().toLowerCase();
const values = Object.values(this.keyMap);
if (values.includes(controllerkey)) {
const index = values.indexOf(controllerkey);
@@ -3705,15 +4617,21 @@ class EmulatorJS {
return -1;
}
getAutofireInterval(playerIndex, buttonIndex) {
- const control = this.controls[playerIndex] && this.controls[playerIndex][buttonIndex];
+ const control =
+ this.controls[playerIndex] &&
+ this.controls[playerIndex][buttonIndex];
if (control && typeof control.autoFireInterval === "number") {
return control.autoFireInterval;
}
const settingValue = this.getSettingValue("autofireInterval");
- return settingValue ? parseInt(settingValue) : this.defaultAutoFireInterval;
+ return settingValue
+ ? parseInt(settingValue)
+ : this.defaultAutoFireInterval;
}
isAutofireEnabled(playerIndex, buttonIndex) {
- const control = this.controls[playerIndex] && this.controls[playerIndex][buttonIndex];
+ const control =
+ this.controls[playerIndex] &&
+ this.controls[playerIndex][buttonIndex];
return control && control.autofire === true;
}
startAutofire(playerIndex, buttonIndex, inputValue) {
@@ -3726,7 +4644,11 @@ class EmulatorJS {
this.autofireIntervals[key] = setInterval(() => {
if (this.paused || !this.gameManager) return;
pressed = !pressed;
- this.gameManager.simulateInput(playerIndex, buttonIndex, pressed ? inputValue : 0);
+ this.gameManager.simulateInput(
+ playerIndex,
+ buttonIndex,
+ pressed ? inputValue : 0,
+ );
}, interval);
}
stopAutofire(playerIndex, buttonIndex) {
@@ -3750,30 +4672,47 @@ class EmulatorJS {
keyChange(e) {
if (e.repeat) return;
if (!this.started) return;
- if (this.controlPopup.parentElement.parentElement.getAttribute("hidden") === null) {
+ if (
+ this.controlPopup.parentElement.parentElement.getAttribute(
+ "hidden",
+ ) === null
+ ) {
const num = this.controlPopup.getAttribute("button-num");
const player = this.controlPopup.getAttribute("player-num");
if (!this.controls[player][num]) {
this.controls[player][num] = {};
}
this.controls[player][num].value = e.keyCode;
- this.controlPopup.parentElement.parentElement.setAttribute("hidden", "");
+ this.controlPopup.parentElement.parentElement.setAttribute(
+ "hidden",
+ "",
+ );
this.checkGamepadInputs();
this.saveSettings();
return;
}
- if (this.settingsMenu.style.display !== "none" || this.isPopupOpen() || this.getSettingValue("keyboardInput") === "enabled") return;
+ if (
+ this.settingsMenu.style.display !== "none" ||
+ this.isPopupOpen() ||
+ this.getSettingValue("keyboardInput") === "enabled"
+ )
+ return;
e.preventDefault();
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 30; j++) {
- if (this.controls[i][j] && this.controls[i][j].value === e.keyCode) {
+ if (
+ this.controls[i][j] &&
+ this.controls[i][j].value === e.keyCode
+ ) {
const isAnalog = this.analogAxes.includes(j);
const inputValue = isAnalog ? 0x7fff : 1;
const isKeyUp = e.type === "keyup";
const value = isKeyUp ? 0 : inputValue;
if (this.isAutofireEnabled(i, j) && !isAnalog) {
- isKeyUp ? this.stopAutofire(i, j) : this.startAutofire(i, j, inputValue);
+ isKeyUp
+ ? this.stopAutofire(i, j)
+ : this.startAutofire(i, j, inputValue);
} else {
this.gameManager.simulateInput(i, j, value);
}
@@ -3783,89 +4722,158 @@ class EmulatorJS {
}
gamepadEvent(e) {
if (!this.started) return;
- const gamepadIndex = this.gamepadSelection.indexOf(this.gamepad.gamepads[e.gamepadIndex].id + "_" + this.gamepad.gamepads[e.gamepadIndex].index);
+ const gamepadIndex = this.gamepadSelection.indexOf(
+ this.gamepad.gamepads[e.gamepadIndex].id +
+ "_" +
+ this.gamepad.gamepads[e.gamepadIndex].index,
+ );
if (gamepadIndex < 0) {
return; // Gamepad not set anywhere
}
- const value = function (value) {
+ const value = (function (value) {
if (value > 0.5 || value < -0.5) {
- return (value > 0) ? 1 : -1;
+ return value > 0 ? 1 : -1;
} else {
return 0;
}
- }(e.value || 0);
- if (this.controlPopup.parentElement.parentElement.getAttribute("hidden") === null) {
- if ("buttonup" === e.type || (e.type === "axischanged" && value === 0)) return;
+ })(e.value || 0);
+ if (
+ this.controlPopup.parentElement.parentElement.getAttribute(
+ "hidden",
+ ) === null
+ ) {
+ if (
+ "buttonup" === e.type ||
+ (e.type === "axischanged" && value === 0)
+ )
+ return;
const num = this.controlPopup.getAttribute("button-num");
- const player = parseInt(this.controlPopup.getAttribute("player-num"));
+ const player = parseInt(
+ this.controlPopup.getAttribute("player-num"),
+ );
if (gamepadIndex !== player) return;
if (!this.controls[player][num]) {
this.controls[player][num] = {};
}
this.controls[player][num].value2 = e.label;
- this.controlPopup.parentElement.parentElement.setAttribute("hidden", "");
+ this.controlPopup.parentElement.parentElement.setAttribute(
+ "hidden",
+ "",
+ );
this.checkGamepadInputs();
this.saveSettings();
return;
}
- if (this.settingsMenu.style.display !== "none" || this.isPopupOpen()) return;
+ if (this.settingsMenu.style.display !== "none" || this.isPopupOpen())
+ return;
for (let i = 0; i < 4; i++) {
if (gamepadIndex !== i) continue;
for (let j = 0; j < 30; j++) {
- if (!this.controls[i][j] || this.controls[i][j].value2 === undefined) {
+ if (
+ !this.controls[i][j] ||
+ this.controls[i][j].value2 === undefined
+ ) {
continue;
}
const controlValue = this.controls[i][j].value2;
const isAnalog = this.analogAxes.includes(j);
- if (["buttonup", "buttondown"].includes(e.type) && (controlValue === e.label || controlValue === e.index)) {
+ if (
+ ["buttonup", "buttondown"].includes(e.type) &&
+ (controlValue === e.label || controlValue === e.index)
+ ) {
const inputValue = isAnalog ? 0x7fff : 1;
const isButtonUp = e.type === "buttonup";
const value = isButtonUp ? 0 : inputValue;
if (this.isAutofireEnabled(i, j) && !isAnalog) {
- isButtonUp ? this.stopAutofire(i, j) : this.startAutofire(i, j, inputValue);
+ isButtonUp
+ ? this.stopAutofire(i, j)
+ : this.startAutofire(i, j, inputValue);
} else {
this.gameManager.simulateInput(i, j, value);
}
} else if (e.type === "axischanged") {
- if (typeof controlValue === "string" && controlValue.split(":")[0] === e.axis) {
+ if (
+ typeof controlValue === "string" &&
+ controlValue.split(":")[0] === e.axis
+ ) {
if (isAnalog) {
if (j === 16 || j === 17) {
if (e.value > 0) {
- this.gameManager.simulateInput(i, 16, 0x7fff * e.value);
+ this.gameManager.simulateInput(
+ i,
+ 16,
+ 0x7fff * e.value,
+ );
this.gameManager.simulateInput(i, 17, 0);
} else {
- this.gameManager.simulateInput(i, 17, -0x7fff * e.value);
+ this.gameManager.simulateInput(
+ i,
+ 17,
+ -0x7fff * e.value,
+ );
this.gameManager.simulateInput(i, 16, 0);
}
} else if (j === 18 || j === 19) {
if (e.value > 0) {
- this.gameManager.simulateInput(i, 18, 0x7fff * e.value);
+ this.gameManager.simulateInput(
+ i,
+ 18,
+ 0x7fff * e.value,
+ );
this.gameManager.simulateInput(i, 19, 0);
} else {
- this.gameManager.simulateInput(i, 19, -0x7fff * e.value);
+ this.gameManager.simulateInput(
+ i,
+ 19,
+ -0x7fff * e.value,
+ );
this.gameManager.simulateInput(i, 18, 0);
}
} else if (j === 20 || j === 21) {
if (e.value > 0) {
- this.gameManager.simulateInput(i, 20, 0x7fff * e.value);
+ this.gameManager.simulateInput(
+ i,
+ 20,
+ 0x7fff * e.value,
+ );
this.gameManager.simulateInput(i, 21, 0);
} else {
- this.gameManager.simulateInput(i, 21, -0x7fff * e.value);
+ this.gameManager.simulateInput(
+ i,
+ 21,
+ -0x7fff * e.value,
+ );
this.gameManager.simulateInput(i, 20, 0);
}
} else if (j === 22 || j === 23) {
if (e.value > 0) {
- this.gameManager.simulateInput(i, 22, 0x7fff * e.value);
+ this.gameManager.simulateInput(
+ i,
+ 22,
+ 0x7fff * e.value,
+ );
this.gameManager.simulateInput(i, 23, 0);
} else {
- this.gameManager.simulateInput(i, 23, -0x7fff * e.value);
+ this.gameManager.simulateInput(
+ i,
+ 23,
+ -0x7fff * e.value,
+ );
this.gameManager.simulateInput(i, 22, 0);
}
}
- } else if (value === 0 || controlValue === e.label || controlValue === `${e.axis}:${value}`) {
- this.gameManager.simulateInput(i, j, ((value === 0) ? 0 : 1));
+ } else if (
+ value === 0 ||
+ controlValue === e.label ||
+ controlValue === `${e.axis}:${value}`
+ ) {
+ this.gameManager.simulateInput(
+ i,
+ j,
+ value === 0 ? 0 : 1,
+ );
}
}
}
@@ -3876,292 +4884,1645 @@ class EmulatorJS {
this.virtualGamepad = this.createElement("div");
this.toggleVirtualGamepad = (show) => {
this.virtualGamepad.style.display = show ? "" : "none";
- }
+ };
this.virtualGamepad.classList.add("ejs_virtualGamepad_parent");
this.elements.parent.appendChild(this.virtualGamepad);
const speedControlButtons = [
- { "type": "button", "text": "Fast", "id": "speed_fast", "location": "center", "left": -35, "top": 50, "fontSize": 15, "block": true, "input_value": 27 },
- { "type": "button", "text": "Slow", "id": "speed_slow", "location": "center", "left": 95, "top": 50, "fontSize": 15, "block": true, "input_value": 29 },
+ {
+ type: "button",
+ text: "Fast",
+ id: "speed_fast",
+ location: "center",
+ left: -35,
+ top: 50,
+ fontSize: 15,
+ block: true,
+ input_value: 27,
+ },
+ {
+ type: "button",
+ text: "Slow",
+ id: "speed_slow",
+ location: "center",
+ left: 95,
+ top: 50,
+ fontSize: 15,
+ block: true,
+ input_value: 29,
+ },
];
if (this.rewindEnabled) {
- speedControlButtons.push({ "type": "button", "text": "Rewind", "id": "speed_rewind", "location": "center", "left": 30, "top": 50, "fontSize": 15, "block": true, "input_value": 28 });
+ speedControlButtons.push({
+ type: "button",
+ text: "Rewind",
+ id: "speed_rewind",
+ location: "center",
+ left: 30,
+ top: 50,
+ fontSize: 15,
+ block: true,
+ input_value: 28,
+ });
}
let info;
- if (this.config.VirtualGamepadSettings && function (set) {
- if (!Array.isArray(set)) {
- if (this.debug) console.warn("Virtual gamepad settings is not array! Using default gamepad settings");
- return false;
- }
- if (!set.length) {
- if (this.debug) console.warn("Virtual gamepad settings is empty! Using default gamepad settings");
- return false;
- }
- for (let i = 0; i < set.length; i++) {
- if (!set[i].type) continue;
- try {
- if (set[i].type === "zone" || set[i].type === "dpad") {
+ if (
+ this.config.VirtualGamepadSettings &&
+ (function (set) {
+ if (!Array.isArray(set)) {
+ if (this.debug)
+ console.warn(
+ "Virtual gamepad settings is not array! Using default gamepad settings",
+ );
+ return false;
+ }
+ if (!set.length) {
+ if (this.debug)
+ console.warn(
+ "Virtual gamepad settings is empty! Using default gamepad settings",
+ );
+ return false;
+ }
+ for (let i = 0; i < set.length; i++) {
+ if (!set[i].type) continue;
+ try {
+ if (set[i].type === "zone" || set[i].type === "dpad") {
+ if (!set[i].location) {
+ console.warn(
+ "Missing location value for " +
+ set[i].type +
+ "! Using default gamepad settings",
+ );
+ return false;
+ } else if (!set[i].inputValues) {
+ console.warn(
+ "Missing inputValues for " +
+ set[i].type +
+ "! Using default gamepad settings",
+ );
+ return false;
+ }
+ continue;
+ }
if (!set[i].location) {
- console.warn("Missing location value for " + set[i].type + "! Using default gamepad settings");
+ console.warn(
+ "Missing location value for button " +
+ set[i].text +
+ "! Using default gamepad settings",
+ );
return false;
- } else if (!set[i].inputValues) {
- console.warn("Missing inputValues for " + set[i].type + "! Using default gamepad settings");
+ } else if (!set[i].type) {
+ console.warn(
+ "Missing type value for button " +
+ set[i].text +
+ "! Using default gamepad settings",
+ );
+ return false;
+ } else if (!set[i].id.toString()) {
+ console.warn(
+ "Missing id value for button " +
+ set[i].text +
+ "! Using default gamepad settings",
+ );
+ return false;
+ } else if (!set[i].input_value.toString()) {
+ console.warn(
+ "Missing input_value for button " +
+ set[i].text +
+ "! Using default gamepad settings",
+ );
return false;
}
- continue;
- }
- if (!set[i].location) {
- console.warn("Missing location value for button " + set[i].text + "! Using default gamepad settings");
- return false;
- } else if (!set[i].type) {
- console.warn("Missing type value for button " + set[i].text + "! Using default gamepad settings");
- return false;
- } else if (!set[i].id.toString()) {
- console.warn("Missing id value for button " + set[i].text + "! Using default gamepad settings");
- return false;
- } else if (!set[i].input_value.toString()) {
- console.warn("Missing input_value for button " + set[i].text + "! Using default gamepad settings");
+ } catch (e) {
+ console.warn(
+ "Error checking values! Using default gamepad settings",
+ );
return false;
}
- } catch(e) {
- console.warn("Error checking values! Using default gamepad settings");
- return false;
}
- }
- return true;
- }(this.config.VirtualGamepadSettings)) {
+ return true;
+ })(this.config.VirtualGamepadSettings)
+ ) {
info = this.config.VirtualGamepadSettings;
} else if ("gba" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "B", "id": "b", "location": "right", "left": 10, "top": 70, "bold": true, "input_value": 0 },
- { "type": "button", "text": "A", "id": "a", "location": "right", "left": 81, "top": 40, "bold": true, "input_value": 8 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "top": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Start", "id": "start", "location": "center", "left": 60, "fontSize": 15, "block": true, "input_value": 3 },
- { "type": "button", "text": "Select", "id": "select", "location": "center", "left": -5, "fontSize": 15, "block": true, "input_value": 2 },
- { "type": "button", "text": "L", "id": "l", "location": "left", "left": 3, "top": -90, "bold": true, "block": true, "input_value": 10 },
- { "type": "button", "text": "R", "id": "r", "location": "right", "right": 3, "top": -90, "bold": true, "block": true, "input_value": 11 }
- ];
- info.push(...speedControlButtons);
- } else if ("gb" === this.getControlScheme()) {
- info = [
- { "type": "button", "text": "A", "id": "a", "location": "right", "left": 81, "top": 40, "bold": true, "input_value": 8 },
- { "type": "button", "text": "B", "id": "b", "location": "right", "left": 10, "top": 70, "bold": true, "input_value": 0 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "top": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Start", "id": "start", "location": "center", "left": 60, "fontSize": 15, "block": true, "input_value": 3 },
- { "type": "button", "text": "Select", "id": "select", "location": "center", "left": -5, "fontSize": 15, "block": true, "input_value": 2 }
- ];
+ {
+ type: "button",
+ text: "B",
+ id: "b",
+ location: "right",
+ left: 10,
+ top: 70,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "button",
+ text: "A",
+ id: "a",
+ location: "right",
+ left: 81,
+ top: 40,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ top: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Start",
+ id: "start",
+ location: "center",
+ left: 60,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
+ {
+ type: "button",
+ text: "Select",
+ id: "select",
+ location: "center",
+ left: -5,
+ fontSize: 15,
+ block: true,
+ input_value: 2,
+ },
+ {
+ type: "button",
+ text: "L",
+ id: "l",
+ location: "left",
+ left: 3,
+ top: -90,
+ bold: true,
+ block: true,
+ input_value: 10,
+ },
+ {
+ type: "button",
+ text: "R",
+ id: "r",
+ location: "right",
+ right: 3,
+ top: -90,
+ bold: true,
+ block: true,
+ input_value: 11,
+ },
+ ];
+ info.push(...speedControlButtons);
+ } else if ("gb" === this.getControlScheme()) {
+ info = [
+ {
+ type: "button",
+ text: "A",
+ id: "a",
+ location: "right",
+ left: 81,
+ top: 40,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "button",
+ text: "B",
+ id: "b",
+ location: "right",
+ left: 10,
+ top: 70,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ top: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Start",
+ id: "start",
+ location: "center",
+ left: 60,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
+ {
+ type: "button",
+ text: "Select",
+ id: "select",
+ location: "center",
+ left: -5,
+ fontSize: 15,
+ block: true,
+ input_value: 2,
+ },
+ ];
info.push(...speedControlButtons);
} else if ("nes" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "B", "id": "b", "location": "right", "right": 75, "top": 70, "bold": true, "input_value": 0 },
- { "type": "button", "text": "A", "id": "a", "location": "right", "right": 5, "top": 70, "bold": true, "input_value": 8 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Start", "id": "start", "location": "center", "left": 60, "fontSize": 15, "block": true, "input_value": 3 },
- { "type": "button", "text": "Select", "id": "select", "location": "center", "left": -5, "fontSize": 15, "block": true, "input_value": 2 }
+ {
+ type: "button",
+ text: "B",
+ id: "b",
+ location: "right",
+ right: 75,
+ top: 70,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "button",
+ text: "A",
+ id: "a",
+ location: "right",
+ right: 5,
+ top: 70,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Start",
+ id: "start",
+ location: "center",
+ left: 60,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
+ {
+ type: "button",
+ text: "Select",
+ id: "select",
+ location: "center",
+ left: -5,
+ fontSize: 15,
+ block: true,
+ input_value: 2,
+ },
];
info.push(...speedControlButtons);
} else if ("n64" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "B", "id": "b", "location": "right", "left": -10, "top": 95, "input_value": 1, "bold": true },
- { "type": "button", "text": "A", "id": "a", "location": "right", "left": 40, "top": 150, "input_value": 0, "bold": true },
- { "type": "zone", "id": "stick", "location": "left", "left": "50%", "top": "100%", "joystickInput": true, "inputValues": [16, 17, 18, 19] },
- { "type": "zone", "id": "dpad", "location": "left", "left": "50%", "top": "0%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Start", "id": "start", "location": "center", "left": 30, "top": -10, "fontSize": 15, "block": true, "input_value": 3 },
- { "type": "button", "text": "L", "id": "l", "block": true, "location": "top", "left": 10, "top": -40, "bold": true, "input_value": 10 },
- { "type": "button", "text": "R", "id": "r", "block": true, "location": "top", "right": 10, "top": -40, "bold": true, "input_value": 11 },
- { "type": "button", "text": "Z", "id": "z", "block": true, "location": "top", "left": 10, "bold": true, "input_value": 12 },
- { "fontSize": 20, "type": "button", "text": "CU", "id": "cu", "joystickInput": true, "location": "right", "left": 25, "top": -65, "input_value": 23 },
- { "fontSize": 20, "type": "button", "text": "CD", "id": "cd", "joystickInput": true, "location": "right", "left": 25, "top": 15, "input_value": 22 },
- { "fontSize": 20, "type": "button", "text": "CL", "id": "cl", "joystickInput": true, "location": "right", "left": -15, "top": -25, "input_value": 21 },
- { "fontSize": 20, "type": "button", "text": "CR", "id": "cr", "joystickInput": true, "location": "right", "left": 65, "top": -25, "input_value": 20 }
+ {
+ type: "button",
+ text: "B",
+ id: "b",
+ location: "right",
+ left: -10,
+ top: 95,
+ input_value: 1,
+ bold: true,
+ },
+ {
+ type: "button",
+ text: "A",
+ id: "a",
+ location: "right",
+ left: 40,
+ top: 150,
+ input_value: 0,
+ bold: true,
+ },
+ {
+ type: "zone",
+ id: "stick",
+ location: "left",
+ left: "50%",
+ top: "100%",
+ joystickInput: true,
+ inputValues: [16, 17, 18, 19],
+ },
+ {
+ type: "zone",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ top: "0%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Start",
+ id: "start",
+ location: "center",
+ left: 30,
+ top: -10,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
+ {
+ type: "button",
+ text: "L",
+ id: "l",
+ block: true,
+ location: "top",
+ left: 10,
+ top: -40,
+ bold: true,
+ input_value: 10,
+ },
+ {
+ type: "button",
+ text: "R",
+ id: "r",
+ block: true,
+ location: "top",
+ right: 10,
+ top: -40,
+ bold: true,
+ input_value: 11,
+ },
+ {
+ type: "button",
+ text: "Z",
+ id: "z",
+ block: true,
+ location: "top",
+ left: 10,
+ bold: true,
+ input_value: 12,
+ },
+ {
+ fontSize: 20,
+ type: "button",
+ text: "CU",
+ id: "cu",
+ joystickInput: true,
+ location: "right",
+ left: 25,
+ top: -65,
+ input_value: 23,
+ },
+ {
+ fontSize: 20,
+ type: "button",
+ text: "CD",
+ id: "cd",
+ joystickInput: true,
+ location: "right",
+ left: 25,
+ top: 15,
+ input_value: 22,
+ },
+ {
+ fontSize: 20,
+ type: "button",
+ text: "CL",
+ id: "cl",
+ joystickInput: true,
+ location: "right",
+ left: -15,
+ top: -25,
+ input_value: 21,
+ },
+ {
+ fontSize: 20,
+ type: "button",
+ text: "CR",
+ id: "cr",
+ joystickInput: true,
+ location: "right",
+ left: 65,
+ top: -25,
+ input_value: 20,
+ },
];
info.push(...speedControlButtons);
} else if ("nds" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "X", "id": "x", "location": "right", "left": 40, "bold": true, "input_value": 9 },
- { "type": "button", "text": "Y", "id": "y", "location": "right", "top": 40, "bold": true, "input_value": 1 },
- { "type": "button", "text": "A", "id": "a", "location": "right", "left": 81, "top": 40, "bold": true, "input_value": 8 },
- { "type": "button", "text": "B", "id": "b", "location": "right", "left": 40, "top": 80, "bold": true, "input_value": 0 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "top": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Start", "id": "start", "location": "center", "left": 60, "fontSize": 15, "block": true, "input_value": 3 },
- { "type": "button", "text": "Select", "id": "select", "location": "center", "left": -5, "fontSize": 15, "block": true, "input_value": 2 },
- { "type": "button", "text": "L", "id": "l", "location": "left", "left": 3, "top": -100, "bold": true, "block": true, "input_value": 10 },
- { "type": "button", "text": "R", "id": "r", "location": "right", "right": 3, "top": -100, "bold": true, "block": true, "input_value": 11 }
+ {
+ type: "button",
+ text: "X",
+ id: "x",
+ location: "right",
+ left: 40,
+ bold: true,
+ input_value: 9,
+ },
+ {
+ type: "button",
+ text: "Y",
+ id: "y",
+ location: "right",
+ top: 40,
+ bold: true,
+ input_value: 1,
+ },
+ {
+ type: "button",
+ text: "A",
+ id: "a",
+ location: "right",
+ left: 81,
+ top: 40,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "button",
+ text: "B",
+ id: "b",
+ location: "right",
+ left: 40,
+ top: 80,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ top: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Start",
+ id: "start",
+ location: "center",
+ left: 60,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
+ {
+ type: "button",
+ text: "Select",
+ id: "select",
+ location: "center",
+ left: -5,
+ fontSize: 15,
+ block: true,
+ input_value: 2,
+ },
+ {
+ type: "button",
+ text: "L",
+ id: "l",
+ location: "left",
+ left: 3,
+ top: -100,
+ bold: true,
+ block: true,
+ input_value: 10,
+ },
+ {
+ type: "button",
+ text: "R",
+ id: "r",
+ location: "right",
+ right: 3,
+ top: -100,
+ bold: true,
+ block: true,
+ input_value: 11,
+ },
];
info.push(...speedControlButtons);
} else if ("snes" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "X", "id": "x", "location": "right", "left": 40, "bold": true, "input_value": 9 },
- { "type": "button", "text": "Y", "id": "y", "location": "right", "top": 40, "bold": true, "input_value": 1 },
- { "type": "button", "text": "A", "id": "a", "location": "right", "left": 81, "top": 40, "bold": true, "input_value": 8 },
- { "type": "button", "text": "B", "id": "b", "location": "right", "left": 40, "top": 80, "bold": true, "input_value": 0 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "top": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Start", "id": "start", "location": "center", "left": 60, "fontSize": 15, "block": true, "input_value": 3 },
- { "type": "button", "text": "Select", "id": "select", "location": "center", "left": -5, "fontSize": 15, "block": true, "input_value": 2 },
- { "type": "button", "text": "L", "id": "l", "location": "left", "left": 3, "top": -100, "bold": true, "block": true, "input_value": 10 },
- { "type": "button", "text": "R", "id": "r", "location": "right", "right": 3, "top": -100, "bold": true, "block": true, "input_value": 11 }
+ {
+ type: "button",
+ text: "X",
+ id: "x",
+ location: "right",
+ left: 40,
+ bold: true,
+ input_value: 9,
+ },
+ {
+ type: "button",
+ text: "Y",
+ id: "y",
+ location: "right",
+ top: 40,
+ bold: true,
+ input_value: 1,
+ },
+ {
+ type: "button",
+ text: "A",
+ id: "a",
+ location: "right",
+ left: 81,
+ top: 40,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "button",
+ text: "B",
+ id: "b",
+ location: "right",
+ left: 40,
+ top: 80,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ top: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Start",
+ id: "start",
+ location: "center",
+ left: 60,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
+ {
+ type: "button",
+ text: "Select",
+ id: "select",
+ location: "center",
+ left: -5,
+ fontSize: 15,
+ block: true,
+ input_value: 2,
+ },
+ {
+ type: "button",
+ text: "L",
+ id: "l",
+ location: "left",
+ left: 3,
+ top: -100,
+ bold: true,
+ block: true,
+ input_value: 10,
+ },
+ {
+ type: "button",
+ text: "R",
+ id: "r",
+ location: "right",
+ right: 3,
+ top: -100,
+ bold: true,
+ block: true,
+ input_value: 11,
+ },
];
info.push(...speedControlButtons);
- } else if (["segaMD", "segaCD", "sega32x"].includes(this.getControlScheme())) {
+ } else if (
+ ["segaMD", "segaCD", "sega32x"].includes(this.getControlScheme())
+ ) {
info = [
- { "type": "button", "text": "A", "id": "a", "location": "right", "right": 145, "top": 70, "bold": true, "input_value": 1 },
- { "type": "button", "text": "B", "id": "b", "location": "right", "right": 75, "top": 70, "bold": true, "input_value": 0 },
- { "type": "button", "text": "C", "id": "c", "location": "right", "right": 5, "top": 70, "bold": true, "input_value": 8 },
- { "type": "button", "text": "X", "id": "x", "location": "right", "right": 145, "top": 0, "bold": true, "input_value": 10 },
- { "type": "button", "text": "Y", "id": "y", "location": "right", "right": 75, "top": 0, "bold": true, "input_value": 9 },
- { "type": "button", "text": "Z", "id": "z", "location": "right", "right": 5, "top": 0, "bold": true, "input_value": 11 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Mode", "id": "mode", "location": "center", "left": -5, "fontSize": 15, "block": true, "input_value": 2 },
- { "type": "button", "text": "Start", "id": "start", "location": "center", "left": 60, "fontSize": 15, "block": true, "input_value": 3 }
+ {
+ type: "button",
+ text: "A",
+ id: "a",
+ location: "right",
+ right: 145,
+ top: 70,
+ bold: true,
+ input_value: 1,
+ },
+ {
+ type: "button",
+ text: "B",
+ id: "b",
+ location: "right",
+ right: 75,
+ top: 70,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "button",
+ text: "C",
+ id: "c",
+ location: "right",
+ right: 5,
+ top: 70,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "button",
+ text: "X",
+ id: "x",
+ location: "right",
+ right: 145,
+ top: 0,
+ bold: true,
+ input_value: 10,
+ },
+ {
+ type: "button",
+ text: "Y",
+ id: "y",
+ location: "right",
+ right: 75,
+ top: 0,
+ bold: true,
+ input_value: 9,
+ },
+ {
+ type: "button",
+ text: "Z",
+ id: "z",
+ location: "right",
+ right: 5,
+ top: 0,
+ bold: true,
+ input_value: 11,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Mode",
+ id: "mode",
+ location: "center",
+ left: -5,
+ fontSize: 15,
+ block: true,
+ input_value: 2,
+ },
+ {
+ type: "button",
+ text: "Start",
+ id: "start",
+ location: "center",
+ left: 60,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
];
info.push(...speedControlButtons);
} else if ("segaMS" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "1", "id": "button_1", "location": "right", "left": 10, "top": 40, "bold": true, "input_value": 0 },
- { "type": "button", "text": "2", "id": "button_2", "location": "right", "left": 81, "top": 40, "bold": true, "input_value": 8 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] }
+ {
+ type: "button",
+ text: "1",
+ id: "button_1",
+ location: "right",
+ left: 10,
+ top: 40,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "button",
+ text: "2",
+ id: "button_2",
+ location: "right",
+ left: 81,
+ top: 40,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
];
info.push(...speedControlButtons);
} else if ("segaGG" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "1", "id": "button_1", "location": "right", "left": 10, "top": 70, "bold": true, "input_value": 0 },
- { "type": "button", "text": "2", "id": "button_2", "location": "right", "left": 81, "top": 40, "bold": true, "input_value": 8 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "top": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Start", "id": "start", "location": "center", "left": 30, "fontSize": 15, "block": true, "input_value": 3 }
+ {
+ type: "button",
+ text: "1",
+ id: "button_1",
+ location: "right",
+ left: 10,
+ top: 70,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "button",
+ text: "2",
+ id: "button_2",
+ location: "right",
+ left: 81,
+ top: 40,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ top: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Start",
+ id: "start",
+ location: "center",
+ left: 30,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
];
info.push(...speedControlButtons);
} else if ("segaSaturn" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "A", "id": "a", "location": "right", "right": 145, "top": 70, "bold": true, "input_value": 1 },
- { "type": "button", "text": "B", "id": "b", "location": "right", "right": 75, "top": 70, "bold": true, "input_value": 0 },
- { "type": "button", "text": "C", "id": "c", "location": "right", "right": 5, "top": 70, "bold": true, "input_value": 8 },
- { "type": "button", "text": "X", "id": "x", "location": "right", "right": 145, "top": 0, "bold": true, "input_value": 9 },
- { "type": "button", "text": "Y", "id": "y", "location": "right", "right": 75, "top": 0, "bold": true, "input_value": 10 },
- { "type": "button", "text": "Z", "id": "z", "location": "right", "right": 5, "top": 0, "bold": true, "input_value": 11 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "L", "id": "l", "location": "left", "left": 3, "top": -90, "bold": true, "block": true, "input_value": 12 },
- { "type": "button", "text": "R", "id": "r", "location": "right", "right": 3, "top": -90, "bold": true, "block": true, "input_value": 13 },
- { "type": "button", "text": "Start", "id": "start", "location": "center", "left": 30, "fontSize": 15, "block": true, "input_value": 3 }
+ {
+ type: "button",
+ text: "A",
+ id: "a",
+ location: "right",
+ right: 145,
+ top: 70,
+ bold: true,
+ input_value: 1,
+ },
+ {
+ type: "button",
+ text: "B",
+ id: "b",
+ location: "right",
+ right: 75,
+ top: 70,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "button",
+ text: "C",
+ id: "c",
+ location: "right",
+ right: 5,
+ top: 70,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "button",
+ text: "X",
+ id: "x",
+ location: "right",
+ right: 145,
+ top: 0,
+ bold: true,
+ input_value: 9,
+ },
+ {
+ type: "button",
+ text: "Y",
+ id: "y",
+ location: "right",
+ right: 75,
+ top: 0,
+ bold: true,
+ input_value: 10,
+ },
+ {
+ type: "button",
+ text: "Z",
+ id: "z",
+ location: "right",
+ right: 5,
+ top: 0,
+ bold: true,
+ input_value: 11,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "L",
+ id: "l",
+ location: "left",
+ left: 3,
+ top: -90,
+ bold: true,
+ block: true,
+ input_value: 12,
+ },
+ {
+ type: "button",
+ text: "R",
+ id: "r",
+ location: "right",
+ right: 3,
+ top: -90,
+ bold: true,
+ block: true,
+ input_value: 13,
+ },
+ {
+ type: "button",
+ text: "Start",
+ id: "start",
+ location: "center",
+ left: 30,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
];
info.push(...speedControlButtons);
} else if ("atari2600" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "", "id": "button_1", "location": "right", "right": 10, "top": 70, "bold": true, "input_value": 0 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Reset", "id": "reset", "location": "center", "left": 60, "fontSize": 15, "block": true, "input_value": 3 },
- { "type": "button", "text": "Select", "id": "select", "location": "center", "left": -5, "fontSize": 15, "block": true, "input_value": 2 }
+ {
+ type: "button",
+ text: "",
+ id: "button_1",
+ location: "right",
+ right: 10,
+ top: 70,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Reset",
+ id: "reset",
+ location: "center",
+ left: 60,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
+ {
+ type: "button",
+ text: "Select",
+ id: "select",
+ location: "center",
+ left: -5,
+ fontSize: 15,
+ block: true,
+ input_value: 2,
+ },
];
info.push(...speedControlButtons);
} else if ("atari7800" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "1", "id": "button_1", "location": "right", "right": 75, "top": 70, "bold": true, "input_value": 0 },
- { "type": "button", "text": "2", "id": "button_2", "location": "right", "right": 5, "top": 70, "bold": true, "input_value": 8 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Reset", "id": "reset", "location": "center", "left": -35, "fontSize": 15, "block": true, "input_value": 9 },
- { "type": "button", "text": "Pause", "id": "pause", "location": "center", "left": 95, "fontSize": 15, "block": true, "input_value": 3 },
- { "type": "button", "text": "Select", "id": "select", "location": "center", "left": 30, "fontSize": 15, "block": true, "input_value": 2 },
+ {
+ type: "button",
+ text: "1",
+ id: "button_1",
+ location: "right",
+ right: 75,
+ top: 70,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "button",
+ text: "2",
+ id: "button_2",
+ location: "right",
+ right: 5,
+ top: 70,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Reset",
+ id: "reset",
+ location: "center",
+ left: -35,
+ fontSize: 15,
+ block: true,
+ input_value: 9,
+ },
+ {
+ type: "button",
+ text: "Pause",
+ id: "pause",
+ location: "center",
+ left: 95,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
+ {
+ type: "button",
+ text: "Select",
+ id: "select",
+ location: "center",
+ left: 30,
+ fontSize: 15,
+ block: true,
+ input_value: 2,
+ },
];
info.push(...speedControlButtons);
} else if ("lynx" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "B", "id": "button_1", "location": "right", "right": 75, "top": 70, "bold": true, "input_value": 0 },
- { "type": "button", "text": "A", "id": "button_2", "location": "right", "right": 5, "top": 70, "bold": true, "input_value": 8 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Opt 1", "id": "option_1", "location": "center", "left": -35, "fontSize": 15, "block": true, "input_value": 10 },
- { "type": "button", "text": "Opt 2", "id": "option_2", "location": "center", "left": 95, "fontSize": 15, "block": true, "input_value": 11 },
- { "type": "button", "text": "Start", "id": "start", "location": "center", "left": 30, "fontSize": 15, "block": true, "input_value": 3 }
+ {
+ type: "button",
+ text: "B",
+ id: "button_1",
+ location: "right",
+ right: 75,
+ top: 70,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "button",
+ text: "A",
+ id: "button_2",
+ location: "right",
+ right: 5,
+ top: 70,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Opt 1",
+ id: "option_1",
+ location: "center",
+ left: -35,
+ fontSize: 15,
+ block: true,
+ input_value: 10,
+ },
+ {
+ type: "button",
+ text: "Opt 2",
+ id: "option_2",
+ location: "center",
+ left: 95,
+ fontSize: 15,
+ block: true,
+ input_value: 11,
+ },
+ {
+ type: "button",
+ text: "Start",
+ id: "start",
+ location: "center",
+ left: 30,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
];
info.push(...speedControlButtons);
} else if ("jaguar" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "A", "id": "a", "location": "right", "right": 145, "top": 70, "bold": true, "input_value": 8 },
- { "type": "button", "text": "B", "id": "b", "location": "right", "right": 75, "top": 70, "bold": true, "input_value": 0 },
- { "type": "button", "text": "C", "id": "c", "location": "right", "right": 5, "top": 70, "bold": true, "input_value": 1 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Option", "id": "option", "location": "center", "left": 60, "fontSize": 15, "block": true, "input_value": 3 },
- { "type": "button", "text": "Pause", "id": "pause", "location": "center", "left": -5, "fontSize": 15, "block": true, "input_value": 2 }
+ {
+ type: "button",
+ text: "A",
+ id: "a",
+ location: "right",
+ right: 145,
+ top: 70,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "button",
+ text: "B",
+ id: "b",
+ location: "right",
+ right: 75,
+ top: 70,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "button",
+ text: "C",
+ id: "c",
+ location: "right",
+ right: 5,
+ top: 70,
+ bold: true,
+ input_value: 1,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Option",
+ id: "option",
+ location: "center",
+ left: 60,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
+ {
+ type: "button",
+ text: "Pause",
+ id: "pause",
+ location: "center",
+ left: -5,
+ fontSize: 15,
+ block: true,
+ input_value: 2,
+ },
];
info.push(...speedControlButtons);
} else if ("vb" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "B", "id": "b", "location": "right", "right": 75, "top": 150, "bold": true, "input_value": 0 },
- { "type": "button", "text": "A", "id": "a", "location": "right", "right": 5, "top": 150, "bold": true, "input_value": 8 },
- { "type": "dpad", "id": "left_dpad", "location": "left", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "dpad", "id": "right_dpad", "location": "right", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [19, 18, 17, 16] },
- { "type": "button", "text": "L", "id": "l", "location": "left", "left": 3, "top": -90, "bold": true, "block": true, "input_value": 10 },
- { "type": "button", "text": "R", "id": "r", "location": "right", "right": 3, "top": -90, "bold": true, "block": true, "input_value": 11 },
- { "type": "button", "text": "Start", "id": "start", "location": "center", "left": 60, "fontSize": 15, "block": true, "input_value": 3 },
- { "type": "button", "text": "Select", "id": "select", "location": "center", "left": -5, "fontSize": 15, "block": true, "input_value": 2 }
+ {
+ type: "button",
+ text: "B",
+ id: "b",
+ location: "right",
+ right: 75,
+ top: 150,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "button",
+ text: "A",
+ id: "a",
+ location: "right",
+ right: 5,
+ top: 150,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "dpad",
+ id: "left_dpad",
+ location: "left",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "dpad",
+ id: "right_dpad",
+ location: "right",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [19, 18, 17, 16],
+ },
+ {
+ type: "button",
+ text: "L",
+ id: "l",
+ location: "left",
+ left: 3,
+ top: -90,
+ bold: true,
+ block: true,
+ input_value: 10,
+ },
+ {
+ type: "button",
+ text: "R",
+ id: "r",
+ location: "right",
+ right: 3,
+ top: -90,
+ bold: true,
+ block: true,
+ input_value: 11,
+ },
+ {
+ type: "button",
+ text: "Start",
+ id: "start",
+ location: "center",
+ left: 60,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
+ {
+ type: "button",
+ text: "Select",
+ id: "select",
+ location: "center",
+ left: -5,
+ fontSize: 15,
+ block: true,
+ input_value: 2,
+ },
];
info.push(...speedControlButtons);
} else if ("3do" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "A", "id": "a", "location": "right", "right": 145, "top": 70, "bold": true, "input_value": 1 },
- { "type": "button", "text": "B", "id": "b", "location": "right", "right": 75, "top": 70, "bold": true, "input_value": 0 },
- { "type": "button", "text": "C", "id": "c", "location": "right", "right": 5, "top": 70, "bold": true, "input_value": 8 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "L", "id": "l", "location": "left", "left": 3, "top": -90, "bold": true, "block": true, "input_value": 10 },
- { "type": "button", "text": "R", "id": "r", "location": "right", "right": 3, "top": -90, "bold": true, "block": true, "input_value": 11 },
- { "type": "button", "text": "X", "id": "x", "location": "center", "left": -5, "fontSize": 15, "block": true, "bold": true, "input_value": 2 },
- { "type": "button", "text": "P", "id": "p", "location": "center", "left": 60, "fontSize": 15, "block": true, "bold": true, "input_value": 3 }
+ {
+ type: "button",
+ text: "A",
+ id: "a",
+ location: "right",
+ right: 145,
+ top: 70,
+ bold: true,
+ input_value: 1,
+ },
+ {
+ type: "button",
+ text: "B",
+ id: "b",
+ location: "right",
+ right: 75,
+ top: 70,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "button",
+ text: "C",
+ id: "c",
+ location: "right",
+ right: 5,
+ top: 70,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "L",
+ id: "l",
+ location: "left",
+ left: 3,
+ top: -90,
+ bold: true,
+ block: true,
+ input_value: 10,
+ },
+ {
+ type: "button",
+ text: "R",
+ id: "r",
+ location: "right",
+ right: 3,
+ top: -90,
+ bold: true,
+ block: true,
+ input_value: 11,
+ },
+ {
+ type: "button",
+ text: "X",
+ id: "x",
+ location: "center",
+ left: -5,
+ fontSize: 15,
+ block: true,
+ bold: true,
+ input_value: 2,
+ },
+ {
+ type: "button",
+ text: "P",
+ id: "p",
+ location: "center",
+ left: 60,
+ fontSize: 15,
+ block: true,
+ bold: true,
+ input_value: 3,
+ },
];
info.push(...speedControlButtons);
} else if ("pce" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "II", "id": "ii", "location": "right", "right": 75, "top": 70, "bold": true, "input_value": 0 },
- { "type": "button", "text": "I", "id": "i", "location": "right", "right": 5, "top": 70, "bold": true, "input_value": 8 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Run", "id": "run", "location": "center", "left": 60, "fontSize": 15, "block": true, "input_value": 3 },
- { "type": "button", "text": "Select", "id": "select", "location": "center", "left": -5, "fontSize": 15, "block": true, "input_value": 2 }
+ {
+ type: "button",
+ text: "II",
+ id: "ii",
+ location: "right",
+ right: 75,
+ top: 70,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "button",
+ text: "I",
+ id: "i",
+ location: "right",
+ right: 5,
+ top: 70,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Run",
+ id: "run",
+ location: "center",
+ left: 60,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
+ {
+ type: "button",
+ text: "Select",
+ id: "select",
+ location: "center",
+ left: -5,
+ fontSize: 15,
+ block: true,
+ input_value: 2,
+ },
];
info.push(...speedControlButtons);
} else if ("ngp" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "A", "id": "a", "location": "right", "right": 75, "top": 70, "bold": true, "input_value": 0 },
- { "type": "button", "text": "B", "id": "b", "location": "right", "right": 5, "top": 50, "bold": true, "input_value": 8 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Option", "id": "option", "location": "center", "left": 30, "fontSize": 15, "block": true, "input_value": 3 }
+ {
+ type: "button",
+ text: "A",
+ id: "a",
+ location: "right",
+ right: 75,
+ top: 70,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "button",
+ text: "B",
+ id: "b",
+ location: "right",
+ right: 5,
+ top: 50,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Option",
+ id: "option",
+ location: "center",
+ left: 30,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
];
info.push(...speedControlButtons);
} else if ("ws" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "B", "id": "b", "location": "right", "right": 75, "top": 150, "bold": true, "input_value": 0 },
- { "type": "button", "text": "A", "id": "a", "location": "right", "right": 5, "top": 150, "bold": true, "input_value": 8 },
- { "type": "dpad", "id": "x_dpad", "location": "left", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "dpad", "id": "y_dpad", "location": "right", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [13, 12, 10, 11] },
- { "type": "button", "text": "Start", "id": "start", "location": "center", "left": 30, "fontSize": 15, "block": true, "input_value": 3 },
+ {
+ type: "button",
+ text: "B",
+ id: "b",
+ location: "right",
+ right: 75,
+ top: 150,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "button",
+ text: "A",
+ id: "a",
+ location: "right",
+ right: 5,
+ top: 150,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "dpad",
+ id: "x_dpad",
+ location: "left",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "dpad",
+ id: "y_dpad",
+ location: "right",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [13, 12, 10, 11],
+ },
+ {
+ type: "button",
+ text: "Start",
+ id: "start",
+ location: "center",
+ left: 30,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
];
info.push(...speedControlButtons);
} else if ("coleco" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "L", "id": "l", "location": "right", "left": 10, "top": 40, "bold": true, "input_value": 8 },
- { "type": "button", "text": "R", "id": "r", "location": "right", "left": 81, "top": 40, "bold": true, "input_value": 0 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] }
+ {
+ type: "button",
+ text: "L",
+ id: "l",
+ location: "right",
+ left: 10,
+ top: 40,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "button",
+ text: "R",
+ id: "r",
+ location: "right",
+ left: 81,
+ top: 40,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
];
info.push(...speedControlButtons);
} else if ("pcfx" === this.getControlScheme()) {
info = [
- { "type": "button", "text": "I", "id": "i", "location": "right", "right": 5, "top": 70, "bold": true, "input_value": 8 },
- { "type": "button", "text": "II", "id": "ii", "location": "right", "right": 75, "top": 70, "bold": true, "input_value": 0 },
- { "type": "button", "text": "III", "id": "iii", "location": "right", "right": 145, "top": 70, "bold": true, "input_value": 9 },
- { "type": "button", "text": "IV", "id": "iv", "location": "right", "right": 5, "top": 0, "bold": true, "input_value": 1 },
- { "type": "button", "text": "V", "id": "v", "location": "right", "right": 75, "top": 0, "bold": true, "input_value": 10 },
- { "type": "button", "text": "VI", "id": "vi", "location": "right", "right": 145, "top": 0, "bold": true, "input_value": 11 },
- { "type": "dpad", "id": "dpad", "location": "left", "left": "50%", "right": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Select", "id": "select", "location": "center", "left": -5, "fontSize": 15, "block": true, "input_value": 2 },
- { "type": "button", "text": "Run", "id": "run", "location": "center", "left": 60, "fontSize": 15, "block": true, "input_value": 3 }
+ {
+ type: "button",
+ text: "I",
+ id: "i",
+ location: "right",
+ right: 5,
+ top: 70,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "button",
+ text: "II",
+ id: "ii",
+ location: "right",
+ right: 75,
+ top: 70,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "button",
+ text: "III",
+ id: "iii",
+ location: "right",
+ right: 145,
+ top: 70,
+ bold: true,
+ input_value: 9,
+ },
+ {
+ type: "button",
+ text: "IV",
+ id: "iv",
+ location: "right",
+ right: 5,
+ top: 0,
+ bold: true,
+ input_value: 1,
+ },
+ {
+ type: "button",
+ text: "V",
+ id: "v",
+ location: "right",
+ right: 75,
+ top: 0,
+ bold: true,
+ input_value: 10,
+ },
+ {
+ type: "button",
+ text: "VI",
+ id: "vi",
+ location: "right",
+ right: 145,
+ top: 0,
+ bold: true,
+ input_value: 11,
+ },
+ {
+ type: "dpad",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ right: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Select",
+ id: "select",
+ location: "center",
+ left: -5,
+ fontSize: 15,
+ block: true,
+ input_value: 2,
+ },
+ {
+ type: "button",
+ text: "Run",
+ id: "run",
+ location: "center",
+ left: 60,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
];
info.push(...speedControlButtons);
} else {
info = [
- { "type": "button", "text": "Y", "id": "y", "location": "right", "left": 40, "bold": true, "input_value": 9 },
- { "type": "button", "text": "X", "id": "x", "location": "right", "top": 40, "bold": true, "input_value": 1 },
- { "type": "button", "text": "B", "id": "b", "location": "right", "left": 81, "top": 40, "bold": true, "input_value": 8 },
- { "type": "button", "text": "A", "id": "a", "location": "right", "left": 40, "top": 80, "bold": true, "input_value": 0 },
- { "type": "zone", "id": "dpad", "location": "left", "left": "50%", "top": "50%", "joystickInput": false, "inputValues": [4, 5, 6, 7] },
- { "type": "button", "text": "Start", "id": "start", "location": "center", "left": 60, "fontSize": 15, "block": true, "input_value": 3 },
- { "type": "button", "text": "Select", "id": "select", "location": "center", "left": -5, "fontSize": 15, "block": true, "input_value": 2 }
+ {
+ type: "button",
+ text: "Y",
+ id: "y",
+ location: "right",
+ left: 40,
+ bold: true,
+ input_value: 9,
+ },
+ {
+ type: "button",
+ text: "X",
+ id: "x",
+ location: "right",
+ top: 40,
+ bold: true,
+ input_value: 1,
+ },
+ {
+ type: "button",
+ text: "B",
+ id: "b",
+ location: "right",
+ left: 81,
+ top: 40,
+ bold: true,
+ input_value: 8,
+ },
+ {
+ type: "button",
+ text: "A",
+ id: "a",
+ location: "right",
+ left: 40,
+ top: 80,
+ bold: true,
+ input_value: 0,
+ },
+ {
+ type: "zone",
+ id: "dpad",
+ location: "left",
+ left: "50%",
+ top: "50%",
+ joystickInput: false,
+ inputValues: [4, 5, 6, 7],
+ },
+ {
+ type: "button",
+ text: "Start",
+ id: "start",
+ location: "center",
+ left: 60,
+ fontSize: 15,
+ block: true,
+ input_value: 3,
+ },
+ {
+ type: "button",
+ text: "Select",
+ id: "select",
+ location: "center",
+ left: -5,
+ fontSize: 15,
+ block: true,
+ input_value: 2,
+ },
];
info.push(...speedControlButtons);
}
@@ -4192,16 +6553,23 @@ class EmulatorJS {
right.classList.toggle("ejs_virtualGamepad_right", !enabled);
left.classList.toggle("ejs_virtualGamepad_right", enabled);
right.classList.toggle("ejs_virtualGamepad_left", enabled);
- }
+ };
const leftHandedMode = false;
- const blockCSS = "height:31px;text-align:center;border:1px solid #ccc;border-radius:5px;line-height:31px;";
- const controlSchemeCls = `cs_${this.getControlScheme()}`.split(/\s/g).join("_");
+ const blockCSS =
+ "height:31px;text-align:center;border:1px solid #ccc;border-radius:5px;line-height:31px;";
+ const controlSchemeCls = `cs_${this.getControlScheme()}`
+ .split(/\s/g)
+ .join("_");
for (let i = 0; i < info.length; i++) {
if (info[i].type !== "button") continue;
- if (leftHandedMode && ["left", "right"].includes(info[i].location)) {
- info[i].location = (info[i].location === "left") ? "right" : "left";
+ if (
+ leftHandedMode &&
+ ["left", "right"].includes(info[i].location)
+ ) {
+ info[i].location =
+ info[i].location === "left" ? "right" : "left";
const amnt = JSON.parse(JSON.stringify(info[i]));
if (amnt.left) {
info[i].right = amnt.left;
@@ -4212,13 +6580,25 @@ class EmulatorJS {
}
let style = "";
if (info[i].left) {
- style += "left:" + info[i].left + (typeof info[i].left === "number" ? "px" : "") + ";";
+ style +=
+ "left:" +
+ info[i].left +
+ (typeof info[i].left === "number" ? "px" : "") +
+ ";";
}
if (info[i].right) {
- style += "right:" + info[i].right + (typeof info[i].right === "number" ? "px" : "") + ";";
+ style +=
+ "right:" +
+ info[i].right +
+ (typeof info[i].right === "number" ? "px" : "") +
+ ";";
}
if (info[i].top) {
- style += "top:" + info[i].top + (typeof info[i].top === "number" ? "px" : "") + ";";
+ style +=
+ "top:" +
+ info[i].top +
+ (typeof info[i].top === "number" ? "px" : "") +
+ ";";
}
if (!info[i].bold) {
style += "font-weight:normal;";
@@ -4234,30 +6614,41 @@ class EmulatorJS {
const button = this.createElement("div");
button.style = style;
button.innerText = info[i].text;
- button.classList.add("ejs_virtualGamepad_button", controlSchemeCls);
+ button.classList.add(
+ "ejs_virtualGamepad_button",
+ controlSchemeCls,
+ );
if (info[i].id) {
button.classList.add(`b_${info[i].id}`);
}
elems[info[i].location].appendChild(button);
const value = info[i].input_new_cores || info[i].input_value;
let downValue = info[i].joystickInput === true ? 0x7fff : 1;
- this.addEventListener(button, "touchstart touchend touchcancel", (e) => {
- e.preventDefault();
- const isAnalog = this.analogAxes.includes(value);
- if (e.type === "touchend" || e.type === "touchcancel") {
- e.target.classList.remove("ejs_virtualGamepad_button_down");
- window.setTimeout(() => {
- this.stopAutofire(0, value);
- this.gameManager.simulateInput(0, value, 0);
- })
- } else {
- e.target.classList.add("ejs_virtualGamepad_button_down");
- this.gameManager.simulateInput(0, value, downValue);
- if (this.isAutofireEnabled(0, value) && !isAnalog) {
- this.startAutofire(0, value, downValue);
+ this.addEventListener(
+ button,
+ "touchstart touchend touchcancel",
+ (e) => {
+ e.preventDefault();
+ const isAnalog = this.analogAxes.includes(value);
+ if (e.type === "touchend" || e.type === "touchcancel") {
+ e.target.classList.remove(
+ "ejs_virtualGamepad_button_down",
+ );
+ window.setTimeout(() => {
+ this.stopAutofire(0, value);
+ this.gameManager.simulateInput(0, value, 0);
+ });
+ } else {
+ e.target.classList.add(
+ "ejs_virtualGamepad_button_down",
+ );
+ this.gameManager.simulateInput(0, value, downValue);
+ if (this.isAutofireEnabled(0, value) && !isAnalog) {
+ this.startAutofire(0, value, downValue);
+ }
}
- }
- })
+ },
+ );
}
}
@@ -4303,21 +6694,27 @@ class EmulatorJS {
if (x >= 10) {
right = 1;
left = 0;
- if (angle < 0 && angle >= -35 || angle > 0 && angle <= 35) {
+ if (
+ (angle < 0 && angle >= -35) ||
+ (angle > 0 && angle <= 35)
+ ) {
right = 0;
}
- up = (angle < 0 && angle >= -55 ? 1 : 0);
- down = (angle > 0 && angle <= 55 ? 1 : 0);
+ up = angle < 0 && angle >= -55 ? 1 : 0;
+ down = angle > 0 && angle <= 55 ? 1 : 0;
}
if (x <= -10) {
right = 0;
left = 1;
- if (angle < 0 && angle >= -35 || angle > 0 && angle <= 35) {
+ if (
+ (angle < 0 && angle >= -35) ||
+ (angle > 0 && angle <= 35)
+ ) {
left = 0;
}
- up = (angle > 0 && angle <= 55 ? 1 : 0);
- down = (angle < 0 && angle >= -55 ? 1 : 0);
+ up = angle > 0 && angle <= 55 ? 1 : 0;
+ down = angle < 0 && angle >= -55 ? 1 : 0;
}
dpadMain.classList.toggle("ejs_dpad_up_pressed", up);
@@ -4326,7 +6723,7 @@ class EmulatorJS {
dpadMain.classList.toggle("ejs_dpad_left_pressed", left);
callback(up, down, left, right);
- }
+ };
const cancelCb = (e) => {
e.preventDefault();
dpadMain.classList.remove("ejs_dpad_up_pressed");
@@ -4335,19 +6732,18 @@ class EmulatorJS {
dpadMain.classList.remove("ejs_dpad_left_pressed");
callback(0, 0, 0, 0);
- }
+ };
this.addEventListener(dpadMain, "touchstart touchmove", updateCb);
this.addEventListener(dpadMain, "touchend touchcancel", cancelCb);
-
container.appendChild(dpadMain);
- }
+ };
info.forEach((dpad, index) => {
if (dpad.type !== "dpad") return;
if (leftHandedMode && ["left", "right"].includes(dpad.location)) {
- dpad.location = (dpad.location === "left") ? "right" : "left";
+ dpad.location = dpad.location === "left" ? "right" : "left";
const amnt = JSON.parse(JSON.stringify(dpad));
if (amnt.left) {
dpad.right = amnt.left;
@@ -4383,17 +6779,29 @@ class EmulatorJS {
if (right === 1) right = 0x7fff;
}
this.gameManager.simulateInput(0, dpad.inputValues[0], up);
- this.gameManager.simulateInput(0, dpad.inputValues[1], down);
- this.gameManager.simulateInput(0, dpad.inputValues[2], left);
- this.gameManager.simulateInput(0, dpad.inputValues[3], right);
- }
+ this.gameManager.simulateInput(
+ 0,
+ dpad.inputValues[1],
+ down,
+ );
+ this.gameManager.simulateInput(
+ 0,
+ dpad.inputValues[2],
+ left,
+ );
+ this.gameManager.simulateInput(
+ 0,
+ dpad.inputValues[3],
+ right,
+ );
+ },
});
- })
+ });
info.forEach((zone, index) => {
if (zone.type !== "zone") return;
if (leftHandedMode && ["left", "right"].includes(zone.location)) {
- zone.location = (zone.location === "left") ? "right" : "left";
+ zone.location = zone.location === "left" ? "right" : "left";
const amnt = JSON.parse(JSON.stringify(zone));
if (amnt.left) {
zone.right = amnt.left;
@@ -4403,22 +6811,26 @@ class EmulatorJS {
}
}
const elem = this.createElement("div");
- this.addEventListener(elem, "touchstart touchmove touchend touchcancel", (e) => {
- e.preventDefault();
- });
+ this.addEventListener(
+ elem,
+ "touchstart touchmove touchend touchcancel",
+ (e) => {
+ e.preventDefault();
+ },
+ );
elem.classList.add(controlSchemeCls);
if (zone.id) {
elem.classList.add(`b_${zone.id}`);
}
elems[zone.location].appendChild(elem);
const zoneObj = nipplejs.create({
- "zone": elem,
- "mode": "static",
- "position": {
- "left": zone.left,
- "top": zone.top
+ zone: elem,
+ mode: "static",
+ position: {
+ left: zone.left,
+ top: zone.top,
},
- "color": zone.color || "red"
+ color: zone.color || "red",
});
zoneObj.on("end", () => {
this.gameManager.simulateInput(0, zone.inputValues[0], 0);
@@ -4430,112 +6842,206 @@ class EmulatorJS {
const degree = info.angle.degree;
const distance = info.distance;
if (zone.joystickInput === true) {
- let x = 0, y = 0;
+ let x = 0,
+ y = 0;
if (degree > 0 && degree <= 45) {
x = distance / 50;
- y = -0.022222222222222223 * degree * distance / 50;
+ y = (-0.022222222222222223 * degree * distance) / 50;
}
if (degree > 45 && degree <= 90) {
- x = 0.022222222222222223 * (90 - degree) * distance / 50;
+ x =
+ (0.022222222222222223 * (90 - degree) * distance) /
+ 50;
y = -distance / 50;
}
if (degree > 90 && degree <= 135) {
- x = 0.022222222222222223 * (90 - degree) * distance / 50;
+ x =
+ (0.022222222222222223 * (90 - degree) * distance) /
+ 50;
y = -distance / 50;
}
if (degree > 135 && degree <= 180) {
x = -distance / 50;
- y = -0.022222222222222223 * (180 - degree) * distance / 50;
+ y =
+ (-0.022222222222222223 *
+ (180 - degree) *
+ distance) /
+ 50;
}
if (degree > 135 && degree <= 225) {
x = -distance / 50;
- y = -0.022222222222222223 * (180 - degree) * distance / 50;
+ y =
+ (-0.022222222222222223 *
+ (180 - degree) *
+ distance) /
+ 50;
}
if (degree > 225 && degree <= 270) {
- x = -0.022222222222222223 * (270 - degree) * distance / 50;
+ x =
+ (-0.022222222222222223 *
+ (270 - degree) *
+ distance) /
+ 50;
y = distance / 50;
}
if (degree > 270 && degree <= 315) {
- x = -0.022222222222222223 * (270 - degree) * distance / 50;
+ x =
+ (-0.022222222222222223 *
+ (270 - degree) *
+ distance) /
+ 50;
y = distance / 50;
}
if (degree > 315 && degree <= 359.9) {
x = distance / 50;
- y = 0.022222222222222223 * (360 - degree) * distance / 50;
+ y =
+ (0.022222222222222223 * (360 - degree) * distance) /
+ 50;
}
if (x > 0) {
- this.gameManager.simulateInput(0, zone.inputValues[0], 0x7fff * x);
- this.gameManager.simulateInput(0, zone.inputValues[1], 0);
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[0],
+ 0x7fff * x,
+ );
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[1],
+ 0,
+ );
} else {
- this.gameManager.simulateInput(0, zone.inputValues[1], 0x7fff * -x);
- this.gameManager.simulateInput(0, zone.inputValues[0], 0);
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[1],
+ 0x7fff * -x,
+ );
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[0],
+ 0,
+ );
}
if (y > 0) {
- this.gameManager.simulateInput(0, zone.inputValues[2], 0x7fff * y);
- this.gameManager.simulateInput(0, zone.inputValues[3], 0);
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[2],
+ 0x7fff * y,
+ );
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[3],
+ 0,
+ );
} else {
- this.gameManager.simulateInput(0, zone.inputValues[3], 0x7fff * -y);
- this.gameManager.simulateInput(0, zone.inputValues[2], 0);
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[3],
+ 0x7fff * -y,
+ );
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[2],
+ 0,
+ );
}
-
} else {
if (degree >= 30 && degree < 150) {
- this.gameManager.simulateInput(0, zone.inputValues[0], 1);
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[0],
+ 1,
+ );
} else {
window.setTimeout(() => {
- this.gameManager.simulateInput(0, zone.inputValues[0], 0);
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[0],
+ 0,
+ );
}, 30);
}
if (degree >= 210 && degree < 330) {
- this.gameManager.simulateInput(0, zone.inputValues[1], 1);
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[1],
+ 1,
+ );
} else {
window.setTimeout(() => {
- this.gameManager.simulateInput(0, zone.inputValues[1], 0);
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[1],
+ 0,
+ );
}, 30);
}
if (degree >= 120 && degree < 240) {
- this.gameManager.simulateInput(0, zone.inputValues[2], 1);
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[2],
+ 1,
+ );
} else {
window.setTimeout(() => {
- this.gameManager.simulateInput(0, zone.inputValues[2], 0);
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[2],
+ 0,
+ );
}, 30);
}
- if (degree >= 300 || degree >= 0 && degree < 60) {
- this.gameManager.simulateInput(0, zone.inputValues[3], 1);
+ if (degree >= 300 || (degree >= 0 && degree < 60)) {
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[3],
+ 1,
+ );
} else {
window.setTimeout(() => {
- this.gameManager.simulateInput(0, zone.inputValues[3], 0);
+ this.gameManager.simulateInput(
+ 0,
+ zone.inputValues[3],
+ 0,
+ );
}, 30);
}
}
});
- })
+ });
if (this.touch || this.hasTouchScreen) {
const menuButton = this.createElement("div");
- menuButton.innerHTML = '';
+ menuButton.innerHTML =
+ '';
menuButton.classList.add("ejs_virtualGamepad_open");
menuButton.style.display = "none";
this.on("start", () => {
menuButton.style.display = "";
- if (matchMedia('(pointer:fine)').matches && this.getSettingValue("menu-bar-button") !== "visible") {
+ if (
+ matchMedia("(pointer:fine)").matches &&
+ this.getSettingValue("menu-bar-button") !== "visible"
+ ) {
menuButton.style.opacity = 0;
- this.changeSettingOption('menu-bar-button', 'hidden', true);
+ this.changeSettingOption("menu-bar-button", "hidden", true);
}
});
this.elements.parent.appendChild(menuButton);
let timeout;
let ready = true;
- this.addEventListener(menuButton, "touchstart touchend mousedown mouseup click", (e) => {
- if (!ready) return;
- clearTimeout(timeout);
- timeout = setTimeout(() => {
- ready = true;
- }, 2000)
- ready = false;
- e.preventDefault();
- this.menu.toggle();
- })
+ this.addEventListener(
+ menuButton,
+ "touchstart touchend mousedown mouseup click",
+ (e) => {
+ if (!ready) return;
+ clearTimeout(timeout);
+ timeout = setTimeout(() => {
+ ready = true;
+ }, 2000);
+ ready = false;
+ e.preventDefault();
+ this.menu.toggle();
+ },
+ );
this.elements.menuToggle = menuButton;
}
@@ -4549,13 +7055,19 @@ class EmulatorJS {
setTimeout(() => {
this.virtualGamepad.style.display = "none";
this.virtualGamepad.style.opacity = "";
- }, 250)
+ }, 250);
}
}
const positionInfo = this.elements.parent.getBoundingClientRect();
- this.game.parentElement.classList.toggle("ejs_small_screen", positionInfo.width <= 575);
+ this.game.parentElement.classList.toggle(
+ "ejs_small_screen",
+ positionInfo.width <= 575,
+ );
//This wouldnt work using :not()... strange.
- this.game.parentElement.classList.toggle("ejs_big_screen", positionInfo.width > 575);
+ this.game.parentElement.classList.toggle(
+ "ejs_big_screen",
+ positionInfo.width > 575,
+ );
if (!this.handleSettingsResize) return;
this.handleSettingsResize();
@@ -4569,35 +7081,48 @@ class EmulatorJS {
const res = elem.getBoundingClientRect();
elem.remove();
return {
- "width": res.width,
- "height": res.height
+ width: res.width,
+ height: res.height,
};
}
saveSettings() {
- if (!window.localStorage || this.config.disableLocalStorage || !this.settingsLoaded) return;
+ if (
+ !window.localStorage ||
+ this.config.disableLocalStorage ||
+ !this.settingsLoaded
+ )
+ return;
if (!this.started && !this.failedToStart) return;
const coreSpecific = {
controlSettings: this.controls,
settings: this.settings,
- cheats: this.cheats
- }
+ cheats: this.cheats,
+ };
const ejs_settings = {
volume: this.volume,
- muted: this.muted
- }
+ muted: this.muted,
+ };
localStorage.setItem("ejs-settings", JSON.stringify(ejs_settings));
- localStorage.setItem(this.getLocalStorageKey(), JSON.stringify(coreSpecific));
+ localStorage.setItem(
+ this.getLocalStorageKey(),
+ JSON.stringify(coreSpecific),
+ );
}
getLocalStorageKey() {
let identifier = (this.config.gameId || 1) + "-" + this.getCore(true);
if (typeof this.config.gameName === "string") {
identifier += "-" + this.config.gameName;
- } else if (typeof this.config.gameUrl === "string" && !this.config.gameUrl.toLowerCase().startsWith("blob:")) {
+ } else if (
+ typeof this.config.gameUrl === "string" &&
+ !this.config.gameUrl.toLowerCase().startsWith("blob:")
+ ) {
identifier += "-" + this.config.gameUrl;
} else if (this.config.gameUrl instanceof File) {
identifier += "-" + this.config.gameUrl.name;
} else if (typeof this.config.gameId !== "number") {
- console.warn("gameId (EJS_gameID) is not set. This may result in settings persisting across games.");
+ console.warn(
+ "gameId (EJS_gameID) is not set. This may result in settings persisting across games.",
+ );
}
return "ejs-" + identifier + "-settings";
}
@@ -4609,7 +7134,7 @@ class EmulatorJS {
if (coreSpecific && coreSpecific.settings) {
return coreSpecific.settings[setting];
}
- } catch(e) {
+ } catch (e) {
console.warn("Could not load previous settings", e);
}
}
@@ -4623,30 +7148,37 @@ class EmulatorJS {
if (this.config.defaultOptions) {
let rv = "";
for (const k in this.config.defaultOptions) {
- let value = isNaN(this.config.defaultOptions[k]) ? `"${this.config.defaultOptions[k]}"` : this.config.defaultOptions[k];
+ let value = isNaN(this.config.defaultOptions[k])
+ ? `"${this.config.defaultOptions[k]}"`
+ : this.config.defaultOptions[k];
rv += `${k} = ${value}\n`;
}
return rv;
}
return "";
- };
+ }
let coreSpecific = localStorage.getItem(this.getLocalStorageKey());
if (coreSpecific) {
try {
coreSpecific = JSON.parse(coreSpecific);
- if (!(coreSpecific.settings instanceof Object)) throw new Error("Not a JSON object");
+ if (!(coreSpecific.settings instanceof Object))
+ throw new Error("Not a JSON object");
let rv = "";
for (const k in coreSpecific.settings) {
- let value = isNaN(coreSpecific.settings[k]) ? `"${coreSpecific.settings[k]}"` : coreSpecific.settings[k];
+ let value = isNaN(coreSpecific.settings[k])
+ ? `"${coreSpecific.settings[k]}"`
+ : coreSpecific.settings[k];
rv += `${k} = ${value}\n`;
}
for (const k in this.config.defaultOptions) {
if (rv.includes(k)) continue;
- let value = isNaN(this.config.defaultOptions[k]) ? `"${this.config.defaultOptions[k]}"` : this.config.defaultOptions[k];
+ let value = isNaN(this.config.defaultOptions[k])
+ ? `"${this.config.defaultOptions[k]}"`
+ : this.config.defaultOptions[k];
rv += `${k} = ${value}\n`;
}
return rv;
- } catch(e) {
+ } catch (e) {
console.warn("Could not load previous settings", e);
}
}
@@ -4660,7 +7192,12 @@ class EmulatorJS {
if (coreSpecific) {
try {
coreSpecific = JSON.parse(coreSpecific);
- if (!(coreSpecific.controlSettings instanceof Object) || !(coreSpecific.settings instanceof Object) || !Array.isArray(coreSpecific.cheats)) return;
+ if (
+ !(coreSpecific.controlSettings instanceof Object) ||
+ !(coreSpecific.settings instanceof Object) ||
+ !Array.isArray(coreSpecific.cheats)
+ )
+ return;
this.controls = coreSpecific.controlSettings;
this.checkGamepadInputs();
for (const k in coreSpecific.settings) {
@@ -4670,7 +7207,10 @@ class EmulatorJS {
const cheat = coreSpecific.cheats[i];
let includes = false;
for (let j = 0; j < this.cheats.length; j++) {
- if (this.cheats[j].desc === cheat.desc && this.cheats[j].code === cheat.code) {
+ if (
+ this.cheats[j].desc === cheat.desc &&
+ this.cheats[j].code === cheat.code
+ ) {
this.cheats[j].checked = cheat.checked;
includes = true;
break;
@@ -4679,19 +7219,22 @@ class EmulatorJS {
if (includes) continue;
this.cheats.push(cheat);
}
-
- } catch(e) {
+ } catch (e) {
console.warn("Could not load previous settings", e);
}
}
if (ejs_settings) {
try {
ejs_settings = JSON.parse(ejs_settings);
- if (typeof ejs_settings.volume !== "number" || typeof ejs_settings.muted !== "boolean") return;
+ if (
+ typeof ejs_settings.volume !== "number" ||
+ typeof ejs_settings.muted !== "boolean"
+ )
+ return;
this.volume = ejs_settings.volume;
this.muted = ejs_settings.muted;
this.setVolume(this.muted ? 0 : this.volume);
- } catch(e) {
+ } catch (e) {
console.warn("Could not load previous settings", e);
}
}
@@ -4705,7 +7248,8 @@ class EmulatorJS {
this.toggleVirtualGamepad(value !== "disabled");
} else if (option === "menu-bar-button") {
this.elements.menuToggle.style.display = "";
- this.elements.menuToggle.style.opacity = value === "visible" ? 0.5 : 0;
+ this.elements.menuToggle.style.opacity =
+ value === "visible" ? 0.5 : 0;
} else if (option === "virtual-gamepad-left-handed-mode") {
this.toggleVirtualGamepadLeftHanded(value !== "disabled");
} else if (option === "ff-ratio") {
@@ -4717,7 +7261,7 @@ class EmulatorJS {
}
setTimeout(() => {
if (this.isFastForward) this.gameManager.toggleFastForward(1);
- }, 10)
+ }, 10);
} else if (option === "fastForward") {
if (value === "enabled") {
this.isFastForward = true;
@@ -4755,7 +7299,10 @@ class EmulatorJS {
this.gameManager.setVideoRotation(0);
this.videoRotationChanged = true;
}
- } else if (option === "save-save-interval" && !this.config.fixedSaveInterval) {
+ } else if (
+ option === "save-save-interval" &&
+ !this.config.fixedSaveInterval
+ ) {
value = parseInt(value);
this.startSaveInterval(value * 1000);
} else if (option === "menubarBehavior") {
@@ -4765,7 +7312,7 @@ class EmulatorJS {
} else if (option === "altKeyboardInput") {
this.gameManager.setAltKeyEnabled(value === "enabled");
} else if (option === "lockMouse") {
- this.enableMouseLock = (value === "enabled");
+ this.enableMouseLock = value === "enabled";
} else if (option === "autofireInterval") {
this.defaultAutoFireInterval = parseInt(value);
}
@@ -4797,31 +7344,38 @@ class EmulatorJS {
needChange = true;
}
let height = this.elements.parent.getBoundingClientRect().height;
- let w2 = this.diskParent.parentElement.getBoundingClientRect().width;
+ let w2 =
+ this.diskParent.parentElement.getBoundingClientRect().width;
let disksX = this.diskParent.getBoundingClientRect().x;
- if (w2 > window.innerWidth) disksX += (w2 - window.innerWidth);
+ if (w2 > window.innerWidth) disksX += w2 - window.innerWidth;
const onTheRight = disksX > (w2 - 15) / 2;
if (height > 375) height = 375;
- home.style["max-height"] = (height - 95) + "px";
- nested.style["max-height"] = (height - 95) + "px";
+ home.style["max-height"] = height - 95 + "px";
+ nested.style["max-height"] = height - 95 + "px";
for (let i = 0; i < menus.length; i++) {
- menus[i].style["max-height"] = (height - 95) + "px";
+ menus[i].style["max-height"] = height - 95 + "px";
}
- this.disksMenu.classList.toggle("ejs_settings_center_left", !onTheRight);
- this.disksMenu.classList.toggle("ejs_settings_center_right", onTheRight);
+ this.disksMenu.classList.toggle(
+ "ejs_settings_center_left",
+ !onTheRight,
+ );
+ this.disksMenu.classList.toggle(
+ "ejs_settings_center_right",
+ onTheRight,
+ );
if (needChange) {
this.disksMenu.style.display = "none";
this.disksMenu.style.opacity = "";
}
- }
+ };
home.classList.add("ejs_setting_menu");
nested.appendChild(home);
let funcs = [];
this.changeDiskOption = (title, newValue) => {
this.disks[title] = newValue;
- funcs.forEach(e => e(title));
- }
+ funcs.forEach((e) => e(title));
+ };
let allOpts = {};
// TODO - Why is this duplicated?
@@ -4841,11 +7395,11 @@ class EmulatorJS {
const button = this.createElement("button");
const goToHome = () => {
const homeSize = this.getElementSize(home);
- nested.style.width = (homeSize.width + 20) + "px";
+ nested.style.width = homeSize.width + 20 + "px";
nested.style.height = homeSize.height + "px";
menu.setAttribute("hidden", "");
home.removeAttribute("hidden");
- }
+ };
this.addEventListener(button, "click", goToHome);
button.type = "button";
@@ -4872,7 +7426,10 @@ class EmulatorJS {
funcs.push((title) => {
if (id !== title) return;
for (let j = 0; j < buttons.length; j++) {
- buttons[j].classList.toggle("ejs_option_row_selected", buttons[j].getAttribute("ejs_value") === this.disks[id]);
+ buttons[j].classList.toggle(
+ "ejs_option_row_selected",
+ buttons[j].getAttribute("ejs_value") === this.disks[id],
+ );
}
this.menuOptionChanged(id, this.disks[id]);
current.innerText = opts[this.disks[id]];
@@ -4896,7 +7453,7 @@ class EmulatorJS {
this.menuOptionChanged(id, opt);
current.innerText = opts[opt];
goToHome();
- })
+ });
if (defaultOption === opt) {
optionButton.classList.add("ejs_option_row_selected");
this.menuOptionChanged(id, opt);
@@ -4913,14 +7470,16 @@ class EmulatorJS {
home.appendChild(optionsMenu);
nested.appendChild(menu);
- }
+ };
if (this.gameManager.getDiskCount() > 1) {
const diskLabels = {};
let isM3U = false;
let disks = {};
if (this.fileName.split(".").pop() === "m3u") {
- disks = this.gameManager.Module.FS.readFile(this.fileName, { encoding: "utf8" }).split("\n");
+ disks = this.gameManager.Module.FS.readFile(this.fileName, {
+ encoding: "utf8",
+ }).split("\n");
isM3U = true;
}
for (let i = 0; i < this.gameManager.getDiskCount(); i++) {
@@ -4933,7 +7492,10 @@ class EmulatorJS {
// get disk name from m3u
const diskLabelValues = disks[i].split("|");
// remove the file extension from the disk file name
- let diskLabel = diskLabelValues[0].replace("." + diskLabelValues[0].split(".").pop(), "");
+ let diskLabel = diskLabelValues[0].replace(
+ "." + diskLabelValues[0].split(".").pop(),
+ "",
+ );
if (diskLabelValues.length >= 2) {
// has a label - use that instead
diskLabel = diskLabelValues[1];
@@ -4941,7 +7503,12 @@ class EmulatorJS {
diskLabels[i.toString()] = diskLabel;
}
}
- addToMenu(this.localization("Disk"), "disk", diskLabels, this.gameManager.getCurrentDisk().toString());
+ addToMenu(
+ this.localization("Disk"),
+ "disk",
+ diskLabels,
+ this.gameManager.getCurrentDisk().toString(),
+ );
}
this.disksMenu.appendChild(nested);
@@ -4950,7 +7517,7 @@ class EmulatorJS {
this.diskParent.style.position = "relative";
const homeSize = this.getElementSize(home);
- nested.style.width = (homeSize.width + 20) + "px";
+ nested.style.width = homeSize.width + 20 + "px";
nested.style.height = homeSize.height + "px";
this.disksMenu.style.display = "none";
@@ -4999,19 +7566,19 @@ class EmulatorJS {
const button = this.createElement("button");
const goToHome = () => {
const homeSize = this.getElementSize(parentElement);
- nested.style.width = (homeSize.width + 20) + "px";
+ nested.style.width = homeSize.width + 20 + "px";
nested.style.height = homeSize.height + "px";
menu.setAttribute("hidden", "");
parentElement.removeAttribute("hidden");
- }
+ };
this.addEventListener(menuOption, "click", (e) => {
const targetSize = this.getElementSize(menu);
- nested.style.width = (targetSize.width + 20) + "px";
+ nested.style.width = targetSize.width + 20 + "px";
nested.style.height = targetSize.height + "px";
menu.removeAttribute("hidden");
rv.scrollTo(0, 0);
parentElement.setAttribute("hidden", "");
- })
+ });
const observer = new MutationObserver((list) => {
for (const k of list) {
for (const removed of k.removedNodes) {
@@ -5021,8 +7588,9 @@ class EmulatorJS {
const index = menus.indexOf(menu);
if (index !== -1) menus.splice(index, 1);
this.settingsMenu.style.display = "";
- const homeSize = this.getElementSize(parentElement);
- nested.style.width = (homeSize.width + 20) + "px";
+ const homeSize =
+ this.getElementSize(parentElement);
+ nested.style.width = homeSize.width + 20 + "px";
nested.style.height = homeSize.height + "px";
// This SHOULD always be called before the game started - this SHOULD never be an issue
this.settingsMenu.style.display = "none";
@@ -5039,7 +7607,7 @@ class EmulatorJS {
pageTitle.innerText = title;
pageTitle.classList.add("ejs_menu_text_a");
button.appendChild(pageTitle);
-
+
// const optionsMenu = this.createElement("div");
// optionsMenu.classList.add("ejs_setting_menu");
// menu.appendChild(optionsMenu);
@@ -5054,14 +7622,14 @@ class EmulatorJS {
}
return rv;
- }
+ };
const checkForEmptyMenu = (element) => {
if (element.firstChild === null) {
element.parentElement.remove(); // No point in keeping an empty menu
parentMenuCt--;
}
- }
+ };
const home = createSettingParent();
@@ -5073,23 +7641,30 @@ class EmulatorJS {
needChange = true;
}
let height = this.elements.parent.getBoundingClientRect().height;
- let w2 = this.settingParent.parentElement.getBoundingClientRect().width;
+ let w2 =
+ this.settingParent.parentElement.getBoundingClientRect().width;
let settingsX = this.settingParent.getBoundingClientRect().x;
- if (w2 > window.innerWidth) settingsX += (w2 - window.innerWidth);
+ if (w2 > window.innerWidth) settingsX += w2 - window.innerWidth;
const onTheRight = settingsX > (w2 - 15) / 2;
if (height > 375) height = 375;
- home.style["max-height"] = (height - 95) + "px";
- nested.style["max-height"] = (height - 95) + "px";
+ home.style["max-height"] = height - 95 + "px";
+ nested.style["max-height"] = height - 95 + "px";
for (let i = 0; i < menus.length; i++) {
- menus[i].style["max-height"] = (height - 95) + "px";
+ menus[i].style["max-height"] = height - 95 + "px";
}
- this.settingsMenu.classList.toggle("ejs_settings_center_left", !onTheRight);
- this.settingsMenu.classList.toggle("ejs_settings_center_right", onTheRight);
+ this.settingsMenu.classList.toggle(
+ "ejs_settings_center_left",
+ !onTheRight,
+ );
+ this.settingsMenu.classList.toggle(
+ "ejs_settings_center_right",
+ onTheRight,
+ );
if (needChange) {
this.settingsMenu.style.display = "none";
this.settingsMenu.style.opacity = "";
}
- }
+ };
nested.appendChild(home);
let funcs = [];
@@ -5100,16 +7675,28 @@ class EmulatorJS {
this.settings[title] = newValue;
}
settings[title] = newValue;
- funcs.forEach(e => e(title));
- }
+ funcs.forEach((e) => e(title));
+ };
let allOpts = {};
- const addToMenu = (title, id, options, defaultOption, parentElement, useParentParent) => {
- if (Array.isArray(this.config.hideSettings) && this.config.hideSettings.includes(id)) {
+ const addToMenu = (
+ title,
+ id,
+ options,
+ defaultOption,
+ parentElement,
+ useParentParent,
+ ) => {
+ if (
+ Array.isArray(this.config.hideSettings) &&
+ this.config.hideSettings.includes(id)
+ ) {
return;
}
parentElement = parentElement || home;
- const transitionElement = useParentParent ? parentElement.parentElement.parentElement : parentElement;
+ const transitionElement = useParentParent
+ ? parentElement.parentElement.parentElement
+ : parentElement;
const menuOption = this.createElement("div");
menuOption.classList.add("ejs_settings_main_bar");
const span = this.createElement("span");
@@ -5137,19 +7724,19 @@ class EmulatorJS {
transitionElement.removeAttribute("hidden");
menu.setAttribute("hidden", "");
const homeSize = this.getElementSize(transitionElement);
- nested.style.width = (homeSize.width + 20) + "px";
+ nested.style.width = homeSize.width + 20 + "px";
nested.style.height = homeSize.height + "px";
transitionElement.removeAttribute("hidden");
- }
+ };
this.addEventListener(menuOption, "click", (e) => {
const targetSize = this.getElementSize(menu);
- nested.style.width = (targetSize.width + 20) + "px";
+ nested.style.width = targetSize.width + 20 + "px";
nested.style.height = targetSize.height + "px";
menu.removeAttribute("hidden");
optionsMenu.scrollTo(0, 0);
transitionElement.setAttribute("hidden", "");
transitionElement.setAttribute("hidden", "");
- })
+ });
this.addEventListener(button, "click", goToHome);
button.type = "button";
@@ -5173,7 +7760,10 @@ class EmulatorJS {
funcs.push((title) => {
if (id !== title) return;
for (let j = 0; j < buttons.length; j++) {
- buttons[j].classList.toggle("ejs_option_row_selected", buttons[j].getAttribute("ejs_value") === settings[id]);
+ buttons[j].classList.toggle(
+ "ejs_option_row_selected",
+ buttons[j].getAttribute("ejs_value") === settings[id],
+ );
}
this.menuOptionChanged(id, settings[id]);
current.innerText = opts[settings[id]];
@@ -5197,7 +7787,7 @@ class EmulatorJS {
this.menuOptionChanged(id, opt);
current.innerText = opts[opt];
goToHome();
- })
+ });
if (defaultOption === opt) {
optionButton.classList.add("ejs_option_row_selected");
this.menuOptionChanged(id, opt);
@@ -5215,20 +7805,41 @@ class EmulatorJS {
menu.appendChild(menuChild);
nested.appendChild(menu);
- }
+ };
const cores = this.getCores();
const core = cores[this.getCore(true)];
if (core && core.length > 1) {
- addToMenu(this.localization("Core" + " (" + this.localization("Requires restart") + ")"), "retroarch_core", core, this.getCore(), home);
+ addToMenu(
+ this.localization(
+ "Core" + " (" + this.localization("Requires restart") + ")",
+ ),
+ "retroarch_core",
+ core,
+ this.getCore(),
+ home,
+ );
}
- if (typeof window.SharedArrayBuffer === "function" && !this.requiresThreads(this.getCore())) {
- addToMenu(this.localization("Threads"), "ejs_threads", {
- "enabled": this.localization("Enabled"),
- "disabled": this.localization("Disabled")
- }, this.config.threads ? "enabled" : "disabled", home);
+ if (
+ typeof window.SharedArrayBuffer === "function" &&
+ !this.requiresThreads(this.getCore())
+ ) {
+ addToMenu(
+ this.localization("Threads"),
+ "ejs_threads",
+ {
+ enabled: this.localization("Enabled"),
+ disabled: this.localization("Disabled"),
+ },
+ this.config.threads ? "enabled" : "disabled",
+ home,
+ );
}
- const graphicsOptions = createSettingParent(true, "Graphics Settings", home);
+ const graphicsOptions = createSettingParent(
+ true,
+ "Graphics Settings",
+ home,
+ );
if (this.shaders) {
const builtinShaders = {
@@ -5243,12 +7854,12 @@ class EmulatorJS {
"crt-mattias.glslp": this.localization("CRT mattias"),
"crt-yeetron": this.localization("CRT yeetron"),
"crt-zfast": this.localization("CRT zfast"),
- "sabr": this.localization("SABR"),
- "bicubic": this.localization("Bicubic"),
+ sabr: this.localization("SABR"),
+ bicubic: this.localization("Bicubic"),
"mix-frames": this.localization("Mix frames"),
};
let shaderMenu = {
- "disabled": this.localization("Disabled"),
+ disabled: this.localization("Disabled"),
};
for (const shaderName in this.shaders) {
if (builtinShaders[shaderName]) {
@@ -5257,79 +7868,149 @@ class EmulatorJS {
shaderMenu[shaderName] = shaderName;
}
}
- addToMenu(this.localization("Shaders"), "shader", shaderMenu, "disabled", graphicsOptions, true);
+ addToMenu(
+ this.localization("Shaders"),
+ "shader",
+ shaderMenu,
+ "disabled",
+ graphicsOptions,
+ true,
+ );
}
if (this.supportsWebgl2 && !this.requiresWebGL2(this.getCore())) {
- addToMenu(this.localization("WebGL2") + " (" + this.localization("Requires restart") + ")", "webgl2Enabled", {
- "enabled": this.localization("Enabled"),
- "disabled": this.localization("Disabled")
- }, this.webgl2Enabled ? "enabled" : "disabled", graphicsOptions, true);
+ addToMenu(
+ this.localization("WebGL2") +
+ " (" +
+ this.localization("Requires restart") +
+ ")",
+ "webgl2Enabled",
+ {
+ enabled: this.localization("Enabled"),
+ disabled: this.localization("Disabled"),
+ },
+ this.webgl2Enabled ? "enabled" : "disabled",
+ graphicsOptions,
+ true,
+ );
}
- addToMenu(this.localization("FPS"), "fps", {
- "show": this.localization("show"),
- "hide": this.localization("hide")
- }, "hide", graphicsOptions, true);
-
- addToMenu(this.localization("VSync"), "vsync", {
- "enabled": this.localization("Enabled"),
- "disabled": this.localization("Disabled")
- }, "enabled", graphicsOptions, true);
+ addToMenu(
+ this.localization("FPS"),
+ "fps",
+ {
+ show: this.localization("show"),
+ hide: this.localization("hide"),
+ },
+ "hide",
+ graphicsOptions,
+ true,
+ );
- addToMenu(this.localization("Video Rotation"), "videoRotation", {
- "0": "0 deg",
- "1": "90 deg",
- "2": "180 deg",
- "3": "270 deg"
- }, this.videoRotation.toString(), graphicsOptions, true);
+ addToMenu(
+ this.localization("VSync"),
+ "vsync",
+ {
+ enabled: this.localization("Enabled"),
+ disabled: this.localization("Disabled"),
+ },
+ "enabled",
+ graphicsOptions,
+ true,
+ );
+
+ addToMenu(
+ this.localization("Video Rotation"),
+ "videoRotation",
+ {
+ 0: "0 deg",
+ 1: "90 deg",
+ 2: "180 deg",
+ 3: "270 deg",
+ },
+ this.videoRotation.toString(),
+ graphicsOptions,
+ true,
+ );
- const screenCaptureOptions = createSettingParent(true, "Screen Capture", home);
+ const screenCaptureOptions = createSettingParent(
+ true,
+ "Screen Capture",
+ home,
+ );
- addToMenu(this.localization("Screenshot Source"), "screenshotSource", {
- "canvas": "canvas",
- "retroarch": "retroarch"
- }, this.capture.photo.source, screenCaptureOptions, true);
+ addToMenu(
+ this.localization("Screenshot Source"),
+ "screenshotSource",
+ {
+ canvas: "canvas",
+ retroarch: "retroarch",
+ },
+ this.capture.photo.source,
+ screenCaptureOptions,
+ true,
+ );
let screenshotFormats = {
- "png": "png",
- "jpeg": "jpeg",
- "webp": "webp"
- }
- if (this.isSafari) {
- delete screenshotFormats["webp"];
+ png: "png",
+ jpeg: "jpeg",
+ webp: "webp",
+ };
+ if (this.isSafari) {
+ delete screenshotFormats["webp"];
}
if (!(this.capture.photo.format in screenshotFormats)) {
this.capture.photo.format = "png";
}
- addToMenu(this.localization("Screenshot Format"), "screenshotFormat", screenshotFormats, this.capture.photo.format, screenCaptureOptions, true);
+ addToMenu(
+ this.localization("Screenshot Format"),
+ "screenshotFormat",
+ screenshotFormats,
+ this.capture.photo.format,
+ screenCaptureOptions,
+ true,
+ );
const screenshotUpscale = this.capture.photo.upscale.toString();
let screenshotUpscales = {
- "0": "native",
- "1": "1x",
- "2": "2x",
- "3": "3x"
- }
+ 0: "native",
+ 1: "1x",
+ 2: "2x",
+ 3: "3x",
+ };
if (!(screenshotUpscale in screenshotUpscales)) {
screenshotUpscales[screenshotUpscale] = screenshotUpscale + "x";
}
- addToMenu(this.localization("Screenshot Upscale"), "screenshotUpscale", screenshotUpscales, screenshotUpscale, screenCaptureOptions, true);
+ addToMenu(
+ this.localization("Screenshot Upscale"),
+ "screenshotUpscale",
+ screenshotUpscales,
+ screenshotUpscale,
+ screenCaptureOptions,
+ true,
+ );
const screenRecordFPS = this.capture.video.fps.toString();
let screenRecordFPSs = {
- "30": "30",
- "60": "60"
- }
+ 30: "30",
+ 60: "60",
+ };
if (!(screenRecordFPS in screenRecordFPSs)) {
screenRecordFPSs[screenRecordFPS] = screenRecordFPS;
}
- addToMenu(this.localization("Screen Recording FPS"), "screenRecordFPS", screenRecordFPSs, screenRecordFPS, screenCaptureOptions, true);
+ addToMenu(
+ this.localization("Screen Recording FPS"),
+ "screenRecordFPS",
+ screenRecordFPSs,
+ screenRecordFPS,
+ screenCaptureOptions,
+ true,
+ );
let screenRecordFormats = {
- "mp4": "mp4",
- "webm": "webm"
- }
+ mp4: "mp4",
+ webm: "webm",
+ };
for (const format in screenRecordFormats) {
if (!MediaRecorder.isTypeSupported("video/" + format)) {
delete screenRecordFormats[format];
@@ -5338,172 +8019,387 @@ class EmulatorJS {
if (!(this.capture.video.format in screenRecordFormats)) {
this.capture.video.format = Object.keys(screenRecordFormats)[0];
}
- addToMenu(this.localization("Screen Recording Format"), "screenRecordFormat", screenRecordFormats, this.capture.video.format, screenCaptureOptions, true);
+ addToMenu(
+ this.localization("Screen Recording Format"),
+ "screenRecordFormat",
+ screenRecordFormats,
+ this.capture.video.format,
+ screenCaptureOptions,
+ true,
+ );
const screenRecordUpscale = this.capture.video.upscale.toString();
let screenRecordUpscales = {
- "1": "1x",
- "2": "2x",
- "3": "3x",
- "4": "4x"
- }
+ 1: "1x",
+ 2: "2x",
+ 3: "3x",
+ 4: "4x",
+ };
if (!(screenRecordUpscale in screenRecordUpscales)) {
- screenRecordUpscales[screenRecordUpscale] = screenRecordUpscale + "x";
- }
- addToMenu(this.localization("Screen Recording Upscale"), "screenRecordUpscale", screenRecordUpscales, screenRecordUpscale, screenCaptureOptions, true);
+ screenRecordUpscales[screenRecordUpscale] =
+ screenRecordUpscale + "x";
+ }
+ addToMenu(
+ this.localization("Screen Recording Upscale"),
+ "screenRecordUpscale",
+ screenRecordUpscales,
+ screenRecordUpscale,
+ screenCaptureOptions,
+ true,
+ );
- const screenRecordVideoBitrate = this.capture.video.videoBitrate.toString();
+ const screenRecordVideoBitrate =
+ this.capture.video.videoBitrate.toString();
let screenRecordVideoBitrates = {
- "1048576": "1 Mbit/sec",
- "2097152": "2 Mbit/sec",
- "2621440": "2.5 Mbit/sec",
- "3145728": "3 Mbit/sec",
- "4194304": "4 Mbit/sec"
- }
+ 1048576: "1 Mbit/sec",
+ 2097152: "2 Mbit/sec",
+ 2621440: "2.5 Mbit/sec",
+ 3145728: "3 Mbit/sec",
+ 4194304: "4 Mbit/sec",
+ };
if (!(screenRecordVideoBitrate in screenRecordVideoBitrates)) {
- screenRecordVideoBitrates[screenRecordVideoBitrate] = screenRecordVideoBitrate + " Bits/sec";
- }
- addToMenu(this.localization("Screen Recording Video Bitrate"), "screenRecordVideoBitrate", screenRecordVideoBitrates, screenRecordVideoBitrate, screenCaptureOptions, true);
+ screenRecordVideoBitrates[screenRecordVideoBitrate] =
+ screenRecordVideoBitrate + " Bits/sec";
+ }
+ addToMenu(
+ this.localization("Screen Recording Video Bitrate"),
+ "screenRecordVideoBitrate",
+ screenRecordVideoBitrates,
+ screenRecordVideoBitrate,
+ screenCaptureOptions,
+ true,
+ );
- const screenRecordAudioBitrate = this.capture.video.audioBitrate.toString();
+ const screenRecordAudioBitrate =
+ this.capture.video.audioBitrate.toString();
let screenRecordAudioBitrates = {
- "65536": "64 Kbit/sec",
- "131072": "128 Kbit/sec",
- "196608": "192 Kbit/sec",
- "262144": "256 Kbit/sec",
- "327680": "320 Kbit/sec"
- }
+ 65536: "64 Kbit/sec",
+ 131072: "128 Kbit/sec",
+ 196608: "192 Kbit/sec",
+ 262144: "256 Kbit/sec",
+ 327680: "320 Kbit/sec",
+ };
if (!(screenRecordAudioBitrate in screenRecordAudioBitrates)) {
- screenRecordAudioBitrates[screenRecordAudioBitrate] = screenRecordAudioBitrate + " Bits/sec";
- }
- addToMenu(this.localization("Screen Recording Audio Bitrate"), "screenRecordAudioBitrate", screenRecordAudioBitrates, screenRecordAudioBitrate, screenCaptureOptions, true);
+ screenRecordAudioBitrates[screenRecordAudioBitrate] =
+ screenRecordAudioBitrate + " Bits/sec";
+ }
+ addToMenu(
+ this.localization("Screen Recording Audio Bitrate"),
+ "screenRecordAudioBitrate",
+ screenRecordAudioBitrates,
+ screenRecordAudioBitrate,
+ screenCaptureOptions,
+ true,
+ );
checkForEmptyMenu(screenCaptureOptions);
const speedOptions = createSettingParent(true, "Speed Options", home);
- addToMenu(this.localization("Fast Forward"), "fastForward", {
- "enabled": this.localization("Enabled"),
- "disabled": this.localization("Disabled")
- }, "disabled", speedOptions, true);
+ addToMenu(
+ this.localization("Fast Forward"),
+ "fastForward",
+ {
+ enabled: this.localization("Enabled"),
+ disabled: this.localization("Disabled"),
+ },
+ "disabled",
+ speedOptions,
+ true,
+ );
- addToMenu(this.localization("Fast Forward Ratio"), "ff-ratio", [
- "1.5", "2.0", "2.5", "3.0", "3.5", "4.0", "4.5", "5.0", "5.5", "6.0", "6.5", "7.0", "7.5", "8.0", "8.5", "9.0", "9.5", "10.0", "unlimited"
- ], "3.0", speedOptions, true);
+ addToMenu(
+ this.localization("Fast Forward Ratio"),
+ "ff-ratio",
+ [
+ "1.5",
+ "2.0",
+ "2.5",
+ "3.0",
+ "3.5",
+ "4.0",
+ "4.5",
+ "5.0",
+ "5.5",
+ "6.0",
+ "6.5",
+ "7.0",
+ "7.5",
+ "8.0",
+ "8.5",
+ "9.0",
+ "9.5",
+ "10.0",
+ "unlimited",
+ ],
+ "3.0",
+ speedOptions,
+ true,
+ );
- addToMenu(this.localization("Slow Motion"), "slowMotion", {
- "enabled": this.localization("Enabled"),
- "disabled": this.localization("Disabled")
- }, "disabled", speedOptions, true);
+ addToMenu(
+ this.localization("Slow Motion"),
+ "slowMotion",
+ {
+ enabled: this.localization("Enabled"),
+ disabled: this.localization("Disabled"),
+ },
+ "disabled",
+ speedOptions,
+ true,
+ );
- addToMenu(this.localization("Slow Motion Ratio"), "sm-ratio", [
- "1.5", "2.0", "2.5", "3.0", "3.5", "4.0", "4.5", "5.0", "5.5", "6.0", "6.5", "7.0", "7.5", "8.0", "8.5", "9.0", "9.5", "10.0"
- ], "3.0", speedOptions, true);
+ addToMenu(
+ this.localization("Slow Motion Ratio"),
+ "sm-ratio",
+ [
+ "1.5",
+ "2.0",
+ "2.5",
+ "3.0",
+ "3.5",
+ "4.0",
+ "4.5",
+ "5.0",
+ "5.5",
+ "6.0",
+ "6.5",
+ "7.0",
+ "7.5",
+ "8.0",
+ "8.5",
+ "9.0",
+ "9.5",
+ "10.0",
+ ],
+ "3.0",
+ speedOptions,
+ true,
+ );
- addToMenu(this.localization("Rewind Enabled" + " (" + this.localization("Requires restart") + ")"), "rewindEnabled", {
- "enabled": this.localization("Enabled"),
- "disabled": this.localization("Disabled")
- }, "disabled", speedOptions, true);
+ addToMenu(
+ this.localization(
+ "Rewind Enabled" +
+ " (" +
+ this.localization("Requires restart") +
+ ")",
+ ),
+ "rewindEnabled",
+ {
+ enabled: this.localization("Enabled"),
+ disabled: this.localization("Disabled"),
+ },
+ "disabled",
+ speedOptions,
+ true,
+ );
if (this.rewindEnabled) {
- addToMenu(this.localization("Rewind Granularity"), "rewind-granularity", [
- "1", "3", "6", "12", "25", "50", "100"
- ], "6", speedOptions, true);
+ addToMenu(
+ this.localization("Rewind Granularity"),
+ "rewind-granularity",
+ ["1", "3", "6", "12", "25", "50", "100"],
+ "6",
+ speedOptions,
+ true,
+ );
}
const inputOptions = createSettingParent(true, "Input Options", home);
- addToMenu(this.localization("Menubar Mouse Trigger"), "menubarBehavior", {
- "downward": this.localization("Downward Movement"),
- "anywhere": this.localization("Movement Anywhere"),
- }, "downward", inputOptions, true);
-
- addToMenu(this.localization("Direct Keyboard Input"), "keyboardInput", {
- "disabled": this.localization("Disabled"),
- "enabled": this.localization("Enabled"),
- }, ((this.defaultCoreOpts && this.defaultCoreOpts.useKeyboard === true) ? "enabled" : "disabled"), inputOptions, true);
-
- addToMenu(this.localization("Forward Alt key"), "altKeyboardInput", {
- "disabled": this.localization("Disabled"),
- "enabled": this.localization("Enabled"),
- }, "disabled", inputOptions, true);
-
- addToMenu(this.localization("Lock Mouse"), "lockMouse", {
- "disabled": this.localization("Disabled"),
- "enabled": this.localization("Enabled"),
- }, (this.enableMouseLock === true ? "enabled" : "disabled"), inputOptions, true);
-
- addToMenu(this.localization("Autofire Interval"), "autofireInterval", {
- "20": "20ms",
- "50": "50ms",
- "100": "100ms",
- "200": "200ms",
- "500": "500ms",
- }, "100", inputOptions, true);
+ addToMenu(
+ this.localization("Menubar Mouse Trigger"),
+ "menubarBehavior",
+ {
+ downward: this.localization("Downward Movement"),
+ anywhere: this.localization("Movement Anywhere"),
+ },
+ "downward",
+ inputOptions,
+ true,
+ );
+
+ addToMenu(
+ this.localization("Direct Keyboard Input"),
+ "keyboardInput",
+ {
+ disabled: this.localization("Disabled"),
+ enabled: this.localization("Enabled"),
+ },
+ this.defaultCoreOpts && this.defaultCoreOpts.useKeyboard === true
+ ? "enabled"
+ : "disabled",
+ inputOptions,
+ true,
+ );
+
+ addToMenu(
+ this.localization("Forward Alt key"),
+ "altKeyboardInput",
+ {
+ disabled: this.localization("Disabled"),
+ enabled: this.localization("Enabled"),
+ },
+ "disabled",
+ inputOptions,
+ true,
+ );
+
+ addToMenu(
+ this.localization("Lock Mouse"),
+ "lockMouse",
+ {
+ disabled: this.localization("Disabled"),
+ enabled: this.localization("Enabled"),
+ },
+ this.enableMouseLock === true ? "enabled" : "disabled",
+ inputOptions,
+ true,
+ );
+
+ addToMenu(
+ this.localization("Autofire Interval"),
+ "autofireInterval",
+ {
+ 20: "20ms",
+ 50: "50ms",
+ 100: "100ms",
+ 200: "200ms",
+ 500: "500ms",
+ },
+ "100",
+ inputOptions,
+ true,
+ );
checkForEmptyMenu(inputOptions);
if (this.saveInBrowserSupported()) {
- const saveStateOpts = createSettingParent(true, "Save States", home);
- addToMenu(this.localization("Save State Slot"), "save-state-slot", ["1", "2", "3", "4", "5", "6", "7", "8", "9"], "1", saveStateOpts, true);
- addToMenu(this.localization("Save State Location"), "save-state-location", {
- "download": this.localization("Download"),
- "browser": this.localization("Keep in Browser")
- }, "download", saveStateOpts, true);
+ const saveStateOpts = createSettingParent(
+ true,
+ "Save States",
+ home,
+ );
+ addToMenu(
+ this.localization("Save State Slot"),
+ "save-state-slot",
+ ["1", "2", "3", "4", "5", "6", "7", "8", "9"],
+ "1",
+ saveStateOpts,
+ true,
+ );
+ addToMenu(
+ this.localization("Save State Location"),
+ "save-state-location",
+ {
+ download: this.localization("Download"),
+ browser: this.localization("Keep in Browser"),
+ },
+ "download",
+ saveStateOpts,
+ true,
+ );
if (!this.config.fixedSaveInterval) {
- addToMenu(this.localization("System Save interval"), "save-save-interval", {
- "0": "Disabled",
- "30": "30 seconds",
- "60": "1 minute",
- "300": "5 minutes",
- "600": "10 minutes",
- "900": "15 minutes",
- "1800": "30 minutes"
- }, "300", saveStateOpts, true);
+ addToMenu(
+ this.localization("System Save interval"),
+ "save-save-interval",
+ {
+ 0: "Disabled",
+ 30: "30 seconds",
+ 60: "1 minute",
+ 300: "5 minutes",
+ 600: "10 minutes",
+ 900: "15 minutes",
+ 1800: "30 minutes",
+ },
+ "300",
+ saveStateOpts,
+ true,
+ );
}
checkForEmptyMenu(saveStateOpts);
}
if (this.touch || this.hasTouchScreen) {
- const virtualGamepad = createSettingParent(true, "Virtual Gamepad", home);
- addToMenu(this.localization("Virtual Gamepad"), "virtual-gamepad", {
- "enabled": this.localization("Enabled"),
- "disabled": this.localization("Disabled")
- }, this.isMobile ? "enabled" : "disabled", virtualGamepad, true);
- addToMenu(this.localization("Menu Bar Button"), "menu-bar-button", {
- "visible": this.localization("visible"),
- "hidden": this.localization("hidden")
- }, "visible", virtualGamepad, true);
- addToMenu(this.localization("Left Handed Mode"), "virtual-gamepad-left-handed-mode", {
- "enabled": this.localization("Enabled"),
- "disabled": this.localization("Disabled")
- }, "disabled", virtualGamepad, true);
+ const virtualGamepad = createSettingParent(
+ true,
+ "Virtual Gamepad",
+ home,
+ );
+ addToMenu(
+ this.localization("Virtual Gamepad"),
+ "virtual-gamepad",
+ {
+ enabled: this.localization("Enabled"),
+ disabled: this.localization("Disabled"),
+ },
+ this.isMobile ? "enabled" : "disabled",
+ virtualGamepad,
+ true,
+ );
+ addToMenu(
+ this.localization("Menu Bar Button"),
+ "menu-bar-button",
+ {
+ visible: this.localization("visible"),
+ hidden: this.localization("hidden"),
+ },
+ "visible",
+ virtualGamepad,
+ true,
+ );
+ addToMenu(
+ this.localization("Left Handed Mode"),
+ "virtual-gamepad-left-handed-mode",
+ {
+ enabled: this.localization("Enabled"),
+ disabled: this.localization("Disabled"),
+ },
+ "disabled",
+ virtualGamepad,
+ true,
+ );
checkForEmptyMenu(virtualGamepad);
}
let coreOpts;
try {
coreOpts = this.gameManager.getCoreOptions();
- } catch(e) {}
+ } catch (e) {}
if (coreOpts) {
- const coreOptions = createSettingParent(true, "Backend Core Options", home);
+ const coreOptions = createSettingParent(
+ true,
+ "Backend Core Options",
+ home,
+ );
coreOpts.split("\n").forEach((line, index) => {
let option = line.split("; ");
let name = option[0];
let options = option[1].split("|"),
- optionName = name.split("|")[0].replace(/_/g, " ").replace(/.+\-(.+)/, "$1");
+ optionName = name
+ .split("|")[0]
+ .replace(/_/g, " ")
+ .replace(/.+\-(.+)/, "$1");
options.slice(1, -1);
if (options.length === 1) return;
let availableOptions = {};
for (let i = 0; i < options.length; i++) {
- availableOptions[options[i]] = this.localization(options[i], this.config.settingsLanguage);
+ availableOptions[options[i]] = this.localization(
+ options[i],
+ this.config.settingsLanguage,
+ );
}
- addToMenu(this.localization(optionName, this.config.settingsLanguage),
- name.split("|")[0], availableOptions,
- (name.split("|").length > 1) ? name.split("|")[1] : options[0].replace("(Default) ", ""),
+ addToMenu(
+ this.localization(optionName, this.config.settingsLanguage),
+ name.split("|")[0],
+ availableOptions,
+ name.split("|").length > 1
+ ? name.split("|")[1]
+ : options[0].replace("(Default) ", ""),
coreOptions,
- true);
- })
+ true,
+ );
+ });
checkForEmptyMenu(coreOptions);
}
@@ -5511,1935 +8407,428 @@ class EmulatorJS {
this.retroarchOpts = [
{
title: "Audio Latency", // String
- name: "audio_latency", // String - value to be set in retroarch.cfg
- // options should ALWAYS be strings here...
- options: ["8", "16", "32", "64", "128"], // values
- options: {"8": "eight", "16": "sixteen", "32": "thirty-two", "64": "sixty-four", "128": "one hundred-twenty-eight"}, // This also works
- default: "128", // Default
- isString: false // Surround value with quotes in retroarch.cfg file?
- }
- ];*/
-
- if (this.retroarchOpts && Array.isArray(this.retroarchOpts)) {
- const retroarchOptsMenu = createSettingParent(true, "RetroArch Options" + " (" + this.localization("Requires restart") + ")", home);
- this.retroarchOpts.forEach(option => {
- addToMenu(this.localization(option.title, this.config.settingsLanguage),
- option.name,
- option.options,
- option.default,
- retroarchOptsMenu,
- true);
- })
- checkForEmptyMenu(retroarchOptsMenu);
- }
-
- checkForEmptyMenu(graphicsOptions);
- checkForEmptyMenu(speedOptions);
-
- this.settingsMenu.appendChild(nested);
-
- this.settingParent.appendChild(this.settingsMenu);
- this.settingParent.style.position = "relative";
-
- this.settingsMenu.style.display = "";
- const homeSize = this.getElementSize(home);
- nested.style.width = (homeSize.width + 20) + "px";
- nested.style.height = homeSize.height + "px";
-
- this.settingsMenu.style.display = "none";
-
- if (this.debug) {
- console.log("Available core options", allOpts);
- }
-
- if (this.config.defaultOptions) {
- for (const k in this.config.defaultOptions) {
- this.changeSettingOption(k, this.config.defaultOptions[k], true);
- }
- }
-
- if (parentMenuCt === 0) {
- this.on("start", () => {
- this.elements.bottomBar.settings[0][0].style.display = "none";
- });
- }
- }
- createSubPopup(hidden) {
- const popup = this.createElement("div");
- popup.classList.add("ejs_popup_container");
- popup.classList.add("ejs_popup_container_box");
- const popupMsg = this.createElement("div");
- popupMsg.innerText = "";
- if (hidden) popup.setAttribute("hidden", "");
- popup.appendChild(popupMsg);
- return [popup, popupMsg];
- }
-
- updateNetplayUI(isJoining) {
- if (!this.elements.bottomBar) return;
-
- const bar = this.elements.bottomBar;
- const isClient = !this.netplay.owner;
- const shouldHideButtons = isJoining && isClient;
- const elementsToToggle = [
- ...(bar.playPause || []),
- ...(bar.restart || []),
- ...(bar.saveState || []),
- ...(bar.loadState || []),
- ...(bar.cheat || []),
- ...(bar.saveSavFiles || []),
- ...(bar.loadSavFiles || []),
- ...(bar.exit || []),
- ...(bar.contextMenu || []),
- ...(bar.cacheManager || [])
- ];
-
- // Add the parent containers to the same logic
- if (bar.settings && bar.settings.length > 0 && bar.settings[0].parentElement) {
- elementsToToggle.push(bar.settings[0].parentElement);
- }
- if (this.diskParent) {
- elementsToToggle.push(this.diskParent);
- }
-
- elementsToToggle.forEach(el => {
- if (el) {
- el.classList.toggle('netplay-hidden', shouldHideButtons);
- }
- });
- }
- createNetplayMenu() {
- const body = this.createPopup("Netplay", {
- "Create a Room": () => {
- if (typeof this.netplay.updateList !== "function")
- this.defineNetplayFunctions();
- if (this.isNetplay) {
- this.netplay.leaveRoom();
- } else {
- this.netplay.showOpenRoomDialog();
- }
- },
- "Close": () => {
- this.netplayMenu.style.display = "none";
- if (this.netplay.updateList) {
- this.netplay.updateList.stop();
- }
- }
- }, true);
- this.netplayMenu = body.parentElement;
- const createButton = this.netplayMenu.getElementsByTagName("a")[0];
- const rooms = this.createElement("div");
- const title = this.createElement("strong");
- title.innerText = this.localization("Rooms");
- const table = this.createElement("table");
- table.classList.add("ejs_netplay_table");
- table.style.width = "100%";
- table.setAttribute("cellspacing", "0");
- const thead = this.createElement("thead");
- const row = this.createElement("tr");
- const addToHeader = (text) => {
- const item = this.createElement("td");
- item.innerText = text;
- item.style["text-align"] = "center";
- row.appendChild(item);
- return item;
- };
- thead.appendChild(row);
- addToHeader("Room Name").style["text-align"] = "left";
- addToHeader("Players").style.width = "80px";
- addToHeader("").style.width = "80px";
- table.appendChild(thead);
- const tbody = this.createElement("tbody");
-
- table.appendChild(tbody);
- rooms.appendChild(title);
- rooms.appendChild(table);
-
- const joined = this.createElement("div");
- const title2 = this.createElement("strong");
- title2.innerText = "{roomname}";
- const password = this.createElement("div");
- password.innerText = "Password: ";
- const table2 = this.createElement("table");
- table2.classList.add("ejs_netplay_table");
- table2.style.width = "100%";
- table2.setAttribute("cellspacing", "0");
- const thead2 = this.createElement("thead");
- const row2 = this.createElement("tr");
- const addToHeader2 = (text) => {
- const item = this.createElement("td");
- item.innerText = text;
- row2.appendChild(item);
- return item;
- };
- thead2.appendChild(row2);
- addToHeader2("Player").style.width = "80px";
- addToHeader2("Name");
- addToHeader2("").style.width = "80px";
- table2.appendChild(thead2);
- const tbody2 = this.createElement("tbody");
-
- table2.appendChild(tbody2);
- joined.appendChild(title2);
- joined.appendChild(password);
- joined.appendChild(table2);
-
- joined.style.display = "none";
- body.appendChild(rooms);
- body.appendChild(joined);
-
- this.openNetplayMenu = () => {
- if (this.netplayShowTurnWarning && !this.netplayWarningShown) {
- const warningDiv = this.createElement("div");
- warningDiv.className = "ejs_netplay_warning";
- warningDiv.innerText = "Warning: No TURN server configured. Netplay connections may fail.";
- const menuBody = this.netplayMenu.querySelector(".ejs_popup_body");
- if (menuBody) {
- menuBody.prepend(warningDiv);
- this.netplayWarningShown = true;
- }
- }
- this.netplayMenu.style.display = "";
- if (!this.netplay || (this.netplay && !this.netplay.name)) {
- this.netplay = {
- table: tbody,
- playerTable: tbody2,
- passwordElem: password,
- roomNameElem: title2,
- createButton: createButton,
- tabs: [rooms, joined],
- ...this.netplay
- };
- const popups = this.createSubPopup();
- this.netplayMenu.appendChild(popups[0]);
- popups[1].classList.add("ejs_cheat_parent");
- const popup = popups[1];
-
- const header = this.createElement("div");
- const title = this.createElement("h2");
- title.innerText = this.localization("Set Player Name");
- title.classList.add("ejs_netplay_name_heading");
- header.appendChild(title);
- popup.appendChild(header);
-
- const main = this.createElement("div");
- main.classList.add("ejs_netplay_header");
- const head = this.createElement("strong");
- head.innerText = this.localization("Player Name");
- const input = this.createElement("input");
- input.type = "text";
- input.setAttribute("maxlength", 20);
-
- main.appendChild(head);
- main.appendChild(this.createElement("br"));
- main.appendChild(input);
- popup.appendChild(main);
-
- popup.appendChild(this.createElement("br"));
- const submit = this.createElement("button");
- submit.classList.add("ejs_button_button");
- submit.classList.add("ejs_popup_submit");
- submit.style["background-color"] = "rgba(var(--ejs-primary-color),1)";
- submit.innerText = this.localization("Submit");
- popup.appendChild(submit);
- this.addEventListener(submit, "click", (e) => {
- if (!input.value.trim())
- return;
- this.netplay.name = input.value.trim();
- popups[0].remove();
- });
- }
- if (typeof this.netplay.updateList !== "function") {
- this.defineNetplayFunctions();
- }
- this.netplay.updateList.start();
- };
- }
-
- defineNetplayFunctions() {
- const EJS_INSTANCE = this;
-
- function guidGenerator() {
- const S4 = function () {
- return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
- };
- return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
- }
- this.getNativeResolution = function () {
- if (this.Module && this.Module.getNativeResolution) {
- try {
- const res = this.Module.getNativeResolution();
- console.log("Native resolution from Module:", res);
- return res;
- } catch (error) {
- console.error("Failed to get native resolution:", error);
- return {
- width: 640,
- height: 480
- };
- }
- }
- return {
- width: 640,
- height: 480
- };
- };
-
- this.netplayGetUserIndex = function () {
- if (!this.isNetplay || !this.netplay.players || !this.netplay.playerID) {
- console.warn("netplayGetUserIndex: Netplay not active or players/playerID undefined");
- return 0;
- }
- const playerIds = Object.keys(this.netplay.players);
- const index = playerIds.indexOf(this.netplay.playerID);
- return index === -1 ? 0 : index;
- };
-
- this.netplay.simulateInput = (player, index, value) => {
- console.log("netplay.simulateInput called:", {
- player,
- index,
- value,
- playerIndex: this.netplayGetUserIndex()
- });
- if (!this.isNetplay || !this.gameManager || !this.gameManager.functions || !this.gameManager.functions.simulateInput) {
- console.error("Cannot simulate input: Netplay not active or gameManager.functions.simulateInput undefined");
- return;
- }
- const playerIndex = this.netplayGetUserIndex();
- let frame = this.netplay.currentFrame || 0;
- if (this.netplay.owner) {
- if (!this.netplay.inputsData[frame])
- this.netplay.inputsData[frame] = [];
- this.netplay.inputsData[frame].push({
- frame: frame,
- connected_input: [playerIndex, index, value]
- });
- this.gameManager.functions.simulateInput(playerIndex, index, value);
- } else {
- this.gameManager.functions.simulateInput(playerIndex, index, value);
- if (this.netplaySendMessage) {
- this.netplaySendMessage({
- "sync-control": [{
- frame: frame + 20,
- connected_input: [playerIndex, index, value]
- }
- ]
- });
- } else {
- console.error("netplaySendMessage is undefined");
- }
- }
- };
-
- this.netplayUpdateTableList = async () => {
- if (!this.netplay || !this.netplay.table) {
- console.error("netplay or netplay.table is undefined");
- return;
- }
-
- const addToTable = (id, name, current, max, hasPassword) => {
- const row = this.createElement("tr");
- row.classList.add("ejs_netplay_table_row");
- const addCell = (text) => {
- const item = this.createElement("td");
- item.innerText = text;
- item.style.padding = "10px 0";
- item.style["text-align"] = "center";
- row.appendChild(item);
- return item;
- };
- addCell(name).style["text-align"] = "left";
- addCell(current + "/" + max).style.width = "80px";
- const parent = addCell("");
- parent.style.width = "80px";
- this.netplay.table.appendChild(row);
-
- if (current < max) {
- const join = this.createElement("button");
- join.classList.add("ejs_netplay_join_button", "ejs_button_button");
- join.style["background-color"] = "rgba(var(--ejs-primary-color),1)";
- join.innerText = this.localization("Join");
- parent.appendChild(join);
-
- this.addEventListener(join, "click", () => {
- if (hasPassword) {
- let password = prompt("Please enter the room password:");
- if (password !== null) {
- password = password.trim();
- this.netplayJoinRoom(id, name, max, password);
- }
- } else {
- this.netplayJoinRoom(id, name, max, null);
- }
- });
- }
- };
-
- try {
- const open = await this.netplayGetOpenRooms();
- this.netplay.table.innerHTML = "";
- for (const k in open) {
- addToTable(k, open[k].room_name, open[k].current, open[k].max, open[k].hasPassword);
- }
- } catch (e) {
- console.error("Could not update room list:", e);
- }
- };
-
- this.netplayGetOpenRooms = async () => {
- if (!this.netplay.url) {
- console.error("netplay.url is undefined");
- return {};
- }
- try {
- const response = await fetch(this.netplay.url + "/list?domain=" + window.location.host + "&game_id=" + this.config.gameId);
- const data = await response.text();
- console.log("Fetched open rooms:", data);
- return JSON.parse(data);
- } catch (error) {
- console.error("Error fetching open rooms:", error);
- return {};
- }
- };
-
- this.netplayUpdateListStart = () => {
- if (!this.netplayUpdateTableList) {
- console.error("netplayUpdateTableList is undefined");
- return;
- }
- this.netplay.updateListInterval = setInterval(this.netplayUpdateTableList.bind(this), 1000);
- };
-
- this.netplayUpdateListStop = () => {
- clearInterval(this.netplay.updateListInterval);
- };
-
- this.netplayShowOpenRoomDialog = () => {
- if (!this.createSubPopup || !this.createElement || !this.localization || !this.addEventListener) {
- console.error("Required methods for netplayShowOpenRoomDialog are undefined");
- return;
- }
- this.originalControls = JSON.parse(JSON.stringify(this.controls));
- const popups = this.createSubPopup();
- this.netplayMenu.appendChild(popups[0]);
- popups[1].classList.add("ejs_cheat_parent");
- const popup = popups[1];
-
- const header = this.createElement("div");
- const title = this.createElement("h2");
- title.innerText = this.localization("Create a room");
- title.classList.add("ejs_netplay_name_heading");
- header.appendChild(title);
- popup.appendChild(header);
-
- const main = this.createElement("div");
- main.classList.add("ejs_netplay_header");
- const rnhead = this.createElement("strong");
- rnhead.innerText = this.localization("Room Name");
- const rninput = this.createElement("input");
- rninput.type = "text";
- rninput.setAttribute("maxlength", "20");
-
- const maxhead = this.createElement("strong");
- maxhead.innerText = this.localization("Max Players");
- const maxinput = this.createElement("select");
- const playerCounts = ["2", "3", "4"];
- playerCounts.forEach(count => {
- const option = this.createElement("option");
- option.value = count;
- option.innerText = count;
- option.classList.add("option-enabled");
- maxinput.appendChild(option);
- });
-
- const pwhead = this.createElement("strong");
- pwhead.innerText = this.localization("Password (optional)");
- const pwinput = this.createElement("input");
- pwinput.type = "text";
- pwinput.setAttribute("maxlength", "20");
-
- main.appendChild(rnhead);
- main.appendChild(this.createElement("br"));
- main.appendChild(rninput);
- main.appendChild(maxhead);
- main.appendChild(this.createElement("br"));
- main.appendChild(maxinput);
- main.appendChild(pwhead);
- main.appendChild(this.createElement("br"));
- main.appendChild(pwinput);
- popup.appendChild(main);
-
- popup.appendChild(this.createElement("br"));
- const submit = this.createElement("button");
- submit.classList.add("ejs_button_button", "ejs_popup_submit");
- submit.style["background-color"] = "rgba(var(--ejs-primary-color),1)";
- submit.style.margin = "0 10px";
- submit.innerText = this.localization("Submit");
- popup.appendChild(submit);
- this.addEventListener(submit, "click", () => {
- console.log("Submit button clicked");
- if (!rninput.value.trim()) {
- console.log("Room name is empty, aborting");
- return;
- }
- const roomName = rninput.value.trim();
- const maxPlayers = parseInt(maxinput.value);
- const password = pwinput.value.trim();
- console.log("Creating room with:", {
- roomName,
- maxPlayers,
- password
- });
- this.netplayOpenRoom(roomName, maxPlayers, password);
- popups[0].remove();
- });
- const close = this.createElement("button");
- close.classList.add("ejs_button_button", "ejs_popup_submit");
- close.style.margin = "0 10px";
- close.innerText = this.localization("Close");
- popup.appendChild(close);
- this.addEventListener(close, "click", () => popups[0].remove());
- };
-
- this.netplayInitWebRTCStream = async () => {
- if (this.netplay.localStream)
- return;
- console.log("Initializing WebRTC stream for owner...");
- const { width: nativeWidth, height: nativeHeight } = this.getNativeResolution();
- if (this.canvas) {
- this.canvas.width = nativeWidth;
- this.canvas.height = nativeHeight;
- }
- if (this.netplay.owner && this.Module && this.Module.setCanvasSize) {
- this.Module.setCanvasSize(nativeWidth, nativeHeight);
- console.log("Set emulator canvas size to native:", {
- width: nativeWidth,
- height: nativeHeight
- });
- }
-
- const stream = this.collectScreenRecordingMediaTracks(this.canvas, 30);
- if (!stream || !stream.getTracks().length) {
- console.error("Failed to capture stream:", stream);
- this.displayMessage("Failed to initialize video stream", 5000);
- return;
- }
- const videoTrack = stream.getVideoTracks()[0];
- if (videoTrack) {
- videoTrack.applyConstraints({
- width: {
- ideal: nativeWidth
- },
- height: {
- ideal: nativeHeight
- },
- frameRate: {
- ideal: 30,
- max: 30
- }
- }).catch(err => console.error("Constraint error:", err));
- console.log("Track settings:", videoTrack.getSettings());
- }
- stream.getTracks().forEach(track => {
- console.log("Track:", {
- kind: track.kind,
- enabled: track.enabled,
- muted: track.muted
- });
- track.onmute = () => console.warn("Track muted:", track.id);
- track.onended = () => console.warn("Track ended:", track.id);
- });
- this.netplay.localStream = stream;
- };
-
- this.netplayCreatePeerConnection = (peerId) => {
- const pc = new RTCPeerConnection({
- iceServers: this.config.netplayICEServers,
- iceCandidatePoolSize: 10
- });
-
- let dataChannel;
-
- if (this.netplay.owner) {
- dataChannel = pc.createDataChannel('inputs');
- dataChannel.onopen = () => console.log(`Data channel opened for peer ${peerId}`);
- dataChannel.onmessage = (event) => {
- const data = JSON.parse(event.data);
- if (data.type === "host-left") {
- this.displayMessage("Host left. Restarting...", 3000);
- this.netplayLeaveRoom();
- return;
- }
- const playerIndex = data.player;
- const frame = this.netplay.currentFrame || 0;
-
- if (!this.netplay.inputsData[frame]) {
- this.netplay.inputsData[frame] = [];
- }
- this.netplay.inputsData[frame].push({
- frame: frame,
- connected_input: [playerIndex, data.index, data.value]
- });
- if (this.gameManager && this.gameManager.functions && this.gameManager.functions.simulateInput) {
- this.gameManager.functions.simulateInput(playerIndex, data.index, data.value);
- } else {
- console.error("Cannot process input: gameManager.functions.simulateInput is undefined");
- }
- };
- } else {
- pc.ondatachannel = (event) => {
- dataChannel = event.channel;
- dataChannel.onopen = () => console.log(`Data channel opened for peer ${peerId}`);
- dataChannel.onmessage = (event) => {
- const data = JSON.parse(event.data);
- if (data.type === "host-left") {
- this.displayMessage("Host left. Restarting...", 3000);
- this.netplayLeaveRoom();
- return;
- }
- console.log(`Received input from host ${peerId}:`, data);
- if (this.gameManager && this.gameManager.functions && this.gameManager.functions.simulateInput) {
- this.gameManager.functions.simulateInput(data.player, data.index, data.value);
- } else {
- console.error("Cannot process input: gameManager.functions.simulateInput is undefined");
- }
- };
- };
- }
-
- if (this.netplay.owner && this.netplay.localStream) {
- this.netplay.localStream.getTracks().forEach(track => {
- pc.addTrack(track, this.netplay.localStream);
- });
-
- const codecs = RTCRtpSender.getCapabilities('video').codecs;
- const preferredCodecs = codecs.filter(codec => ['video/H264', 'video/VP8'].includes(codec.mimeType));
- const transceiver = pc.getTransceivers().find(t => t.sender && t.sender.track && t.sender.track.kind === 'video');
- if (transceiver && preferredCodecs.length) {
- try {
- transceiver.setCodecPreferences(preferredCodecs);
- } catch (error) {
- console.error("Failed to set codec preferences:", error);
- }
- }
- } else {
- pc.addTransceiver('video', {
- direction: 'recvonly'
- });
- }
-
- this.netplay.peerConnections[peerId] = {
- pc,
- dataChannel
- };
-
- let streamReceived = false;
- const streamTimeout = setTimeout(() => {
- if (!streamReceived && !this.netplay.owner) {
- this.displayMessage("Failed to receive video stream. Check your network and try again.", 5000);
- this.netplayLeaveRoom();
- }
- }, 10000);
-
- pc.onicecandidate = (event) => {
- if (event.candidate) {
- this.netplay.socket.emit("webrtc-signal", {
- target: peerId,
- candidate: event.candidate
- });
- }
- };
-
- pc.onicecandidateerror = (event) => {
- console.error("ICE candidate error for peer", peerId, ":", event);
- };
-
- pc.onconnectionstatechange = () => {
- if (pc.connectionState === "connected") {
- this.netplay.webRtcReady = true;
- } else if (pc.connectionState === "failed" || pc.connectionState === "disconnected") {
- this.displayMessage("Connection with player lost. Attempting to reconnect...", 3000);
- clearTimeout(streamTimeout);
- pc.close();
- delete this.netplay.peerConnections[peerId];
- setTimeout(() => this.netplayCreatePeerConnection(peerId), 2000);
- }
- };
-
- pc.ontrack = (event) => {
- if (!this.netplay.owner) {
- streamReceived = true;
- clearTimeout(streamTimeout);
- const stream = event.streams[0];
- if (!this.netplay.video) {
- this.netplay.video = document.createElement('video');
- this.netplay.video.muted = true;
- this.netplay.video.playsInline = true;
- }
- this.netplay.video.srcObject = stream;
- this.netplay.video.play().catch(() => {
- if (this.isMobile) {
- this.promptUserInteraction(this.netplay.video);
- }
- });
- this.drawVideoToCanvas();
- }
- };
-
- if (this.netplay.owner && this.netplay.localStream) {
- pc.createOffer()
- .then(offer => {
- offer.sdp = offer.sdp.replace(/profile-level-id=[0-9a-fA-F]+/, 'profile-level-id=42e01f');
- return pc.setLocalDescription(offer);
- })
- .then(() => {
- this.netplay.socket.emit("webrtc-signal", {
- target: peerId,
- offer: pc.localDescription
- });
- })
- .catch(error => console.error("Error creating offer:", error));
- }
-
- return pc;
- };
-
- this.showVideoOverlay = () => {
- const videoElement = this.netplay.video;
- if (!videoElement) {
- console.error("showVideoOverlay: videoElement is not initialized");
- return;
- }
- console.log("showVideoOverlay called, videoElement exists:", videoElement);
-
- if (videoElement.parentElement) {
- console.log("Removing video element from current parent:", videoElement.parentElement);
- videoElement.parentElement.removeChild(videoElement);
- }
-
- videoElement.style.position = "absolute";
- if (this.isMobile) {
- videoElement.style.top = "0";
- videoElement.style.left = "0";
- videoElement.style.width = "100vw";
- videoElement.style.height = "100vh";
- videoElement.style.maxHeight = "100vh";
- } else {
- videoElement.style.top = "0";
- videoElement.style.left = "0";
- videoElement.style.width = "100%";
- videoElement.style.height = "100%";
- }
- videoElement.style.border = "1px solid white";
- videoElement.style.zIndex = "1";
- videoElement.style.display = "";
- videoElement.style.objectFit = "contain";
- document.body.appendChild(videoElement);
- console.log("Video overlay added to DOM, styles:", videoElement.style.cssText);
-
- const playVideo = async() => {
- console.log("Attempting to play video, readyState:", videoElement.readyState, "Paused:", videoElement.paused, "Ended:", videoElement.ended, "Muted:", videoElement.muted);
- try {
- await videoElement.play();
- console.log("Video playback started successfully, currentTime:", videoElement.currentTime);
- } catch (error) {
- console.error("Video play error:", error);
- if (this.isMobile) {
- this.promptUserInteraction(videoElement);
- } else {
- console.log("Autoplay failed on desktop, but user interaction not required for muted video");
- }
- }
- if (videoElement.videoWidth === 0 || videoElement.videoHeight === 0) {
- console.warn("Video element has zero dimensions, likely no valid frame:", {
- videoWidth: videoElement.videoWidth,
- videoHeight: videoElement.videoHeight
- });
- } else {
- console.log("Video dimensions:", {
- videoWidth: videoElement.videoWidth,
- videoHeight: videoElement.videoHeight
- });
- }
- };
- playVideo();
- };
-
- this.drawVideoToCanvas = () => {
- const videoElement = this.netplay.video;
- const canvas = this.netplayCanvas;
- if (!canvas) {
- console.error("drawVideoToCanvas: Missing canvas!");
- }
- const ctx = canvas.getContext('2d', {
- alpha: false,
- willReadFrequently: true
- });
-
- if (!videoElement || !ctx) {
- console.error("drawVideoToCanvas: Missing video, or context!");
- return;
- }
-
- const { width: nativeWidth, height: nativeHeight } = this.getNativeResolution() || {
- width: 720,
- height: 700
- };
- canvas.width = nativeWidth;
- canvas.height = nativeHeight;
-
- const ensureVideoPlaying = async() => {
- let retries = 0;
- const maxRetries = 5;
- while (retries < maxRetries) {
- if (videoElement.paused || videoElement.ended) {
- try {
- await videoElement.play();
- } catch (error) {
- if (this.isMobile)
- this.promptUserInteraction(videoElement);
- }
- }
- if (videoElement.videoWidth > 0 && videoElement.videoHeight > 0) {
- if (!this.netplay.lockedAspectRatio) {
- this.netplay.lockedAspectRatio = videoElement.videoWidth / videoElement.videoHeight;
- console.log("Locked aspect ratio:", this.netplay.lockedAspectRatio);
- }
- break;
- }
- retries++;
- await new Promise(resolve => setTimeout(resolve, 1000));
- }
-
- if (retries >= maxRetries) {
- this.displayMessage("Failed to initialize video stream", 5000);
- this.netplayLeaveRoom();
- }
- };
-
- const drawFrame = () => {
- if (!this.isNetplay || this.netplay.owner)
- return;
-
- const aspect = this.netplay.lockedAspectRatio || (videoElement.videoWidth / videoElement.videoHeight) || (nativeWidth / nativeHeight);
-
- if (videoElement.readyState >= videoElement.HAVE_CURRENT_DATA && videoElement.videoWidth > 0) {
- ctx.clearRect(0, 0, canvas.width, canvas.height);
-
- const canvasAspect = nativeWidth / nativeHeight;
- let drawWidth,
- drawHeight,
- offsetX,
- offsetY;
-
- if (aspect > canvasAspect) {
- drawWidth = nativeWidth;
- drawHeight = nativeWidth / aspect;
- offsetX = 0;
- offsetY = 0;
- } else {
- drawHeight = nativeHeight;
- drawWidth = nativeHeight * aspect;
- offsetX = (nativeWidth - drawWidth) / 2;
- offsetY = 0;
- }
-
- ctx.drawImage(videoElement, 0, 0, videoElement.videoWidth, videoElement.videoHeight, offsetX, offsetY, drawWidth, drawHeight);
- }
-
- requestAnimationFrame(drawFrame);
- };
-
- videoElement.addEventListener('loadeddata', () => {
- ensureVideoPlaying().then(drawFrame);
- }, {
- once: true
- });
-
- ensureVideoPlaying();
- };
-
- this.netplayStartSocketIO = (callback) => {
- if (!this.netplay.previousPlayers) {
- this.netplay.previousPlayers = {};
- }
-
- if (typeof io === "undefined") {
- console.error("Socket.IO client library not loaded. Please include ");
- this.displayMessage("Socket.IO not available", 5000);
- return;
- }
- if (this.netplay.socket && this.netplay.socket.connected) {
- console.log("Socket already connected, reusing:", this.netplay.socket.id);
- callback();
- return;
- }
- if (!this.netplay.url) {
- console.error("Cannot initialize Socket.IO: netplay.url is undefined");
- this.displayMessage("Network configuration error", 5000);
- return;
- }
- console.log("Initializing new Socket.IO connection to:", this.netplay.url);
- this.netplay.socket = io(this.netplay.url);
- this.netplay.socket.on("connect", () => {
- console.log("Socket.IO connected:", this.netplay.socket.id);
- callback();
- });
- this.netplay.socket.on("connect_error", (error) => {
- console.error("Socket.IO connection error:", error.message);
- this.displayMessage("Failed to connect to server: " + error.message, 5000);
- });
- this.netplay.socket.on("users-updated", (users) => {
- const currentPlayers = users || {};
- const previousPlayerIds = Object.keys(this.netplay.previousPlayers);
- const currentPlayerIds = Object.keys(currentPlayers);
-
- // Find who joined
- currentPlayerIds.forEach(id => {
- if (!previousPlayerIds.includes(id) && id !== this.netplay.playerID) {
- const playerName = currentPlayers[id].player_name || 'A player';
- this.displayMessage(`${playerName} has joined the room.`);
- }
- });
-
- // Find who left
- previousPlayerIds.forEach(id => {
- if (!currentPlayerIds.includes(id)) {
- const playerName = this.netplay.previousPlayers[id].player_name || 'A player';
- this.displayMessage(`${playerName} has left the room.`);
- }
- });
-
- this.netplay.previousPlayers = currentPlayers;
-
- console.log("Users updated:", users);
- this.netplay.players = users;
- this.netplayUpdatePlayersTable();
- if (this.netplay.owner) {
- console.log("Owner setting up WebRTC for updated users...");
- this.netplayInitWebRTCStream().then(() => {
- Object.keys(users).forEach(playerId => {
- if (playerId !== this.netplay.playerID) {
- const socketId = this.netplay.players[playerId].socketId;
- if (!socketId) {
- console.error("No socketId for player", playerId, "- WebRTC may fail");
- return;
- }
- const peerId = socketId;
- if (!this.netplay.peerConnections[peerId]) {
- console.log("Creating peer connection for", peerId);
- this.netplayCreatePeerConnection(peerId);
- }
- }
- });
- }).catch(error => console.error("Failed to initialize WebRTC stream in users-updated:", error));
- }
- });
- this.netplay.socket.on("disconnect", () => this.netplayLeaveRoom());
- this.netplay.socket.on("data-message", (data) => this.netplayDataMessage(data));
- this.netplay.socket.on("webrtc-signal", async(data) => {
- const { sender, offer, candidate, answer, requestRenegotiate } = data;
- console.log(`Received WebRTC signal from ${sender}:`, {
- offer: !!offer,
- answer: !!answer,
- candidate: !!candidate,
- requestRenegotiate
- });
- if (!sender && !requestRenegotiate) {
- console.warn("Ignoring signal with no sender and no renegotiation request", data);
- return;
- }
- if (requestRenegotiate && !sender) {
- console.warn("Ignoring renegotiation request with undefined sender", data);
- this.netplay.socket.emit("webrtc-signal-error", {
- error: "Renegotiation request missing sender",
- data
- });
- return;
- }
- let pcData = sender ? this.netplay.peerConnections[sender] : null;
-
- if (pcData && !pcData.iceCandidateQueue) {
- pcData.iceCandidateQueue = [];
- }
-
- if (!pcData && sender) {
- console.log("No existing peer connection for", sender, "- creating new one");
- pcData = {
- pc: this.netplayCreatePeerConnection(sender),
- dataChannel: null,
- iceCandidateQueue: []
- };
- this.netplay.peerConnections[sender] = pcData;
- }
- const pc = pcData.pc;
- try {
- if (offer) {
- console.log("Processing offer from", sender);
- await pc.setRemoteDescription(new RTCSessionDescription(offer));
-
- if (pcData.iceCandidateQueue.length > 0) {
- console.log(`Processing ${pcData.iceCandidateQueue.length} queued ICE candidates.`);
- for (const queuedCandidate of pcData.iceCandidateQueue) {
- await pc.addIceCandidate(new RTCIceCandidate(queuedCandidate));
- }
- pcData.iceCandidateQueue = [];
- }
-
- const answer = await pc.createAnswer();
- await pc.setLocalDescription(answer);
- console.log("Sending answer to", sender);
- this.netplay.socket.emit("webrtc-signal", {
- target: sender,
- answer: pc.localDescription
- });
- } else if (answer) {
- console.log("Processing answer from", sender);
- await pc.setRemoteDescription(new RTCSessionDescription(answer));
-
- if (pcData.iceCandidateQueue.length > 0) {
- console.log(`Processing ${pcData.iceCandidateQueue.length} queued ICE candidates.`);
- for (const queuedCandidate of pcData.iceCandidateQueue) {
- await pc.addIceCandidate(new RTCIceCandidate(queuedCandidate));
- }
- pcData.iceCandidateQueue = [];
- }
-
- } else if (candidate) {
- if (pc.remoteDescription) {
- console.log("Adding ICE candidate from", sender);
- await pc.addIceCandidate(new RTCIceCandidate(candidate));
- } else {
- console.log("Remote description not set. Queueing ICE candidate from", sender);
- pcData.iceCandidateQueue.push(candidate);
- }
- } else if (requestRenegotiate && this.netplay.owner) {
- console.log("Owner handling renegotiation request...");
- Object.keys(this.netplay.peerConnections).forEach(peerId => {
- if (peerId && this.netplay.peerConnections[peerId]) {
- const peerConn = this.netplay.peerConnections[peerId].pc;
- console.log("Closing and recreating peer connection for", peerId);
- peerConn.close();
- delete this.netplay.peerConnections[peerId];
- this.netplayCreatePeerConnection(peerId);
- }
- });
- }
- } catch (error) {
- console.error("WebRTC signaling error:", error);
- }
- });
- };
-
- this.netplayUpdatePlayersTable = () => {
- if (!this.netplay.playerTable) {
- console.error("netplay.playerTable is undefined");
- return;
- }
- const table = this.netplay.playerTable;
- table.innerHTML = "";
-
- const playerCount = Object.keys(this.netplay.players).length;
- const maxPlayers = this.netplay.maxPlayers || "?";
-
- const addToTable = (playerNumber, playerName, statusText) => {
- const row = this.createElement("tr");
- const addCell = (text) => {
- const item = this.createElement("td");
- item.innerText = text;
- row.appendChild(item);
- return item;
- };
- addCell(playerNumber).style.width = "80px";
- addCell(playerName);
- addCell(statusText).style.width = "80px";
- table.appendChild(row);
- };
-
- let i = 0;
- for (const k in this.netplay.players) {
- const playerNumber = i + 1;
- const playerName = this.netplay.players[k].player_name || "Unknown";
- const statusText = (i === 0) ? `${playerCount}/${maxPlayers}` : "";
- addToTable(playerNumber, playerName, statusText);
- i++;
- }
- };
-
- this.netplayOpenRoom = (roomName, maxPlayers, password) => {
- const sessionid = guidGenerator();
- this.netplay.playerID = guidGenerator();
- this.netplay.players = {};
- this.netplay.maxPlayers = maxPlayers;
- this.netplay.extra = {
- domain: window.location.host,
- game_id: this.config.gameId,
- room_name: roomName,
- player_name: this.netplay.name,
- userid: this.netplay.playerID,
- sessionid: sessionid
- };
- this.netplay.players[this.netplay.playerID] = this.netplay.extra;
- this.netplay.owner = true;
- this.netplayStartSocketIO(() => {
- this.netplay.socket.emit("open-room", {
- extra: this.netplay.extra,
- maxPlayers: maxPlayers,
- password: password
- }, (error) => {
- if (error) {
- console.error("Error opening room:", error);
- this.displayMessage("Failed to create room: " + error, 5000);
- return;
- }
- this.netplayRoomJoined(true, roomName, password, sessionid);
- });
- });
- };
-
- this.netplayJoinRoom = (sessionid, roomName, maxPlayers, password) => {
- this.netplay.playerID = guidGenerator();
- this.netplay.players = {};
- this.netplay.maxPlayers = maxPlayers;
- this.netplay.extra = {
- domain: window.location.host,
- game_id: this.config.gameId,
- room_name: roomName,
- player_name: this.netplay.name,
- userid: this.netplay.playerID,
- sessionid: sessionid
- };
- this.netplay.players[this.netplay.playerID] = this.netplay.extra;
- this.netplay.owner = false;
- this.netplayStartSocketIO(() => {
- this.netplay.socket.emit("join-room", {
- extra: this.netplay.extra,
- password: password
- }, (error, users) => {
- if (error) {
- console.error("Error joining room:", error);
- alert("Error joining room: " + error);
- return;
- }
- this.netplay.players = users;
- this.netplayRoomJoined(false, roomName, password, sessionid);
- });
- });
- };
-
- this.netplayRoomJoined = (isOwner, roomName, password, roomId) => {
- EJS_INSTANCE.updateNetplayUI(true);
-
- if (!this.netplay || !this.canvas || !this.elements || !this.elements.parent) {
- console.error("netplayRoomJoined: Required objects are undefined", {
- netplay: !!this.netplay,
- canvas: !!this.canvas,
- elements: !!this.elements,
- parent: !!(this.elements && this.elements.parent)
- });
- this.displayMessage("Failed to initialize netplay room", 5000);
- return;
- }
-
- if (!this.netplayCanvas) {
- this.netplayCanvas = this.createElement("canvas");
- this.netplayCanvas.classList.add("ejs_canvas");
- this.netplayCanvas.style.display = "none";
- this.netplayCanvas.style.position = "absolute";
- this.netplayCanvas.style.top = "0";
- this.netplayCanvas.style.left = "0";
- this.netplayCanvas.style.zIndex = "5";
- this.netplayCanvas.style.objectFit = "contain";
- this.netplayCanvas.style.width = "100%";
- this.netplayCanvas.style.height = "100%";
- this.netplayCanvas.style.objectPosition = "top";
- }
-
- this.isNetplay = true;
- this.netplay.inputs = {};
- this.netplay.owner = isOwner;
- console.log("Room joined with extra:", this.netplay.extra);
-
- if (this.netplay.roomNameElem) {
- this.netplay.roomNameElem.innerText = roomName;
- }
- if (this.netplay.tabs && this.netplay.tabs[0] && this.netplay.tabs[1]) {
- this.netplay.tabs[0].style.display = "none";
- this.netplay.tabs[1].style.display = "";
- }
- if (this.netplay.passwordElem) {
- if (password) {
- this.netplay.passwordElem.style.display = "";
- this.netplay.passwordElem.innerText = this.localization("Password") + ": " + password;
- } else {
- this.netplay.passwordElem.style.display = "none";
- }
- }
- if (this.netplay.createButton) {
- this.netplay.createButton.innerText = this.localization("Leave Room");
- }
- this.netplayUpdatePlayersTable();
-
- this.elements.parent.style.width = "100vw";
- this.elements.parent.style.height = "100vh";
- this.elements.parent.style.position = "relative";
-
- const { width: nativeWidth, height: nativeHeight } = this.getNativeResolution() || {
- width: 700,
- height: 720
- };
-
- if (!this.netplay.owner) {
- this.canvas.style.display = "none";
- if (!this.netplayCanvas.parentElement) {
- this.elements.parent.appendChild(this.netplayCanvas);
- console.log("Appended netplayCanvas to this.elements.parent:", this.elements.parent);
- }
- this.netplayCanvas.width = nativeWidth;
- this.netplayCanvas.height = nativeHeight;
- Object.assign(this.netplayCanvas.style, {
- position: 'absolute',
- top: '0',
- left: '0',
- width: '100%',
- height: 'auto',
- maxHeight: '100%',
- zIndex: '5',
- display: 'block',
- pointerEvents: 'none'
- });
-
- const parentStyles = window.getComputedStyle(this.elements.parent);
- console.log("Parent container styles:", {
- display: parentStyles.display,
- visibility: parentStyles.visibility,
- opacity: parentStyles.opacity,
- position: parentStyles.position,
- zIndex: parentStyles.zIndex
- });
-
- if (this.elements.bottomBar && this.elements.bottomBar.cheat && this.elements.bottomBar.cheat[0]) {
- this.netplay.oldStyles = [this.elements.bottomBar.cheat[0].style.display];
- this.elements.bottomBar.cheat[0].style.display = "none";
- }
- if (this.gameManager && this.gameManager.resetCheat) {
- this.gameManager.resetCheat();
- }
- console.log("Player 2 joined, awaiting WebRTC stream...");
- this.elements.parent.focus();
-
- if (this.gameManager && this.gameManager.functions && this.gameManager.functions.simulateInput) {
- const originalSimulateInput = this.gameManager.functions.simulateInput;
- this.gameManager.functions.simulateInput = (player, index, value) => {
- const playerIndex = this.netplayGetUserIndex();
- console.log("Player 2 input:", {
- player,
- index,
- value,
- playerIndex
- });
- Object.values(this.netplay.peerConnections).forEach((pcData) => {
- if (
- pcData.pc &&
- pcData.pc.connectionState === "connected" &&
- pcData.dataChannel &&
- pcData.dataChannel.readyState === "open"
- ) {
- pcData.dataChannel.send(
- JSON.stringify({
- player: playerIndex,
- index,
- value,
- }));
- }
- });
- };
- this.netplayLeaveRoom = (originalLeaveRoom => {
- return function () {
- originalLeaveRoom.call(this);
- this.gameManager.functions.simulateInput = originalSimulateInput;
- if (this.netplay.video && this.netplay.video.parentElement) {
- this.netplay.video.parentElement.removeChild(this.netplay.video);
- }
- };
- })(this.netplayLeaveRoom);
- } else {
- console.error("Cannot override simulateInput: gameManager.functions.simulateInput is undefined");
- }
-
- if (this.isMobile && this.gamepadElement) {
- const newGamepad = this.gamepadElement.cloneNode(true);
- this.gamepadElement.parentNode.replaceChild(newGamepad, this.gamepadElement);
- this.gamepadElement = newGamepad;
- Object.assign(this.gamepadElement.style, {
- zIndex: "1000",
- position: "absolute",
- pointerEvents: "auto"
- });
-
- this.gamepadElement.addEventListener("touchstart", (e) => {
- e.preventDefault();
- const button = e.target.closest('[data-button]');
- if (button && this.gameManager && this.gameManager.functions && this.gameManager.functions.simulateInput) {
- this.gameManager.functions.simulateInput(0, button.dataset.button, 1);
- }
- }, {
- passive: false
- });
-
- this.gamepadElement.addEventListener("touchend", (e) => {
- e.preventDefault();
- const button = e.target.closest('[data-button]');
- if (button && this.gameManager && this.gameManager.functions && this.gameManager.functions.simulateInput) {
- this.gameManager.functions.simulateInput(0, button.dataset.button, 0);
- }
- }, {
- passive: false
- });
-
- this.gamepadElement.focus();
- }
- const updateGamepadStyles = () => {
- if (this.isMobile && this.gamepadElement) {
- Object.assign(this.gamepadElement.style, {
- zIndex: "1000",
- position: "absolute",
- pointerEvents: "auto"
- });
- this.netplayCanvas.style.pointerEvents = "none";
- this.netplayCanvas.width = nativeWidth;
- this.netplayCanvas.height = nativeHeight;
- this.netplayCanvas.style.width = "100%";
- this.netplayCanvas.style.height = "100%";
- }
- };
- document.addEventListener("fullscreenchange", updateGamepadStyles);
- document.addEventListener("webkitfullscreenchange", updateGamepadStyles);
-
- setTimeout(() => {
- if (!this.netplay.webRtcReady) {
- console.error("WebRTC connection not established after timeout");
- this.displayMessage("Failed to connect to Player 1. Please check your network and try again.", 5000);
- if (this.interactionOverlay) {
- this.interactionOverlay.remove();
- this.interactionOverlay = null;
- }
- this.netplayLeaveRoom();
- }
- }, 10000);
- } else {
- if (this.canvas) {
- this.canvas.width = nativeWidth;
- this.canvas.height = nativeHeight;
- this.canvas.style.display = "block";
- this.canvas.style.objectFit = "contain";
- }
- if (this.netplayCanvas) {
- this.netplayCanvas.style.display = "none";
- }
- if (this.netplay.videoContainer) {
- this.netplay.videoContainer.style.display = "none";
- }
- if (this.elements.bottomBar && this.elements.bottomBar.cheat && this.elements.bottomBar.cheat[0]) {
- this.netplay.oldStyles = [this.elements.bottomBar.cheat[0].style.display];
- }
-
- if (this.netplay.owner && this.Module && this.Module.setCanvasSize) {
- this.Module.setCanvasSize(nativeWidth, nativeHeight);
- }
-
- this.netplay.lockedAspectRatio = nativeWidth / nativeHeight;
- const resizeCanvasWithAspect = () => {
- const aspect = this.netplay.lockedAspectRatio;
- const vw = window.innerWidth;
- const vh = window.innerHeight;
- let newWidth,
- newHeight;
-
- if (vw / vh > aspect) {
- newHeight = vh;
- newWidth = vh * aspect;
- } else {
- newWidth = vw;
- newHeight = vw / aspect;
- }
-
- if (this.canvas) {
- Object.assign(this.canvas.style, {
- width: `${newWidth}px`,
- height: `${newHeight}px`,
- display: "block",
- objectFit: "contain"
- });
-
- const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement;
-
- if (isFullscreen) {
- Object.assign(this.canvas.style, {
- position: "absolute",
- top: "0",
- left: "50%",
- transform: "translateX(-50%)"
- });
- } else {
- Object.assign(this.canvas.style, {
- position: "",
- left: "",
- top: "",
- transform: ""
- });
- }
- }
- };
- this._netplayResizeCanvas = resizeCanvasWithAspect;
- window.addEventListener("resize", resizeCanvasWithAspect);
- document.addEventListener("fullscreenchange", resizeCanvasWithAspect);
- document.addEventListener("webkitfullscreenchange", resizeCanvasWithAspect);
- resizeCanvasWithAspect();
- window.dispatchEvent(new Event('resize'));
- }
- };
-
- this.netplayLeaveRoom = () => {
- EJS_INSTANCE.updateNetplayUI(false);
-
- console.log("Leaving netplay room...");
-
- if (this.netplay.owner && this.netplaySendMessage) {
- this.netplaySendMessage({
- type: "host-left"
- });
- }
-
- if (this.netplay.socket && this.netplay.socket.connected) {
- this.netplay.socket.emit('leave-room');
- }
-
- if (this.netplay.socket) {
- this.netplay.socket.disconnect();
- this.netplay.socket = null;
- }
-
- if (this.netplay.localStream) {
- this.netplay.localStream.getTracks().forEach(track => track.stop());
- this.netplay.localStream = null;
- }
-
- if (this.netplay.peerConnections) {
- Object.values(this.netplay.peerConnections).forEach(pcData => {
- if (pcData.pc)
- pcData.pc.close();
- });
- this.netplay.peerConnections = {};
- }
-
- if (this.netplayCanvas && this.netplayCanvas.parentElement) {
- this.netplayCanvas.parentElement.removeChild(this.netplayCanvas);
- this.netplayCanvas.style.display = "none";
- }
- if (this.netplay.video && this.netplay.video.parentElement) {
- this.netplay.video.parentElement.removeChild(this.netplay.video);
- this.netplay.video.srcObject = null;
- this.netplay.video = null;
- }
- if (this.netplay.videoContainer) {
- this.netplay.videoContainer.style.display = "none";
- }
-
- if (this.canvas) {
- Object.assign(this.canvas.style, {
- display: "block",
- width: "100%",
- height: "100%",
- objectFit: "contain",
- position: "absolute",
- top: "0",
- left: "0",
- transform: "none"
- });
- }
-
- if (this.netplay.createButton) {
- this.netplay.createButton.innerText = this.localization("Create Room");
- }
- if (this.netplay.tabs) {
- this.netplay.tabs[0].style.display = "";
- this.netplay.tabs[1].style.display = "none";
- }
- if (this.netplay.roomNameElem) {
- this.netplay.roomNameElem.innerText = "";
- }
- if (this.netplay.passwordElem) {
- this.netplay.passwordElem.style.display = "none";
- this.netplay.passwordElem.innerText = "";
- }
- if (this.netplay.playerTable) {
- this.netplay.playerTable.innerHTML = "";
- }
-
- if (this.netplay.oldStyles && this.elements.bottomBar && this.elements.bottomBar.cheat && this.elements.bottomBar.cheat[0]) {
- this.elements.bottomBar.cheat[0].style.display = this.netplay.oldStyles[0] || "";
- }
-
- if (this._netplayResizeCanvas) {
- window.removeEventListener("resize", this._netplayResizeCanvas);
- document.removeEventListener("fullscreenchange", this._netplayResizeCanvas);
- document.removeEventListener("webkitfullscreenchange", this._netplayResizeCanvas);
- this._netplayResizeCanvas = null;
- }
-
- // Restore the original input function when leaving the room
- if (this.netplay.originalSimulateInput && this.gameManager && this.gameManager.functions) {
- this.gameManager.functions.simulateInput = this.netplay.originalSimulateInput;
- this.netplay.originalSimulateInput = null;
- }
-
- this.isNetplay = false;
- this.netplay.owner = false;
- this.netplay.players = {};
- this.netplay.playerID = null;
- this.netplay.inputs = {};
- this.netplay.inputsData = {};
- this.netplay.webRtcReady = false;
- this.netplay.lockedAspectRatio = null;
- this.player = 1;
-
- if (this.originalControls) {
- this.controls = JSON.parse(JSON.stringify(this.originalControls));
- this.originalControls = null;
- }
-
- if (this.isMobile && this.gamepadElement) {
- Object.assign(this.gamepadElement.style, {
- zIndex: "1000",
- position: "absolute",
- pointerEvents: "auto"
- });
+ name: "audio_latency", // String - value to be set in retroarch.cfg
+ // options should ALWAYS be strings here...
+ options: ["8", "16", "32", "64", "128"], // values
+ options: {"8": "eight", "16": "sixteen", "32": "thirty-two", "64": "sixty-four", "128": "one hundred-twenty-eight"}, // This also works
+ default: "128", // Default
+ isString: false // Surround value with quotes in retroarch.cfg file?
}
+ ];*/
- if (this.gameManager && this.gameManager.restart) {
- this.gameManager.restart();
- } else if (this.startGame) {
- this.startGame();
- }
+ if (this.retroarchOpts && Array.isArray(this.retroarchOpts)) {
+ const retroarchOptsMenu = createSettingParent(
+ true,
+ "RetroArch Options" +
+ " (" +
+ this.localization("Requires restart") +
+ ")",
+ home,
+ );
+ this.retroarchOpts.forEach((option) => {
+ addToMenu(
+ this.localization(
+ option.title,
+ this.config.settingsLanguage,
+ ),
+ option.name,
+ option.options,
+ option.default,
+ retroarchOptsMenu,
+ true,
+ );
+ });
+ checkForEmptyMenu(retroarchOptsMenu);
+ }
- this.displayMessage("Left the room", 3000);
- };
+ checkForEmptyMenu(graphicsOptions);
+ checkForEmptyMenu(speedOptions);
- this.netplayDataMessage = function (data) {
- if (data["sync-control"]) {
- data["sync-control"].forEach((value) => {
- let inFrame = parseInt(value.frame);
- if (!value.connected_input || value.connected_input[0] < 0)
- return;
- this.netplay.inputsData[inFrame] = this.netplay.inputsData[inFrame] || [];
- this.netplay.inputsData[inFrame].push(value);
- this.netplaySendMessage({
- frameAck: inFrame
- });
- if (this.netplay.owner) {
- console.log("Owner processing input:", value.connected_input);
- if (this.gameManager && this.gameManager.functions && this.gameManager.functions.simulateInput) {
- this.gameManager.functions.simulateInput(
- value.connected_input[0],
- value.connected_input[1],
- value.connected_input[2]);
- } else {
- console.error("Cannot process input: gameManager.functions.simulateInput is undefined");
- }
- }
- });
- }
- if (data.frameData) {
- console.log("Received frame data on Player 2:", data.frameData);
- if (!this.canvas) {
- console.error("Canvas unavailable for frame data processing");
- return;
- }
- const ctx = this.canvas.getContext('2d');
- if (!ctx) {
- console.error("Canvas context unavailable for frame data processing");
- return;
- }
- if (data.frameData.pixelSample.every(v => v === 0)) {
- console.warn("Frame data indicates black screen, attempting reconstruction");
- if (this.reconstructFrame) {
- this.reconstructFrame(data.frameData.inputs);
- } else {
- console.error("reconstructFrame is undefined");
- }
- } else {
- console.log("Frame data indicates content, relying on WebRTC stream");
- }
- }
- };
+ this.settingsMenu.appendChild(nested);
- this.netplaySendMessage = (data) => {
- if (this.netplay.socket && this.netplay.socket.connected) {
- this.netplay.socket.emit("data-message", data);
- console.log("Sent data message:", data);
- } else {
- console.error("Cannot send message: Socket is not connected");
- }
- };
+ this.settingParent.appendChild(this.settingsMenu);
+ this.settingParent.style.position = "relative";
- this.netplayReset = () => {
- this.netplay.init_frame = this.gameManager ? this.gameManager.getFrameNum() : 0;
- this.netplay.currentFrame = 0;
- this.netplay.inputsData = {};
- this.netplay.syncing = false;
- };
+ this.settingsMenu.style.display = "";
+ const homeSize = this.getElementSize(home);
+ nested.style.width = homeSize.width + 20 + "px";
+ nested.style.height = homeSize.height + "px";
- this.netplayInitModulePostMainLoop = () => {
- if (this.isNetplay && !this.netplay.owner) {
- return;
- }
+ this.settingsMenu.style.display = "none";
- this.netplay.currentFrame = parseInt(this.gameManager ? this.gameManager.getFrameNum() : 0) - (this.netplay.init_frame || 0);
- if (!this.isNetplay)
- return;
+ if (this.debug) {
+ console.log("Available core options", allOpts);
+ }
- if (this.netplay.owner) {
- let to_send = [];
- let i = this.netplay.currentFrame;
- if (this.netplay.inputsData[i]) {
- this.netplay.inputsData[i].forEach((value) => {
- if (this.gameManager && this.gameManager.functions && this.gameManager.functions.simulateInput) {
- this.gameManager.functions.simulateInput(
- value.connected_input[0],
- value.connected_input[1],
- value.connected_input[2]);
- }
- value.frame = this.netplay.currentFrame + 20;
- to_send.push(value);
- });
- this.netplaySendMessage({
- "sync-control": to_send
- });
- delete this.netplay.inputsData[i];
- }
+ if (this.config.defaultOptions) {
+ for (const k in this.config.defaultOptions) {
+ this.changeSettingOption(
+ k,
+ this.config.defaultOptions[k],
+ true,
+ );
}
- };
-
- this.netplay.updateList = {
- start: this.netplayUpdateListStart,
- stop: this.netplayUpdateListStop
- };
- this.netplay.showOpenRoomDialog = this.netplayShowOpenRoomDialog;
- this.netplay.openRoom = this.netplayOpenRoom;
- this.netplay.joinRoom = this.netplayJoinRoom;
- this.netplay.leaveRoom = this.netplayLeaveRoom;
- this.netplay.sendMessage = this.netplaySendMessage;
- this.netplay.updatePlayersTable = this.netplayUpdatePlayersTable;
- this.netplay.createPeerConnection = this.netplayCreatePeerConnection;
- this.netplay.initWebRTCStream = this.netplayInitWebRTCStream;
- this.netplay.roomJoined = this.netplayRoomJoined;
-
- this.netplay = this.netplay || {};
- this.netplay.init_frame = 0;
- this.netplay.currentFrame = 0;
- this.netplay.inputsData = {};
- this.netplay.syncing = false;
- this.netplay.ready = 0;
- this.netplay.webRtcReady = false;
- this.netplay.peerConnections = this.netplay.peerConnections || {};
-
- this.netplay.url = this.config.netplayUrl || window.EJS_netplayUrl;
-
- if (!this.netplay.url) {
- if (this.debug) console.error("netplayUrl is not defined. Please set it in EJS_config or as a global EJS_netplayUrl variable.");
- this.displayMessage("Network configuration error: netplay URL is not set.", 5000);
- return;
}
- while (this.netplay.url.endsWith("/")) {
- this.netplay.url = this.netplay.url.substring(0, this.netplay.url.length - 1);
- }
- this.netplay.current_frame = 0;
-
- if (this.gameManager && this.gameManager.Module) {
- this.gameManager.Module.postMainLoop = this.netplayInitModulePostMainLoop.bind(this);
- } else if (this.Module) {
- this.Module.postMainLoop = this.netplayInitModulePostMainLoop.bind(this);
- } else if (this.debug) {
- console.warn("Module is undefined. postMainLoop will not be set.");
+ if (parentMenuCt === 0) {
+ this.on("start", () => {
+ this.elements.bottomBar.settings[0][0].style.display = "none";
+ });
}
}
- createCheatsMenu() {
- const body = this.createPopup("Cheats", {
- "Add Cheat": () => {
- const popups = this.createSubPopup();
- this.cheatMenu.appendChild(popups[0]);
- popups[1].classList.add("ejs_cheat_parent");
- popups[1].style.width = "100%";
- const popup = popups[1];
- const header = this.createElement("div");
- header.classList.add("ejs_cheat_header");
- const title = this.createElement("h2");
- title.innerText = this.localization("Add Cheat Code");
- title.classList.add("ejs_cheat_heading");
- const close = this.createElement("button");
- close.classList.add("ejs_cheat_close");
- header.appendChild(title);
- header.appendChild(close);
- popup.appendChild(header);
- this.addEventListener(close, "click", (e) => {
- popups[0].remove();
- })
+ createSubPopup(hidden) {
+ const popup = this.createElement("div");
+ popup.classList.add("ejs_popup_container");
+ popup.classList.add("ejs_popup_container_box");
+ const popupMsg = this.createElement("div");
+ popupMsg.innerText = "";
+ if (hidden) popup.setAttribute("hidden", "");
+ popup.appendChild(popupMsg);
+ return [popup, popupMsg];
+ }
- let cheatDB = {};
- const systemKey = this.getCore(true);
- const cleanRomTags = (name) => {
- return name.replace(/\([^)]+\)/g, '').replace(/\[[^\]]+\]/g, '').trim();
- };
+ createCheatsMenu() {
+ const body = this.createPopup(
+ "Cheats",
+ {
+ "Add Cheat": () => {
+ const popups = this.createSubPopup();
+ this.cheatMenu.appendChild(popups[0]);
+ popups[1].classList.add("ejs_cheat_parent");
+ popups[1].style.width = "100%";
+ const popup = popups[1];
+ const header = this.createElement("div");
+ header.classList.add("ejs_cheat_header");
+ const title = this.createElement("h2");
+ title.innerText = this.localization("Add Cheat Code");
+ title.classList.add("ejs_cheat_heading");
+ const close = this.createElement("button");
+ close.classList.add("ejs_cheat_close");
+ header.appendChild(title);
+ header.appendChild(close);
+ popup.appendChild(header);
+ this.addEventListener(close, "click", (e) => {
+ popups[0].remove();
+ });
- const normalizeAndConvertNumerals = (name) => {
- let normalized = name.toLowerCase();
- normalized = normalized.replace(/ iv/g, ' 4');
- normalized = normalized.replace(/ iii/g, ' 3');
- normalized = normalized.replace(/ ii/g, ' 2');
- normalized = normalized.replace(/ v/g, ' 5');
- normalized = normalized.replace(/ i/g, ' 1');
+ let cheatDB = {};
+ const systemKey = this.getCore(true);
+ const cleanRomTags = (name) => {
+ return name
+ .replace(/\([^)]+\)/g, "")
+ .replace(/\[[^\]]+\]/g, "")
+ .trim();
+ };
- return normalized.replace(/[^a-z0-9]/g, '');
- };
+ const normalizeAndConvertNumerals = (name) => {
+ let normalized = name.toLowerCase();
+ normalized = normalized.replace(/ iv/g, " 4");
+ normalized = normalized.replace(/ iii/g, " 3");
+ normalized = normalized.replace(/ ii/g, " 2");
+ normalized = normalized.replace(/ v/g, " 5");
+ normalized = normalized.replace(/ i/g, " 1");
- const createSelect = (labelText) => {
- const div = this.createElement("div");
- const label = this.createElement("strong");
- label.innerText = this.localization(labelText);
- div.appendChild(label);
- div.appendChild(this.createElement("br"));
- const select = this.createElement("select");
- select.style.width = "100%";
- select.classList.add("ejs_cheat_code");
- div.appendChild(select);
- return {
- container: div,
- select: select
+ return normalized.replace(/[^a-z0-9]/g, "");
};
- };
- const importDiv = this.createElement("div");
- importDiv.classList.add("ejs_cheat_main");
- importDiv.style.borderBottom = "1px solid #555";
- importDiv.style.paddingBottom = "10px";
- importDiv.style.display = 'none';
-
- const importTitle = this.createElement("h3");
- importTitle.innerText = this.localization("Import from Database") + (systemKey ? ` (${systemKey.toUpperCase()})` : "");
- importTitle.style.marginTop = "0px";
- importDiv.appendChild(importTitle);
-
- const gameSelectUI = createSelect("Game");
- const cheatSelectUI = createSelect("Cheat");
-
- importDiv.appendChild(gameSelectUI.container);
- importDiv.appendChild(cheatSelectUI.container);
-
- popup.appendChild(importDiv);
-
- const main = this.createElement("div");
- main.classList.add("ejs_cheat_main");
- const header3 = this.createElement("strong");
- header3.innerText = this.localization("Manual Entry - Code");
- main.appendChild(header3);
- main.appendChild(this.createElement("br"));
-
- const manualCodeTextarea = this.createElement("textarea");
- manualCodeTextarea.classList.add("ejs_cheat_code");
- manualCodeTextarea.style.width = "100%";
- manualCodeTextarea.style.height = "80px";
- main.appendChild(manualCodeTextarea);
- main.appendChild(this.createElement("br"));
-
- const header2 = this.createElement("strong");
- header2.innerText = this.localization("Manual Entry - Description");
- main.appendChild(header2);
- main.appendChild(this.createElement("br"));
-
- const manualDescriptionInput = this.createElement("input");
- manualDescriptionInput.type = "text";
- manualDescriptionInput.classList.add("ejs_cheat_code");
- manualDescriptionInput.style.width = "100%";
- main.appendChild(manualDescriptionInput);
- main.appendChild(this.createElement("br"));
- popup.appendChild(main);
-
-
- const loadCheatList = (gameName) => {
- cheatSelectUI.select.innerHTML = "";
-
- const defaultOpt = this.createElement("option");
- defaultOpt.value = "";
- defaultOpt.innerText = "--- " + this.localization("Select a Cheat") + " ---";
- cheatSelectUI.select.appendChild(defaultOpt);
-
- manualCodeTextarea.value = "";
- manualDescriptionInput.value = "";
-
- if (!gameName || !cheatDB[gameName]) return;
-
- const cheats = cheatDB[gameName];
- cheats.forEach(cheat => {
- const opt = this.createElement("option");
- opt.value = cheat.desc;
- opt.innerText = cheat.desc;
- cheatSelectUI.select.appendChild(opt);
- });
+ const createSelect = (labelText) => {
+ const div = this.createElement("div");
+ const label = this.createElement("strong");
+ label.innerText = this.localization(labelText);
+ div.appendChild(label);
+ div.appendChild(this.createElement("br"));
+ const select = this.createElement("select");
+ select.style.width = "100%";
+ select.classList.add("ejs_cheat_code");
+ div.appendChild(select);
+ return {
+ container: div,
+ select: select,
+ };
+ };
- if (cheats.length > 0) {
- cheatSelectUI.select.value = cheats[0].desc;
- manualCodeTextarea.value = cheats[0].code;
- manualDescriptionInput.value = cheats[0].desc;
- }
- };
+ const importDiv = this.createElement("div");
+ importDiv.classList.add("ejs_cheat_main");
+ importDiv.style.borderBottom = "1px solid #555";
+ importDiv.style.paddingBottom = "10px";
+ importDiv.style.display = "none";
- const loadCheatDatabase = async (system) => {
- gameSelectUI.select.innerHTML = "";
- cheatSelectUI.select.innerHTML = "";
+ const importTitle = this.createElement("h3");
+ importTitle.innerText =
+ this.localization("Import from Database") +
+ (systemKey ? ` (${systemKey.toUpperCase()})` : "");
+ importTitle.style.marginTop = "0px";
+ importDiv.appendChild(importTitle);
- const defaultGameOpt = this.createElement("option");
- defaultGameOpt.value = "";
- defaultGameOpt.innerText = "--- " + this.localization("Select a Game") + " ---";
- gameSelectUI.select.appendChild(defaultGameOpt);
+ const gameSelectUI = createSelect("Game");
+ const cheatSelectUI = createSelect("Cheat");
- if (!this.config.cheatPath) {
- if (this.debug) console.error("Cheat file load error: EJS_cheatPath is not configured.");
- importDiv.style.display = 'none';
- return;
- }
+ importDiv.appendChild(gameSelectUI.container);
+ importDiv.appendChild(cheatSelectUI.container);
- const url = this.config.cheatPath + system + ".json";
+ popup.appendChild(importDiv);
- try {
- const res = await this.downloadFile(url, null, true, {
- responseType: "text",
- method: "GET"
- });
+ const main = this.createElement("div");
+ main.classList.add("ejs_cheat_main");
+ const header3 = this.createElement("strong");
+ header3.innerText = this.localization(
+ "Manual Entry - Code",
+ );
+ main.appendChild(header3);
+ main.appendChild(this.createElement("br"));
+
+ const manualCodeTextarea = this.createElement("textarea");
+ manualCodeTextarea.classList.add("ejs_cheat_code");
+ manualCodeTextarea.style.width = "100%";
+ manualCodeTextarea.style.height = "80px";
+ main.appendChild(manualCodeTextarea);
+ main.appendChild(this.createElement("br"));
+
+ const header2 = this.createElement("strong");
+ header2.innerText = this.localization(
+ "Manual Entry - Description",
+ );
+ main.appendChild(header2);
+ main.appendChild(this.createElement("br"));
+
+ const manualDescriptionInput = this.createElement("input");
+ manualDescriptionInput.type = "text";
+ manualDescriptionInput.classList.add("ejs_cheat_code");
+ manualDescriptionInput.style.width = "100%";
+ main.appendChild(manualDescriptionInput);
+ main.appendChild(this.createElement("br"));
+ popup.appendChild(main);
+
+ const loadCheatList = (gameName) => {
+ cheatSelectUI.select.innerHTML = "";
+
+ const defaultOpt = this.createElement("option");
+ defaultOpt.value = "";
+ defaultOpt.innerText =
+ "--- " +
+ this.localization("Select a Cheat") +
+ " ---";
+ cheatSelectUI.select.appendChild(defaultOpt);
- let data;
- if (res === -1) {
- throw new Error("Cheat JSON not found. Create a file at: " + url);
- } else {
- data = res.data;
- }
+ manualCodeTextarea.value = "";
+ manualDescriptionInput.value = "";
- cheatDB = data;
- importDiv.style.display = '';
+ if (!gameName || !cheatDB[gameName]) return;
- const gameNames = Object.keys(cheatDB).sort();
- gameNames.forEach(name => {
+ const cheats = cheatDB[gameName];
+ cheats.forEach((cheat) => {
const opt = this.createElement("option");
- opt.value = name;
- opt.innerText = name;
- gameSelectUI.select.appendChild(opt);
+ opt.value = cheat.desc;
+ opt.innerText = cheat.desc;
+ cheatSelectUI.select.appendChild(opt);
});
- let currentFileBaseName = this.getBaseFileName(true);
- currentFileBaseName = currentFileBaseName.replace(/\.[^/.]+$/, "");
- const cleanedFileName = cleanRomTags(currentFileBaseName);
- const normalizedFile = normalizeAndConvertNumerals(cleanedFileName);
- let matchedGameName = null;
- if (this.config.gameName && gameNames.includes(this.config.gameName)) {
- matchedGameName = this.config.gameName;
+ if (cheats.length > 0) {
+ cheatSelectUI.select.value = cheats[0].desc;
+ manualCodeTextarea.value = cheats[0].code;
+ manualDescriptionInput.value = cheats[0].desc;
+ }
+ };
+
+ const loadCheatDatabase = async (system) => {
+ gameSelectUI.select.innerHTML = "";
+ cheatSelectUI.select.innerHTML = "";
+
+ const defaultGameOpt = this.createElement("option");
+ defaultGameOpt.value = "";
+ defaultGameOpt.innerText =
+ "--- " +
+ this.localization("Select a Game") +
+ " ---";
+ gameSelectUI.select.appendChild(defaultGameOpt);
+
+ if (!this.config.cheatPath) {
+ if (this.debug)
+ console.error(
+ "Cheat file load error: EJS_cheatPath is not configured.",
+ );
+ importDiv.style.display = "none";
+ return;
}
- if (!matchedGameName) {
- for (const name of gameNames) {
- if (normalizeAndConvertNumerals(name) === normalizedFile) {
- matchedGameName = name;
- break;
+ const globalUrl = this.config.cheatPath + "cheats.json";
+ const systemUrl =
+ this.config.cheatPath + system + ".json";
+
+ try {
+ let response = await fetch(globalUrl);
+ if (!response.ok) {
+ if (this.debug)
+ console.log(
+ `[Cheats] cheats.json not found. Trying ${system}.json fallback...`,
+ );
+ response = await fetch(systemUrl);
+ if (!response.ok) {
+ throw new Error(
+ `Cheat JSON not found at ${globalUrl} or ${systemUrl}`,
+ );
}
}
- }
- if (matchedGameName) {
- gameSelectUI.select.value = matchedGameName;
- }
+ let data = await response.json();
+ if (
+ data &&
+ data.data &&
+ typeof data.data === "object" &&
+ !Array.isArray(data.data)
+ ) {
+ data = data.data;
+ }
+ if (data && data.systems && data.systems[system]) {
+ cheatDB = data.systems[system];
+ } else if (data && data[system]) {
+ cheatDB = data[system];
+ } else {
+ cheatDB = data;
+ }
- loadCheatList(gameSelectUI.select.value);
+ importDiv.style.display = "";
- } catch (e) {
- if (this.debug) console.error("Cheat file load error:", e.message);
- importDiv.style.display = 'none';
- cheatDB = {};
- loadCheatList(null);
- }
- };
+ const gameNames = Object.keys(cheatDB).sort();
+ gameNames.forEach((name) => {
+ const opt = this.createElement("option");
+ opt.value = name;
+ opt.innerText = name;
+ gameSelectUI.select.appendChild(opt);
+ });
- gameSelectUI.select.addEventListener("change", () => {
- loadCheatList(gameSelectUI.select.value);
- });
+ let currentFileBaseName =
+ this.getBaseFileName(true);
+ currentFileBaseName = currentFileBaseName.replace(
+ /\.[^/.]+$/,
+ "",
+ );
+ const cleanedFileName =
+ cleanRomTags(currentFileBaseName);
+ const normalizedFile =
+ normalizeAndConvertNumerals(cleanedFileName);
+
+ let matchedGameName = null;
+ if (
+ this.config.gameName &&
+ gameNames.includes(this.config.gameName)
+ ) {
+ matchedGameName = this.config.gameName;
+ }
- cheatSelectUI.select.addEventListener("change", () => {
- const game = gameSelectUI.select.value;
- const cheatDesc = cheatSelectUI.select.value;
+ if (!matchedGameName) {
+ for (const name of gameNames) {
+ if (
+ normalizeAndConvertNumerals(name) ===
+ normalizedFile
+ ) {
+ matchedGameName = name;
+ break;
+ }
+ }
+ }
- if (!game || !cheatDesc) {
- manualCodeTextarea.value = "";
- manualDescriptionInput.value = "";
- return;
- }
+ if (matchedGameName) {
+ gameSelectUI.select.value = matchedGameName;
+ }
- const cheat = cheatDB[game].find(c => c.desc === cheatDesc);
- if (cheat) {
- manualCodeTextarea.value = cheat.code;
- manualDescriptionInput.value = cheat.desc;
- }
- });
+ loadCheatList(gameSelectUI.select.value);
+ } catch (e) {
+ if (this.debug)
+ console.error(
+ "Cheat file load error:",
+ e.message,
+ );
+ importDiv.style.display = "none";
+ cheatDB = {};
+ loadCheatList(null);
+ }
+ };
- if (systemKey) {
- loadCheatDatabase(systemKey).catch(e => {
- if (this.debug) console.error("Initial cheat load failed:", e);
+ gameSelectUI.select.addEventListener("change", () => {
+ loadCheatList(gameSelectUI.select.value);
});
- } else {
- importDiv.style.display = 'none';
- }
- const footer = this.createElement("footer");
- const submit = this.createElement("button");
- const closeButton = this.createElement("button");
- submit.innerText = this.localization("Submit");
- closeButton.innerText = this.localization("Close");
- submit.classList.add("ejs_button_button");
- closeButton.classList.add("ejs_button_button");
- submit.classList.add("ejs_popup_submit");
- closeButton.classList.add("ejs_popup_submit");
- submit.style["background-color"] = "rgba(var(--ejs-primary-color),1)";
- footer.appendChild(submit);
- const span = this.createElement("span");
- span.innerText = " ";
- footer.appendChild(span);
- footer.appendChild(closeButton);
- popup.appendChild(footer);
+ cheatSelectUI.select.addEventListener("change", () => {
+ const game = gameSelectUI.select.value;
+ const cheatDesc = cheatSelectUI.select.value;
- this.addEventListener(submit, "click", (e) => {
- if (!manualCodeTextarea.value.trim() || !manualDescriptionInput.value.trim()) return;
- popups[0].remove();
- this.cheats.push({
- code: manualCodeTextarea.value,
- desc: manualDescriptionInput.value,
- checked: false
+ if (!game || !cheatDesc) {
+ manualCodeTextarea.value = "";
+ manualDescriptionInput.value = "";
+ return;
+ }
+
+ const cheat = cheatDB[game].find(
+ (c) => c.desc === cheatDesc,
+ );
+ if (cheat) {
+ manualCodeTextarea.value = cheat.code;
+ manualDescriptionInput.value = cheat.desc;
+ }
});
- this.updateCheatUI();
- this.saveSettings();
- })
- this.addEventListener(closeButton, "click", (e) => {
- popups[0].remove();
- })
+
+ if (systemKey) {
+ loadCheatDatabase(systemKey).catch((e) => {
+ if (this.debug)
+ console.error("Initial cheat load failed:", e);
+ });
+ } else {
+ importDiv.style.display = "none";
+ }
+
+ const footer = this.createElement("footer");
+ const submit = this.createElement("button");
+ const closeButton = this.createElement("button");
+ submit.innerText = this.localization("Submit");
+ closeButton.innerText = this.localization("Close");
+ submit.classList.add("ejs_button_button");
+ closeButton.classList.add("ejs_button_button");
+ submit.classList.add("ejs_popup_submit");
+ closeButton.classList.add("ejs_popup_submit");
+ submit.style["background-color"] =
+ "rgba(var(--ejs-primary-color),1)";
+ footer.appendChild(submit);
+ const span = this.createElement("span");
+ span.innerText = " ";
+ footer.appendChild(span);
+ footer.appendChild(closeButton);
+ popup.appendChild(footer);
+
+ this.addEventListener(submit, "click", (e) => {
+ if (
+ !manualCodeTextarea.value.trim() ||
+ !manualDescriptionInput.value.trim()
+ )
+ return;
+ popups[0].remove();
+ this.cheats.push({
+ code: manualCodeTextarea.value,
+ desc: manualDescriptionInput.value,
+ checked: false,
+ });
+ this.updateCheatUI();
+ this.saveSettings();
+ });
+ this.addEventListener(closeButton, "click", (e) => {
+ popups[0].remove();
+ });
+ },
+ Close: () => {
+ this.cheatMenu.style.display = "none";
+ },
},
- "Close": () => {
- this.cheatMenu.style.display = "none";
- }
- }, true);
+ true,
+ );
this.cheatMenu = body.parentElement;
- this.cheatMenu.getElementsByTagName("h4")[0].style["padding-bottom"] = "0px";
+ this.cheatMenu.getElementsByTagName("h4")[0].style["padding-bottom"] =
+ "0px";
const msg = this.createElement("div");
msg.style["padding-top"] = "0px";
msg.style["padding-bottom"] = "15px";
- msg.innerText = this.localization("Note that some cheats require a restart to disable");
+ msg.innerText = this.localization(
+ "Note that some cheats require a restart to disable",
+ );
body.appendChild(msg);
const rows = this.createElement("div");
body.appendChild(rows);
@@ -7468,7 +8857,7 @@ class EmulatorJS {
this.cheats[i].checked = input.checked;
this.cheatChanged(input.checked, code, i);
this.saveSettings();
- })
+ });
if (!is_permanent) {
const close = this.createElement("a");
close.classList.add("ejs_cheat_row_button");
@@ -7479,14 +8868,20 @@ class EmulatorJS {
this.cheats.splice(i, 1);
this.updateCheatUI();
this.saveSettings();
- })
+ });
}
this.elements.cheatRows.appendChild(row);
this.cheatChanged(checked, code, i);
- }
+ };
this.gameManager.resetCheat();
for (let i = 0; i < this.cheats.length; i++) {
- addToMenu(this.cheats[i].desc, this.cheats[i].checked, this.cheats[i].code, this.cheats[i].is_permanent, i);
+ addToMenu(
+ this.cheats[i].desc,
+ this.cheats[i].checked,
+ this.cheats[i].code,
+ this.cheats[i].is_permanent,
+ i,
+ );
}
}
cheatChanged(checked, code, index) {
@@ -7498,7 +8893,7 @@ class EmulatorJS {
if (!this.gameManager) return;
try {
this.Module.FS.unlink("/shader/shader.glslp");
- } catch(e) {}
+ } catch (e) {}
if (name === "disabled" || !this.shaders[name]) {
this.gameManager.toggleShader(0);
@@ -7508,13 +8903,30 @@ class EmulatorJS {
const shaderConfig = this.shaders[name];
if (typeof shaderConfig === "string") {
- this.Module.FS.writeFile("/shader/shader.glslp", shaderConfig, {}, "w+");
+ this.Module.FS.writeFile(
+ "/shader/shader.glslp",
+ shaderConfig,
+ {},
+ "w+",
+ );
} else {
const shader = shaderConfig.shader;
- this.Module.FS.writeFile("/shader/shader.glslp", shader.type === "base64" ? atob(shader.value) : shader.value, {}, "w+");
+ this.Module.FS.writeFile(
+ "/shader/shader.glslp",
+ shader.type === "base64" ? atob(shader.value) : shader.value,
+ {},
+ "w+",
+ );
if (shaderConfig.resources && shaderConfig.resources.length) {
- shaderConfig.resources.forEach(resource => {
- this.Module.FS.writeFile(`/shader/${resource.name}`, resource.type === "base64" ? atob(resource.value) : resource.value, {}, "w+");
+ shaderConfig.resources.forEach((resource) => {
+ this.Module.FS.writeFile(
+ `/shader/${resource.name}`,
+ resource.type === "base64"
+ ? atob(resource.value)
+ : resource.value,
+ {},
+ "w+",
+ );
});
}
}
@@ -7523,27 +8935,41 @@ class EmulatorJS {
}
screenshot(callback, source, format, upscale) {
- const imageFormat = format || this.getSettingValue("screenshotFormat") || this.capture.photo.format;
- const imageUpscale = upscale || parseInt(this.getSettingValue("screenshotUpscale") || this.capture.photo.upscale);
- const screenshotSource = source || this.getSettingValue("screenshotSource") || this.capture.photo.source;
- const videoRotation = parseInt(this.getSettingValue("videoRotation") || 0);
- const aspectRatio = this.gameManager.getVideoDimensions("aspect") || 1.333333;
+ const imageFormat =
+ format ||
+ this.getSettingValue("screenshotFormat") ||
+ this.capture.photo.format;
+ const imageUpscale =
+ upscale ||
+ parseInt(
+ this.getSettingValue("screenshotUpscale") ||
+ this.capture.photo.upscale,
+ );
+ const screenshotSource =
+ source ||
+ this.getSettingValue("screenshotSource") ||
+ this.capture.photo.source;
+ const videoRotation = parseInt(
+ this.getSettingValue("videoRotation") || 0,
+ );
+ const aspectRatio =
+ this.gameManager.getVideoDimensions("aspect") || 1.333333;
const gameWidth = this.gameManager.getVideoDimensions("width") || 256;
const gameHeight = this.gameManager.getVideoDimensions("height") || 224;
- const videoTurned = (videoRotation === 1 || videoRotation === 3);
+ const videoTurned = videoRotation === 1 || videoRotation === 3;
let width = this.canvas.width;
let height = this.canvas.height;
let scaleHeight = imageUpscale;
let scaleWidth = imageUpscale;
let scale = 1;
-
+
if (screenshotSource === "retroarch") {
if (width >= height) {
width = height * aspectRatio;
} else if (width < height) {
height = width / aspectRatio;
}
- this.gameManager.screenshot().then(screenshot => {
+ this.gameManager.screenshot().then((screenshot) => {
const blob = new Blob([screenshot], { type: "image/png" });
if (imageUpscale === 0) {
callback(blob, "png");
@@ -7560,13 +8986,17 @@ class EmulatorJS {
ctx.imageSmoothingEnabled = false;
ctx.scale(scaleWidth, scaleHeight);
ctx.drawImage(img, 0, 0, width, height);
- canvas.toBlob((blob) => {
- callback(blob, imageFormat);
- img.remove();
- URL.revokeObjectURL(screenshotUrl);
- canvas.remove();
- }, "image/" + imageFormat, 1);
- }
+ canvas.toBlob(
+ (blob) => {
+ callback(blob, imageFormat);
+ img.remove();
+ URL.revokeObjectURL(screenshotUrl);
+ canvas.remove();
+ },
+ "image/" + imageFormat,
+ 1,
+ );
+ };
}
});
} else if (screenshotSource === "canvas") {
@@ -7575,9 +9005,9 @@ class EmulatorJS {
} else if (width < height && !videoTurned) {
height = width / aspectRatio;
} else if (width >= height && videoTurned) {
- width = height * (1/aspectRatio);
+ width = height * (1 / aspectRatio);
} else if (width < height && videoTurned) {
- width = height / (1/aspectRatio);
+ width = height / (1 / aspectRatio);
}
if (imageUpscale === 0) {
scale = gameHeight / height;
@@ -7604,11 +9034,21 @@ class EmulatorJS {
offsetY = (this.canvas.height - height) / -2;
}
const drawNextFrame = () => {
- captureCtx.drawImage(this.canvas, offsetX, offsetY, this.canvas.width, this.canvas.height);
- captureCanvas.toBlob((blob) => {
- callback(blob, imageFormat);
- captureCanvas.remove();
- }, "image/" + imageFormat, 1);
+ captureCtx.drawImage(
+ this.canvas,
+ offsetX,
+ offsetY,
+ this.canvas.width,
+ this.canvas.height,
+ );
+ captureCanvas.toBlob(
+ (blob) => {
+ callback(blob, imageFormat);
+ captureCanvas.remove();
+ },
+ "image/" + imageFormat,
+ 1,
+ );
};
requestAnimationFrame(drawNextFrame);
}
@@ -7616,65 +9056,244 @@ class EmulatorJS {
takeScreenshot(source, format, upscale) {
return new Promise((resolve) => {
- this.screenshot(async (blob, returnFormat) => {
- const arrayBuffer = await blob.arrayBuffer();
- const uint8 = new Uint8Array(arrayBuffer);
- resolve({ screenshot: uint8, format: returnFormat });
- }, source, format, upscale);
+ this.screenshot(
+ async (blob, returnFormat) => {
+ const arrayBuffer = await blob.arrayBuffer();
+ const uint8 = new Uint8Array(arrayBuffer);
+ resolve({ screenshot: uint8, format: returnFormat });
+ },
+ source,
+ format,
+ upscale,
+ );
});
}
collectScreenRecordingMediaTracks(canvasEl, fps) {
+ if (this.debug) console.log("collectScreenRecordingMediaTracks");
+ if (this.debug)
+ console.log("Canvas: " + canvasEl.width + "x" + canvasEl.height);
+
let videoTrack = null;
const videoTracks = canvasEl.captureStream(fps).getVideoTracks();
+ if (this.debug)
+ console.log(
+ "Video tracks from captureStream: " + videoTracks.length,
+ );
+
if (videoTracks.length !== 0) {
videoTrack = videoTracks[0];
+ if (this.debug)
+ console.log(
+ "Video track: " +
+ videoTrack.label +
+ " " +
+ videoTrack.readyState,
+ );
} else {
if (this.debug) console.error("Unable to capture video stream");
return null;
}
let audioTrack = null;
- if (this.Module.AL && this.Module.AL.currentCtx && this.Module.AL.currentCtx.audioCtx) {
+
+ if (
+ this.Module &&
+ this.Module.AL &&
+ this.Module.AL.currentCtx &&
+ this.Module.AL.currentCtx.audioCtx
+ ) {
const alContext = this.Module.AL.currentCtx;
const audioContext = alContext.audioCtx;
+ if (this.debug)
+ console.log("AL AudioContext state: " + audioContext.state);
+ if (this.debug)
+ console.log(
+ "AL sources: " +
+ Object.keys(alContext.sources || {}).length,
+ );
+
+ if (audioContext.state === "suspended") {
+ audioContext.resume().catch((e) => {
+ if (this.debug)
+ console.error("Failed to resume AudioContext:", e);
+ });
+ }
+
const gainNodes = [];
- for (let sourceIdx in alContext.sources) {
- gainNodes.push(alContext.sources[sourceIdx].gain);
+ if (alContext.sources) {
+ for (const sourceIdx in alContext.sources) {
+ const source = alContext.sources[sourceIdx];
+ if (source && source.gain) gainNodes.push(source.gain);
+ }
}
+ if (this.debug)
+ console.log("Gain nodes collected: " + gainNodes.length);
+
+ const masterGain =
+ alContext.gain || alContext.masterGain || alContext.outputGain;
+
+ if (masterGain || gainNodes.length > 0) {
+ try {
+ this.netplay = this.netplay || {};
+
+ const destination =
+ this.netplay.audioDestination ||
+ audioContext.createMediaStreamDestination();
+ this.netplay.audioDestination = destination;
+
+ const streamGain =
+ this.netplay.streamCompensationGain ||
+ audioContext.createGain();
+ this.netplay.streamCompensationGain = streamGain;
+
+ const currentVolume =
+ typeof this.volume === "number" && this.volume > 0.01
+ ? this.volume
+ : 1.0;
+ streamGain.gain.value = 1.0 / currentVolume;
+ if (this.debug)
+ console.log(
+ "Stream compensation gain: " +
+ streamGain.gain.value +
+ " (local volume: " +
+ currentVolume +
+ ")",
+ );
+
+ if (!this.netplay._streamGainConnectedToDest) {
+ streamGain.connect(destination);
+ this.netplay._streamGainConnectedToDest = true;
+ }
+
+ if (!this.netplay._audioTapRetryStarted) {
+ this.netplay._audioTapRetryStarted = true;
+ this.netplay._connectedSourceGains =
+ this.netplay._connectedSourceGains || new WeakSet();
+
+ const self = this;
+ const tryTap = function () {
+ const masterGain =
+ alContext.gain ||
+ alContext.masterGain ||
+ alContext.outputGain;
+ if (
+ masterGain &&
+ typeof masterGain.connect === "function"
+ ) {
+ if (!self.netplay._alMasterConnected) {
+ if (self.debug)
+ console.log(
+ "Using OpenAL master gain tap for stream audio",
+ );
+ try {
+ masterGain.connect(streamGain);
+ } catch (e) {}
+ self.netplay._alMasterConnected = true;
+ }
+ return true;
+ }
- const merger = audioContext.createChannelMerger(gainNodes.length);
- gainNodes.forEach(node => node.connect(merger));
+ const sources = alContext.sources || {};
+ let connectedAny = false;
+ for (const k in sources) {
+ const s = sources[k];
+ const g = s && s.gain;
+ if (
+ g &&
+ !self.netplay._connectedSourceGains.has(g)
+ ) {
+ try {
+ g.connect(streamGain);
+ } catch (e) {}
+ self.netplay._connectedSourceGains.add(g);
+ connectedAny = true;
+ }
+ }
+ return connectedAny;
+ };
- const destination = audioContext.createMediaStreamDestination();
- merger.connect(destination);
+ tryTap();
+ clearInterval(this.netplay._audioTapRetryTimer);
+ this.netplay._audioTapRetryTimer = setInterval(
+ function () {
+ if (self.netplay._alMasterConnected) {
+ clearInterval(
+ self.netplay._audioTapRetryTimer,
+ );
+ self.netplay._audioTapRetryTimer = null;
+ return;
+ }
+ tryTap();
+ },
+ 500,
+ );
+ }
- const audioTracks = destination.stream.getAudioTracks();
- if (audioTracks.length !== 0) {
- audioTrack = audioTracks[0];
+ const audioTracks = destination.stream.getAudioTracks();
+ if (this.debug)
+ console.log(
+ "Audio tracks created: " + audioTracks.length,
+ );
+
+ if (audioTracks.length !== 0) {
+ audioTrack = audioTracks[0];
+ if (this.debug)
+ console.log(
+ "Audio track: " +
+ audioTrack.label +
+ " readyState: " +
+ audioTrack.readyState +
+ " muted: " +
+ audioTrack.muted,
+ );
+ }
+ } catch (e) {
+ if (this.debug)
+ console.error("Error creating audio destination:", e);
+ }
}
}
const stream = new MediaStream();
- if (videoTrack && videoTrack.readyState === "live") {
+ if (videoTrack && videoTrack.readyState === "live")
stream.addTrack(videoTrack);
- }
- if (audioTrack && audioTrack.readyState === "live") {
+ if (audioTrack && audioTrack.readyState === "live")
stream.addTrack(audioTrack);
- }
+
+ if (this.debug)
+ console.log(
+ "Final stream - video tracks: " +
+ stream.getVideoTracks().length +
+ " audio tracks: " +
+ stream.getAudioTracks().length,
+ );
return stream;
}
screenRecord() {
- const captureFps = this.getSettingValue("screenRecordingFPS") || this.capture.video.fps;
- const captureFormat = this.getSettingValue("screenRecordFormat") || this.capture.video.format;
- const captureUpscale = this.getSettingValue("screenRecordUpscale") || this.capture.video.upscale;
- const captureVideoBitrate = this.getSettingValue("screenRecordVideoBitrate") || this.capture.video.videoBitrate;
- const captureAudioBitrate = this.getSettingValue("screenRecordAudioBitrate") || this.capture.video.audioBitrate;
- const aspectRatio = this.gameManager.getVideoDimensions("aspect") || 1.333333;
- const videoRotation = parseInt(this.getSettingValue("videoRotation") || 0);
- const videoTurned = (videoRotation === 1 || videoRotation === 3);
+ const captureFps =
+ this.getSettingValue("screenRecordingFPS") ||
+ this.capture.video.fps;
+ const captureFormat =
+ this.getSettingValue("screenRecordFormat") ||
+ this.capture.video.format;
+ const captureUpscale =
+ this.getSettingValue("screenRecordUpscale") ||
+ this.capture.video.upscale;
+ const captureVideoBitrate =
+ this.getSettingValue("screenRecordVideoBitrate") ||
+ this.capture.video.videoBitrate;
+ const captureAudioBitrate =
+ this.getSettingValue("screenRecordAudioBitrate") ||
+ this.capture.video.audioBitrate;
+ const aspectRatio =
+ this.gameManager.getVideoDimensions("aspect") || 1.333333;
+ const videoRotation = parseInt(
+ this.getSettingValue("videoRotation") || 0,
+ );
+ const videoTurned = videoRotation === 1 || videoRotation === 3;
let width = 800;
let height = 600;
let frameAspect = this.canvas.width / this.canvas.height;
@@ -7686,29 +9305,31 @@ class EmulatorJS {
const captureCtx = captureCanvas.getContext("2d", { alpha: false });
captureCtx.fillStyle = "#000";
captureCtx.imageSmoothingEnabled = false;
+
+ const self = this;
const updateSize = () => {
- width = this.canvas.width;
- height = this.canvas.height;
- frameAspect = width / height
+ width = self.canvas.width;
+ height = self.canvas.height;
+ frameAspect = width / height;
if (width >= height && !videoTurned) {
width = height * aspectRatio;
} else if (width < height && !videoTurned) {
height = width / aspectRatio;
} else if (width >= height && videoTurned) {
- width = height * (1/aspectRatio);
+ width = height * (1 / aspectRatio);
} else if (width < height && videoTurned) {
- width = height / (1/aspectRatio);
+ width = height / (1 / aspectRatio);
}
canvasAspect = width / height;
captureCanvas.width = width * captureUpscale;
captureCanvas.height = height * captureUpscale;
captureCtx.scale(captureUpscale, captureUpscale);
if (frameAspect > canvasAspect) {
- offsetX = (this.canvas.width - width) / -2;
+ offsetX = (self.canvas.width - width) / -2;
} else if (frameAspect < canvasAspect) {
- offsetY = (this.canvas.height - height) / -2;
+ offsetY = (self.canvas.height - height) / -2;
}
- }
+ };
updateSize();
this.addEventListener(this.canvas, "resize", () => {
updateSize();
@@ -7717,7 +9338,13 @@ class EmulatorJS {
let animation = true;
const drawNextFrame = () => {
- captureCtx.drawImage(this.canvas, offsetX, offsetY, this.canvas.width, this.canvas.height);
+ captureCtx.drawImage(
+ self.canvas,
+ offsetX,
+ offsetY,
+ self.canvas.width,
+ self.canvas.height,
+ );
if (animation) {
requestAnimationFrame(drawNextFrame);
}
@@ -7725,13 +9352,16 @@ class EmulatorJS {
requestAnimationFrame(drawNextFrame);
const chunks = [];
- const tracks = this.collectScreenRecordingMediaTracks(captureCanvas, captureFps);
+ const tracks = this.collectScreenRecordingMediaTracks(
+ captureCanvas,
+ captureFps,
+ );
const recorder = new MediaRecorder(tracks, {
videoBitsPerSecond: captureVideoBitrate,
audioBitsPerSecond: captureAudioBitrate,
- mimeType: "video/" + captureFormat
+ mimeType: "video/" + captureFormat,
});
- recorder.addEventListener("dataavailable", e => {
+ recorder.addEventListener("dataavailable", (e) => {
chunks.push(e.data);
});
recorder.addEventListener("stop", () => {
@@ -7740,7 +9370,16 @@ class EmulatorJS {
const date = new Date();
const a = document.createElement("a");
a.href = url;
- a.download = this.getBaseFileName() + "-" + date.getMonth() + "-" + date.getDate() + "-" + date.getFullYear() + "." + captureFormat;
+ a.download =
+ this.getBaseFileName() +
+ "-" +
+ date.getMonth() +
+ "-" +
+ date.getDate() +
+ "-" +
+ date.getFullYear() +
+ "." +
+ captureFormat;
a.click();
animation = false;
@@ -7754,7 +9393,9 @@ class EmulatorJS {
enableSaveUpdateEvent() {
function withGameSaveHash(saveFile, callback) {
if (saveFile) {
- this.utils.cyrb53(saveFile).then(digest => callback(digest, saveFile));
+ this.utils
+ .cyrb53(saveFile)
+ .then((digest) => callback(digest, saveFile));
} else {
console.warn("Save file not found when attempting to hash");
callback(null, null);
@@ -7762,24 +9403,34 @@ class EmulatorJS {
}
var recentHash = null;
- if (this.gameManager) { withGameSaveHash(this.gameManager.getSaveFile(false), (hash, _) => { recentHash = hash }) }
+ if (this.gameManager) {
+ withGameSaveHash(this.gameManager.getSaveFile(false), (hash, _) => {
+ recentHash = hash;
+ });
+ }
- this.on("saveSaveFiles", saveFile => {
+ this.on("saveSaveFiles", (saveFile) => {
withGameSaveHash(saveFile, (newHash, fileContents) => {
if (newHash && fileContents && newHash !== recentHash) {
recentHash = newHash;
- this.takeScreenshot(this.capture.photo.source, this.capture.photo.format, this.capture.photo.upscale).then(({ screenshot, format }) => {
+ this.takeScreenshot(
+ this.capture.photo.source,
+ this.capture.photo.format,
+ this.capture.photo.upscale,
+ ).then(({ screenshot, format }) => {
this.callEvent("saveUpdate", {
hash: newHash,
save: fileContents,
screenshot: screenshot,
- format: format
+ format: format,
});
- })
+ });
}
- })
- })
+ });
+ });
}
}
+Object.assign(EmulatorJS.prototype, netplayMethods);
+
export default EmulatorJS;
diff --git a/data/src/netplay.js b/data/src/netplay.js
new file mode 100644
index 000000000..6f997667c
--- /dev/null
+++ b/data/src/netplay.js
@@ -0,0 +1,2119 @@
+export const netplayMethods = {
+
+ updateNetplayUI(isJoining) {
+ if (!this.elements.bottomBar) return;
+
+ const bar = this.elements.bottomBar;
+ const isClient = !this.netplay.owner;
+ const shouldHideButtons = isJoining && isClient;
+ const elementsToToggle = [
+ ...(bar.playPause || []),
+ ...(bar.restart || []),
+ ...(bar.saveState || []),
+ ...(bar.loadState || []),
+ ...(bar.cheat || []),
+ ...(bar.saveSavFiles || []),
+ ...(bar.loadSavFiles || []),
+ ...(bar.exit || []),
+ ...(bar.contextMenu || []),
+ ...(bar.cacheManager || [])
+ ];
+
+ if (bar.settings && bar.settings.length > 0 && bar.settings[0].parentElement) {
+ elementsToToggle.push(bar.settings[0].parentElement);
+ }
+ if (this.diskParent) {
+ elementsToToggle.push(this.diskParent);
+ }
+
+ elementsToToggle.forEach((el) => {
+ if (el) {
+ el.classList.toggle("netplay-hidden", shouldHideButtons);
+ }
+ });
+ },
+
+ createNetplayMenu() {
+ const body = this.createPopup("Netplay", {
+ "Create a Room": () => {
+ if (this.netplayUnlockMobileAudio) this.netplayUnlockMobileAudio();
+ if (typeof this.netplay.updateList !== "function") this.defineNetplayFunctions();
+ if (this.isNetplay) this.netplay.leaveRoom();
+ else this.netplay.showOpenRoomDialog();
+ },
+ "Close": () => {
+ this.netplayMenu.style.display = "none";
+ if (this.netplay.updateList) this.netplay.updateList.stop();
+ }
+ }, true);
+
+ this.netplayMenu = body.parentElement;
+ const createButton = this.netplayMenu.getElementsByTagName("a")[0];
+
+ const rooms = this.createElement("div");
+ const title = this.createElement("strong");
+ title.innerText = this.localization("Rooms");
+
+ const table = this.createElement("table");
+ table.classList.add("ejs_netplay_table");
+ table.style.width = "100%";
+ table.setAttribute("cellspacing", "0");
+
+ const thead = this.createElement("thead");
+ const row = this.createElement("tr");
+ const addToHeader = (text) => {
+ const item = this.createElement("td");
+ item.innerText = text;
+ item.style.textAlign = "center";
+ row.appendChild(item);
+ return item;
+ };
+ thead.appendChild(row);
+ addToHeader("Room Name").style.textAlign = "left";
+ addToHeader("Players").style.width = "80px";
+ addToHeader("").style.width = "80px";
+ table.appendChild(thead);
+
+ const tbody = this.createElement("tbody");
+ table.appendChild(tbody);
+
+ rooms.appendChild(title);
+ rooms.appendChild(table);
+
+ const joined = this.createElement("div");
+ const title2 = this.createElement("strong");
+ title2.innerText = "{roomname}";
+
+ const password = this.createElement("div");
+ password.innerText = "Password: ";
+
+ const table2 = this.createElement("table");
+ table2.classList.add("ejs_netplay_table");
+ table2.style.width = "100%";
+ table2.setAttribute("cellspacing", "0");
+
+ const thead2 = this.createElement("thead");
+ const row2 = this.createElement("tr");
+ const addToHeader2 = (text) => {
+ const item = this.createElement("td");
+ item.innerText = text;
+ row2.appendChild(item);
+ return item;
+ };
+ thead2.appendChild(row2);
+ addToHeader2("Player").style.width = "80px";
+ addToHeader2("Name");
+ addToHeader2("").style.width = "80px";
+ table2.appendChild(thead2);
+
+ const tbody2 = this.createElement("tbody");
+ table2.appendChild(tbody2);
+
+ joined.appendChild(title2);
+ joined.appendChild(password);
+ joined.appendChild(table2);
+
+ const chatWrap = this.createElement("div");
+ chatWrap.classList.add("ejs_netplay_chat_container");
+ chatWrap.style.marginTop = "10px";
+
+ const chatHeaderRow = this.createElement("div");
+ chatHeaderRow.classList.add("ejs_netplay_chat_header_row");
+ chatWrap.appendChild(chatHeaderRow);
+
+ const chatTitle = this.createElement("strong");
+ chatTitle.innerText = this.localization("Chat");
+ chatHeaderRow.appendChild(chatTitle);
+
+ const chatHint = this.createElement("span");
+ chatHint.classList.add("ejs_netplay_chat_hint");
+ chatHint.innerText = this.localization("Everyone or private");
+ chatHeaderRow.appendChild(chatHint);
+
+ const chatLog = this.createElement("div");
+ chatLog.classList.add("ejs_netplay_chat_log");
+ chatWrap.appendChild(chatLog);
+
+ const chatRow = this.createElement("div");
+ chatRow.classList.add("ejs_netplay_chat_row");
+ chatWrap.appendChild(chatRow);
+
+ const chatTo = this.createElement("select");
+ chatTo.classList.add("ejs_netplay_chat_to");
+ const optAll = document.createElement("option");
+ optAll.value = "all";
+ optAll.innerText = this.localization("Everyone");
+ chatTo.appendChild(optAll);
+ chatRow.appendChild(chatTo);
+
+ const chatInput = this.createElement("input");
+ chatInput.type = "text";
+ chatInput.maxLength = 300;
+ chatInput.placeholder = this.localization("Type a message...");
+ chatInput.classList.add("ejs_netplay_chat_input");
+ chatRow.appendChild(chatInput);
+
+ const chatSend = this.createElement("button");
+ chatSend.classList.add("ejs_button_button");
+ chatSend.style.height = "34px";
+ chatSend.style.minWidth = "70px";
+ chatSend.innerText = this.localization("Send");
+ chatRow.appendChild(chatSend);
+
+ joined.appendChild(chatWrap);
+
+ joined.style.display = "none";
+ body.appendChild(rooms);
+ body.appendChild(joined);
+
+ this.openNetplayMenu = () => {
+ if (this.netplayShowTurnWarning && !this.netplayWarningShown) {
+ const warningDiv = this.createElement("div");
+ warningDiv.className = "ejs_netplay_warning";
+ warningDiv.innerText = "Warning: No TURN server configured. Netplay connections may fail.";
+ const menuBody = this.netplayMenu.querySelector(".ejs_popup_body");
+ if (menuBody) {
+ menuBody.prepend(warningDiv);
+ this.netplayWarningShown = true;
+ }
+ }
+
+ this.netplayMenu.style.display = "";
+
+ this.netplay = {
+ table: tbody,
+ playerTable: tbody2,
+ passwordElem: password,
+ roomNameElem: title2,
+ createButton: createButton,
+ tabs: [rooms, joined],
+
+ chatWrap,
+ chatLog,
+ chatTo,
+ chatInput,
+ chatSend,
+
+ ...this.netplay
+ };
+
+ if (!this.netplay.name) {
+ const popups = this.createSubPopup();
+ this.netplayMenu.appendChild(popups[0]);
+ popups[1].classList.add("ejs_cheat_parent");
+ const popup = popups[1];
+
+ const header = this.createElement("div");
+ const title = this.createElement("h2");
+ title.innerText = this.localization("Set Player Name");
+ title.classList.add("ejs_netplay_name_heading");
+ header.appendChild(title);
+ popup.appendChild(header);
+
+ const main = this.createElement("div");
+ main.classList.add("ejs_netplay_header");
+ const head = this.createElement("strong");
+ head.innerText = this.localization("Player Name");
+ const input = this.createElement("input");
+ input.type = "text";
+ input.setAttribute("maxlength", 20);
+
+ main.appendChild(head);
+ main.appendChild(this.createElement("br"));
+ main.appendChild(input);
+ popup.appendChild(main);
+
+ popup.appendChild(this.createElement("br"));
+
+ const buttonRow = this.createElement("div");
+ buttonRow.style.display = "flex";
+ buttonRow.style.justifyContent = "center";
+ buttonRow.style.gap = "10px";
+ popup.appendChild(buttonRow);
+
+ const submit = this.createElement("button");
+ submit.classList.add("ejs_button_button", "ejs_popup_submit");
+ submit.style.backgroundColor = "rgba(var(--ejs-primary-color),1)";
+ submit.innerText = this.localization("Submit");
+ buttonRow.appendChild(submit);
+
+ const cancel = this.createElement("button");
+ cancel.classList.add("ejs_button_button", "ejs_popup_submit");
+ cancel.innerText = this.localization("Cancel");
+ buttonRow.appendChild(cancel);
+
+ const closeNamePopup = () => popups[0].remove();
+
+ this.addEventListener(submit, "click", () => {
+ if (!input.value.trim()) return;
+ this.netplay.name = input.value.trim();
+ closeNamePopup();
+ });
+
+ this.addEventListener(cancel, "click", () => {
+ closeNamePopup();
+ this.netplayMenu.style.display = "none";
+ if (this.netplay.updateList) this.netplay.updateList.stop();
+ });
+
+ this.addEventListener(input, "keydown", (e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ submit.click();
+ } else if (e.key === "Escape") {
+ e.preventDefault();
+ cancel.click();
+ }
+ });
+
+ setTimeout(() => input.focus(), 0);
+ }
+
+ if (typeof this.netplay.updateList !== "function") {
+ this.defineNetplayFunctions();
+ }
+
+ if (this.netplayBindChatUI) this.netplayBindChatUI();
+ if (this.netplayChatRefreshRecipients) this.netplayChatRefreshRecipients();
+
+ this.netplay.updateList.start();
+ };
+ },
+
+ defineNetplayFunctions() {
+ this.netplay = this.netplay || {};
+
+ const guid = () => {
+ const s4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
+ return s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4();
+ };
+
+ const log = (role, ...args) => {
+ if (this.debug) console.log("[NETPLAY " + role + "]", ...args);
+ };
+
+ this.getNativeResolution = () => {
+ try {
+ if (this.Module && this.Module.getNativeResolution) {
+ return this.Module.getNativeResolution();
+ }
+ } catch (e) {}
+ return { width: 640, height: 480 };
+ };
+
+ this.netplayGetUserIndex = () => {
+ if (!this.isNetplay || !this.netplay || !this.netplay.players || !this.netplay.playerID) return 0;
+ const idx = Object.keys(this.netplay.players).indexOf(this.netplay.playerID);
+ return idx === -1 ? 0 : idx;
+ };
+
+ this.netplayEnsureRemoteAudioElement = (peerId) => {
+ this.netplay.remoteAudioElements = this.netplay.remoteAudioElements || {};
+
+ const id = "ejs-remote-audio-" + peerId;
+ let el = this.netplay.remoteAudioElements[peerId] || document.getElementById(id);
+
+ if (!el) {
+ el = document.createElement("audio");
+ el.id = id;
+ el.autoplay = true;
+ el.playsInline = true;
+ el.style.display = "none";
+ document.body.appendChild(el);
+ this.netplay.remoteAudioElements[peerId] = el;
+ }
+
+ el.muted = false;
+ el.volume = this.muted ? 0 : (typeof this.volume === "number" ? this.volume : 1);
+
+ return el;
+ };
+
+ this.netplayArmGuestAudioUnlock = () => {
+ if (!this.isNetplay || (this.netplay && this.netplay.owner)) return;
+ if (this.netplay._audioUnlockArmed) return;
+
+ this.netplay._audioUnlockArmed = true;
+
+ const tryPlayAll = () => {
+ try {
+ const els = document.querySelectorAll("audio[id^=\"ejs-remote-audio-\"]");
+ els.forEach((a) => {
+ a.muted = false;
+ a.volume = this.muted ? 0 : (typeof this.volume === "number" ? this.volume : 1);
+ a.play().catch(() => {});
+ });
+ } catch (e) {}
+ cleanup();
+ };
+
+ const cleanup = () => {
+ if (!this.netplay._audioUnlockArmed) return;
+ this.netplay._audioUnlockArmed = false;
+
+ document.removeEventListener("pointerdown", tryPlayAll, true);
+ document.removeEventListener("touchend", tryPlayAll, true);
+ document.removeEventListener("keydown", tryPlayAll, true);
+ };
+
+ document.addEventListener("pointerdown", tryPlayAll, true);
+ document.addEventListener("touchend", tryPlayAll, true);
+ document.addEventListener("keydown", tryPlayAll, true);
+
+ this.netplay._audioUnlockCleanup = cleanup;
+ };
+
+ this.netplayChooseStreamSize = () => {
+ const fps = (this.config && this.config.netplayFps) || window.EJS_netplayFps || 30;
+
+ const override = (this.config && this.config.netplayStream) || window.EJS_netplayStream;
+ if (override && typeof override === "string") {
+ const m = override.trim().match(/^(\d+)\s*x\s*(\d+)$/i);
+ if (m) {
+ let ow = Math.max(2, parseInt(m[1], 10));
+ let oh = Math.max(2, parseInt(m[2], 10));
+ if (ow % 2) ow++;
+ if (oh % 2) oh++;
+ return { w: ow, h: oh, fps: fps, mode: "override" };
+ }
+ }
+
+ const n = this.getNativeResolution();
+ const aw = (n && n.width) ? n.width : 640;
+ const ah = (n && n.height) ? n.height : 480;
+ let aspect = aw / ah;
+ if (!isFinite(aspect) || aspect <= 0) aspect = 4 / 3;
+
+ if (aspect < 1.1) aspect = 1.1;
+ if (aspect > 2.0) aspect = 2.0;
+
+ let H = 720;
+ let W = Math.round(H * aspect);
+
+ if (W > 1280) {
+ W = 1280;
+ H = Math.round(W / aspect);
+ }
+
+ if (W % 2) W++;
+ if (H % 2) H++;
+
+ return { w: W, h: H, fps: fps, mode: "auto" };
+ };
+
+ this.netplayGetAnchorElement = () => {
+ try {
+ if (this.config && this.config.player) {
+ const el = document.querySelector(this.config.player);
+ if (el) return el;
+ }
+ } catch (e) {}
+ return this.canvas || null;
+ };
+
+ this.netplayEnsureOverlay = () => {
+ if (this.netplay._overlay && this.netplay._overlay.parentNode) return;
+
+ const ov = document.createElement("div");
+ ov.id = "ejs-netplay-overlay";
+ ov.classList.add("ejs_netplay_overlay");
+ this.elements.parent.appendChild(ov);
+ this.netplay._overlay = ov;
+
+ this.netplay._overlaySync = () => {
+ this.netplaySyncOverlay(false);
+ };
+ window.addEventListener("resize", this.netplay._overlaySync, true);
+ window.addEventListener("scroll", this.netplay._overlaySync, true);
+ window.addEventListener("orientationchange", this.netplay._overlaySync, true);
+
+ if (window.visualViewport) {
+ this.netplay._vvSync = () => {
+ this.netplaySyncOverlay(false);
+ };
+ window.visualViewport.addEventListener("resize", this.netplay._vvSync, true);
+ window.visualViewport.addEventListener("scroll", this.netplay._vvSync, true);
+ }
+
+ if (!this.netplay._overlayRO && window.ResizeObserver) {
+ this.netplay._overlayRO = new ResizeObserver(() => {
+ this.netplaySyncOverlay(false);
+ });
+ try {
+ const anchor = this.netplayGetAnchorElement();
+ if (anchor) this.netplay._overlayRO.observe(anchor);
+ if (anchor && anchor.parentElement) this.netplay._overlayRO.observe(anchor.parentElement);
+ } catch (e) {}
+ }
+
+ this.netplaySyncOverlay(true);
+ };
+
+ this.netplaySyncOverlay = (force) => {
+ if (!this.netplayCanvas || !this.netplay._overlay) return;
+
+ const anchor = this.netplayGetAnchorElement();
+ if (!anchor || !anchor.getBoundingClientRect) return;
+
+ const rect = anchor.getBoundingClientRect();
+ if (!rect || rect.width <= 0 || rect.height <= 0) return;
+
+ const dpr = window.devicePixelRatio || 1;
+
+ const vv = window.visualViewport;
+ const offX = vv ? vv.offsetLeft : 0;
+ const offY = vv ? vv.offsetTop : 0;
+ const vW = vv ? vv.width : window.innerWidth;
+ const vH = vv ? vv.height : window.innerHeight;
+
+ let cssW = Math.max(1, Math.round(rect.width));
+ let cssH = Math.max(1, Math.round(rect.height));
+
+ let left = rect.left + offX;
+ let top = rect.top + offY;
+
+ if (cssW > vW) cssW = Math.max(1, Math.round(vW));
+ if (cssH > vH) cssH = Math.max(1, Math.round(vH));
+ left = Math.max(0, Math.min(left, vW - cssW));
+ top = Math.max(0, Math.min(top, vH - cssH));
+
+ this.netplay.guestDisplayWidth = cssW;
+ this.netplay.guestDisplayHeight = cssH;
+
+ if (this.netplayCanvas.parentNode !== this.netplay._overlay) {
+ this.netplay._overlay.appendChild(this.netplayCanvas);
+ }
+
+ this.netplayCanvas.style.position = "fixed";
+ this.netplayCanvas.style.left = left + "px";
+ this.netplayCanvas.style.top = top + "px";
+ this.netplayCanvas.style.width = cssW + "px";
+ this.netplayCanvas.style.height = cssH + "px";
+ this.netplayCanvas.style.zIndex = "10000";
+ this.netplayCanvas.style.pointerEvents = "none";
+ this.netplayCanvas.style.background = "#000";
+ this.netplayCanvas.style.imageRendering = "pixelated";
+
+ const pxW = Math.max(1, Math.round(cssW * dpr));
+ const pxH = Math.max(1, Math.round(cssH * dpr));
+ if (force || this.netplayCanvas.width !== pxW || this.netplayCanvas.height !== pxH) {
+ this.netplayCanvas.width = pxW;
+ this.netplayCanvas.height = pxH;
+ }
+ };
+
+ this.netplayDestroyOverlay = () => {
+ if (this.netplay._overlaySync) {
+ window.removeEventListener("resize", this.netplay._overlaySync, true);
+ window.removeEventListener("scroll", this.netplay._overlaySync, true);
+ window.removeEventListener("orientationchange", this.netplay._overlaySync, true);
+ this.netplay._overlaySync = null;
+ }
+ if (this.netplay._vvSync && window.visualViewport) {
+ window.visualViewport.removeEventListener("resize", this.netplay._vvSync, true);
+ window.visualViewport.removeEventListener("scroll", this.netplay._vvSync, true);
+ this.netplay._vvSync = null;
+ }
+ if (this.netplay._overlayRO) {
+ try {
+ this.netplay._overlayRO.disconnect();
+ } catch (e) {}
+ this.netplay._overlayRO = null;
+ }
+ if (this.netplay._overlay && this.netplay._overlay.parentNode) {
+ try {
+ this.netplay._overlay.parentNode.removeChild(this.netplay._overlay);
+ } catch (e) {}
+ }
+ this.netplay._overlay = null;
+ };
+
+ this.netplayBoostGuestUIZ = () => {
+ if (!this.isNetplay || this.netplay.owner) return;
+ if (this.netplay._uiZBoosted) return;
+
+ this.netplay._uiZBoosted = [];
+ let root = null;
+
+ if (!this.msgElem && typeof this.displayMessage === "function") {
+ this.displayMessage("", 1);
+ }
+
+ try {
+ root = (this.config && this.config.player) ? document.querySelector(this.config.player) : null;
+ } catch (e) {}
+ if (!root) root = document;
+
+ const sel = [
+ ".ejs_message",
+ ".ejs_menu_bar",
+ ".ejs_settings_parent",
+ ".ejs_context_menu",
+ ".ejs_popup_container",
+ ".ejs_popup_container_box",
+ ".ejs_virtualGamepad_parent",
+ ".ejs_virtualGamepad_top",
+ ".ejs_virtualGamepad_left",
+ ".ejs_virtualGamepad_right",
+ ".ejs_virtualGamepad_bottom",
+ ".ejs_virtualGamepad_open"
+ ].join(",");
+
+ const nodes = root.querySelectorAll(sel);
+
+ for (let i = 0; i < nodes.length; i++) {
+ const el = nodes[i];
+ const cs = window.getComputedStyle(el);
+
+ this.netplay._uiZBoosted.push({
+ el: el,
+ z: el.style.zIndex,
+ pos: el.style.position,
+ pe: el.style.pointerEvents
+ });
+
+ if (cs.position === "static") el.style.position = "relative";
+
+ el.style.zIndex = "10002";
+ el.style.pointerEvents = "auto";
+ }
+ };
+
+ this.netplayRestoreGuestUIZ = () => {
+ const list = this.netplay._uiZBoosted;
+ if (!list) return;
+
+ for (let i = 0; i < list.length; i++) {
+ const item = list[i];
+ if (!item || !item.el) continue;
+ item.el.style.zIndex = item.z || "";
+ item.el.style.position = item.pos || "";
+ item.el.style.pointerEvents = item.pe || "";
+ }
+
+ this.netplay._uiZBoosted = null;
+ };
+
+ this.netplayFreezeGuest = () => {
+ log("GUEST", "Freezing emulator...");
+
+ this.netplay.frozen = this.netplay.frozen || { originals: {} };
+ const orig = this.netplay.frozen.originals;
+
+ if (this.gameManager) {
+ try {
+ this.gameManager.toggleMainLoop(0);
+ } catch (e) {}
+ if (this.gameManager.pause) {
+ try {
+ this.gameManager.pause();
+ } catch (e) {}
+ }
+ }
+ if (this.Module && this.Module.pauseMainLoop) {
+ try {
+ this.Module.pauseMainLoop();
+ } catch (e) {}
+ }
+
+ if (this.handleResize && !orig.handleResize) {
+ orig.handleResize = this.handleResize;
+ this.handleResize = (...args) => {
+ try {
+ orig.handleResize.apply(this, args);
+ } catch (e) {}
+ if (this.isNetplay && !this.netplay.owner) this.netplaySyncOverlay(true);
+ };
+ }
+
+ if (this.gameManager && this.gameManager.audioNode) {
+ try {
+ this.gameManager.audioNode.disconnect();
+ } catch (e) {}
+ }
+ if (this.Module && this.Module.AL && this.Module.AL.currentCtx) {
+ const ctx = this.Module.AL.currentCtx;
+ if (ctx.sources) {
+ for (const id in ctx.sources) {
+ try {
+ ctx.sources[id].gain.gain.value = 0;
+ } catch (e) {}
+ }
+ }
+ if (ctx.audioCtx) {
+ ctx.audioCtx.suspend().catch(() => {});
+ }
+ }
+
+ log("GUEST", "Emulator frozen");
+ };
+
+ this.netplayUnfreezeGuest = () => {
+ if (!this.netplay.frozen) return;
+ log("GUEST", "Unfreezing emulator...");
+
+ const orig = this.netplay.frozen.originals || {};
+ if (orig.handleResize) this.handleResize = orig.handleResize;
+
+ if (this.Module && this.Module.AL && this.Module.AL.currentCtx && this.Module.AL.currentCtx.audioCtx) {
+ this.Module.AL.currentCtx.audioCtx.resume().catch(() => {});
+ const vol = this.muted ? 0 : this.volume;
+ if (this.Module.AL.currentCtx.sources) {
+ for (const id in this.Module.AL.currentCtx.sources) {
+ try {
+ this.Module.AL.currentCtx.sources[id].gain.gain.value = vol;
+ } catch (e) {}
+ }
+ }
+ }
+ if (this.gameManager && this.gameManager.audioNode && this.gameManager.audioContext) {
+ try {
+ this.gameManager.audioNode.connect(this.gameManager.audioContext.destination);
+ } catch (e) {}
+ }
+
+ if (this.Module && this.Module.resumeMainLoop) {
+ try {
+ this.Module.resumeMainLoop();
+ } catch (e) {}
+ }
+ if (this.gameManager) {
+ try {
+ this.gameManager.toggleMainLoop(1);
+ } catch (e) {}
+ }
+
+ this.netplay.frozen = null;
+ log("GUEST", "Emulator unfrozen");
+ };
+
+ this.netplayRequestRenegotiate = (peerId, reason) => {
+ try {
+ if (!this.netplay || !this.netplay.socket || !this.netplay.socket.connected) return;
+ log(this.netplay.owner ? "HOST" : "GUEST", "Request renegotiate (" + (reason || "unknown") + ") with " + peerId);
+ this.netplay.socket.emit("webrtc-signal", { target: peerId, requestRenegotiate: true, reason: reason || "" });
+ } catch (e) {}
+ };
+
+ this.netplayInitWebRTCStream = () => {
+ if (this.netplay.localStream) return Promise.resolve();
+
+ if (this.Module && this.Module.AL && this.Module.AL.currentCtx && this.Module.AL.currentCtx.audioCtx) {
+ this.Module.AL.currentCtx.audioCtx.resume().catch(() => {});
+ }
+
+ return new Promise((resolve) => {
+ if (!this.canvas || !this.canvas.captureStream) {
+ if (this.debug) console.error("[NETPLAY HOST] canvas.captureStream unavailable");
+ resolve();
+ return;
+ }
+
+ const chosen = this.netplayChooseStreamSize();
+ const outW = chosen.w;
+ const outH = chosen.h;
+ const fps = chosen.fps;
+ const outAspect = outW / outH;
+
+ log("HOST", "Init stream (decoupled " + chosen.mode + ") " + outW + "x" + outH + " @ " + fps + "fps");
+
+ let rawStream = null;
+ try {
+ rawStream = this.canvas.captureStream(fps);
+ } catch (e) {}
+ if (!rawStream || !rawStream.getVideoTracks || !rawStream.getVideoTracks()[0]) {
+ if (this.debug) console.error("[NETPLAY HOST] No video track from canvas.captureStream()");
+ resolve();
+ return;
+ }
+ this.netplay._hostRawStream = rawStream;
+
+ const srcVideo = document.createElement("video");
+ srcVideo.muted = true;
+ srcVideo.autoplay = true;
+ srcVideo.playsInline = true;
+ srcVideo.classList.add("ejs_netplay_offscreen");
+ document.body.appendChild(srcVideo);
+ this.netplay._hostSourceVideo = srcVideo;
+
+ srcVideo.srcObject = rawStream;
+ srcVideo.play().catch((err) => {
+ log("HOST", "source video play() warning:", err);
+ });
+
+ const cap = document.createElement("canvas");
+ cap.width = outW;
+ cap.height = outH;
+ cap.classList.add("ejs_netplay_offscreen_canvas");
+ document.body.appendChild(cap);
+ this.netplay.captureCanvas = cap;
+
+ const capCtx = cap.getContext("2d", { alpha: false });
+ this.netplay.captureRunning = true;
+
+ const drawToFixedCanvas = () => {
+ if (!this.netplay.captureRunning) return;
+
+ capCtx.fillStyle = "#000";
+ capCtx.fillRect(0, 0, outW, outH);
+
+ if (srcVideo.readyState >= 2 && srcVideo.videoWidth > 0 && srcVideo.videoHeight > 0) {
+ const srcW = srcVideo.videoWidth;
+ const srcH = srcVideo.videoHeight;
+ const srcAspect = srcW / srcH;
+
+ let sx = 0;
+ let sy = 0;
+ let sw = srcW;
+ let sh = srcH;
+
+ if (srcAspect > outAspect) {
+ sw = srcH * outAspect;
+ sx = (srcW - sw) / 2;
+ } else if (srcAspect < outAspect) {
+ sh = srcW / outAspect;
+ const portraitish = (srcH / srcW) >= 1.25;
+ sy = portraitish ? 0 : (srcH - sh) / 2;
+
+ if (sy < 0) sy = 0;
+ if (sy + sh > srcH) sy = srcH - sh;
+ if (sy < 0) sy = 0;
+ }
+
+ capCtx.imageSmoothingEnabled = true;
+ capCtx.drawImage(srcVideo, sx, sy, sw, sh, 0, 0, outW, outH);
+ }
+
+ requestAnimationFrame(drawToFixedCanvas);
+ };
+ requestAnimationFrame(drawToFixedCanvas);
+
+ // --- DELEGATE AUDIO CAPTURE TO MAIN EMULATOR.JS FUNCTION ---
+ if (typeof this.collectScreenRecordingMediaTracks === "function") {
+
+ // Grab both video and audio tracks simultaneously using your existing logic
+ const finalStream = this.collectScreenRecordingMediaTracks(cap, fps);
+
+ // Optimize video encoding detail for netplay
+ try {
+ const outVideoTrack = finalStream.getVideoTracks()[0];
+ if (outVideoTrack) outVideoTrack.contentHint = "detail";
+ } catch (e) {}
+
+ this.netplay.localStream = finalStream;
+ this.netplay._hostOutStream = finalStream;
+
+ log("HOST", "Stream ready - Video tracks: " + finalStream.getVideoTracks().length + ", Audio tracks: " + finalStream.getAudioTracks().length);
+ } else {
+ if (this.debug) console.warn("[NETPLAY HOST] collectScreenRecordingMediaTracks missing! Fallback to video only.");
+ const fallbackStream = cap.captureStream(fps);
+ this.netplay.localStream = fallbackStream;
+ this.netplay._hostOutStream = fallbackStream;
+ }
+
+ resolve();
+ });
+ };
+
+ this.netplayRoomJoined = (isOwner, roomName, password, roomId) => {
+ log(isOwner ? "HOST" : "GUEST", "Room joined: " + roomName);
+
+ if (this.updateNetplayUI) this.updateNetplayUI(true);
+
+ this.isNetplay = true;
+ this.netplay.inputs = {};
+ this.netplay.owner = isOwner;
+
+ if (this.netplay.roomNameElem) this.netplay.roomNameElem.innerText = roomName;
+ if (this.netplay.tabs && this.netplay.tabs[0]) {
+ this.netplay.tabs[0].style.display = "none";
+ this.netplay.tabs[1].style.display = "";
+ }
+ if (this.netplay.passwordElem) {
+ this.netplay.passwordElem.style.display = password ? "" : "none";
+ this.netplay.passwordElem.innerText = password ? "Password: " + password : "";
+ }
+ if (this.netplay.createButton) this.netplay.createButton.innerText = this.localization("Leave Room");
+
+ this.netplayUpdatePlayersTable();
+
+ if (!isOwner) {
+ const anchor = this.netplayGetAnchorElement();
+ const rect = (anchor && anchor.getBoundingClientRect)
+ ? anchor.getBoundingClientRect()
+ : (this.canvas ? this.canvas.getBoundingClientRect() : { width: 640, height: 480 });
+
+ const cssW = Math.max(1, Math.round(rect.width));
+ const cssH = Math.max(1, Math.round(rect.height));
+ const dpr = window.devicePixelRatio || 1;
+
+ log("GUEST", "Display rect: " + cssW + "x" + cssH);
+
+ this.netplayFreezeGuest();
+
+ this.netplay._restoreCanvasStyle = {
+ opacity: this.canvas ? this.canvas.style.opacity : "",
+ pointerEvents: this.canvas ? this.canvas.style.pointerEvents : "",
+ visibility: this.canvas ? this.canvas.style.visibility : ""
+ };
+
+ this.netplayCanvas = document.createElement("canvas");
+ this.netplayCanvas.id = "ejs-netplay-canvas";
+ this.netplayCanvas.width = Math.max(1, Math.round(cssW * dpr));
+ this.netplayCanvas.height = Math.max(1, Math.round(cssH * dpr));
+ this.netplayCanvas.classList.add("ejs_netplay_canvas");
+ this.netplayCanvas.style.width = cssW + "px";
+ this.netplayCanvas.style.height = cssH + "px";
+
+ this.netplayEnsureOverlay();
+ this.netplaySyncOverlay(true);
+
+ this.netplayBoostGuestUIZ();
+
+ if (this.canvas) {
+ this.canvas.style.opacity = "0";
+ this.canvas.style.visibility = "visible";
+ this.canvas.style.pointerEvents = "";
+ }
+
+ const ctx = this.netplayCanvas.getContext("2d", { alpha: false });
+ ctx.fillStyle = "#000";
+ ctx.fillRect(0, 0, this.netplayCanvas.width, this.netplayCanvas.height);
+ ctx.fillStyle = "#fff";
+ ctx.font = (20 * dpr) + "px sans-serif";
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.fillText("Connecting...", this.netplayCanvas.width / 2, this.netplayCanvas.height / 2);
+
+ if (this.gameManager && this.gameManager.functions && this.gameManager.functions.simulateInput) {
+ this.netplay.originalSimulateInput = this.gameManager.functions.simulateInput;
+ this.gameManager.functions.simulateInput = (player, index, value) => {
+ const pidx = this.netplayGetUserIndex();
+ const pcs = this.netplay.peerConnections;
+ for (const key in pcs) {
+ if (pcs[key] && pcs[key].dataChannel && pcs[key].dataChannel.readyState === "open") {
+ pcs[key].dataChannel.send(JSON.stringify({ player: pidx, index: index, value: value }));
+ }
+ }
+ };
+ }
+
+ if (this.elements.bottomBar && this.elements.bottomBar.cheat && this.elements.bottomBar.cheat[0]) {
+ this.netplay.oldCheatDisplay = this.elements.bottomBar.cheat[0].style.display;
+ this.elements.bottomBar.cheat[0].style.display = "none";
+ }
+
+ this.netplay._gotVideoEver = false;
+
+ this.netplay.connectionTimeout = setTimeout(() => {
+ if (!this.netplay.webRtcReady && !this.netplay._gotVideoEver) {
+ this.displayMessage("Connection failed", 5000);
+ this.netplayLeaveRoom();
+ }
+ }, 15000);
+
+ log("GUEST", "Setup complete - waiting for stream");
+ } else {
+ log("HOST", "Setup complete");
+ if (this.gameManager) {
+ try {
+ this.gameManager.toggleMainLoop(1);
+ } catch (e) {}
+ }
+ }
+ };
+
+ this.drawVideoToCanvas = () => {
+ const video = this.netplay.video;
+ const canvas = this.netplayCanvas;
+ if (!video || !canvas) return;
+
+ const ctx = canvas.getContext("2d", { alpha: false });
+ if (!ctx) return;
+
+ log("GUEST", "Starting draw loop");
+
+ let running = true;
+ let lockedAspect = null;
+ let lastVideoSize = "";
+
+ const draw = () => {
+ if (!running || !this.isNetplay || this.netplay.owner) return;
+ if (!canvas.parentNode) return;
+
+ this.netplaySyncOverlay(false);
+
+ const W = canvas.width;
+ const H = canvas.height;
+
+ ctx.fillStyle = "#000";
+ ctx.fillRect(0, 0, W, H);
+
+ if (video.readyState >= 2 && video.videoWidth > 0 && video.videoHeight > 0) {
+ const vs = video.videoWidth + "x" + video.videoHeight;
+ if (vs !== lastVideoSize) {
+ lastVideoSize = vs;
+ log("GUEST", "Video size: " + vs);
+ }
+
+ if (lockedAspect === null) {
+ lockedAspect = video.videoWidth / video.videoHeight;
+ log("GUEST", "Aspect locked: " + lockedAspect.toFixed(4));
+ }
+
+ const guestAspect = W / H;
+ let drawW, drawH, ox, oy;
+
+ if (lockedAspect > guestAspect) {
+ drawW = W;
+ drawH = W / lockedAspect;
+ ox = 0;
+ oy = (H - drawH) / 2;
+ } else {
+ drawH = H;
+ drawW = H * lockedAspect;
+ ox = (W - drawW) / 2;
+ oy = 0;
+ }
+
+ ctx.imageSmoothingEnabled = true;
+ ctx.drawImage(video, ox, oy, drawW, drawH);
+ }
+
+ requestAnimationFrame(draw);
+ };
+
+ video.onloadeddata = () => {
+ log("GUEST", "Video loadeddata");
+ requestAnimationFrame(draw);
+ };
+
+ if (video.readyState >= 2) requestAnimationFrame(draw);
+
+ this.netplay.stopDrawLoop = () => {
+ running = false;
+ };
+ };
+
+ this.netplayCreatePeerConnection = (peerId) => {
+ const role = this.netplay.owner ? "HOST" : "GUEST";
+
+ log(role, "Creating peer connection: " + peerId);
+
+ const pc = new RTCPeerConnection({
+ iceServers: this.config.netplayICEServers,
+ iceCandidatePoolSize: 10
+ });
+
+ let dc;
+
+ if (this.netplay.owner) {
+ dc = pc.createDataChannel("inputs");
+ dc.onmessage = (e) => {
+ const d = JSON.parse(e.data);
+ if (d.type === "host-left") {
+ this.displayMessage("Host left", 3000);
+ this.netplayLeaveRoom();
+ return;
+ }
+ const f = this.netplay.currentFrame || 0;
+ this.netplay.inputsData[f] = this.netplay.inputsData[f] || [];
+ this.netplay.inputsData[f].push({ frame: f, connected_input: [d.player, d.index, d.value] });
+ if (this.gameManager && this.gameManager.functions && this.gameManager.functions.simulateInput) {
+ this.gameManager.functions.simulateInput(d.player, d.index, d.value);
+ }
+ };
+ } else {
+ pc.ondatachannel = (e) => {
+ dc = e.channel;
+ if (this.netplay.peerConnections[peerId]) this.netplay.peerConnections[peerId].dataChannel = dc;
+ dc.onmessage = (e) => {
+ const d = JSON.parse(e.data);
+ if (d.type === "host-left") {
+ this.displayMessage("Host left", 3000);
+ this.netplayLeaveRoom();
+ }
+ };
+ };
+ }
+
+ if (this.netplay.owner && this.netplay.localStream) {
+ const tracks = this.netplay.localStream.getTracks();
+ log("HOST", "Adding " + tracks.length + " tracks");
+ for (let i = 0; i < tracks.length; i++) {
+ pc.addTrack(tracks[i], this.netplay.localStream);
+ }
+
+ try {
+ const sender = pc.getSenders().find((s) => s.track && s.track.kind === "video");
+ if (sender) {
+ const p = sender.getParameters();
+ p.degradationPreference = "maintain-resolution";
+ if (!p.encodings || !p.encodings.length) p.encodings = [{}];
+ p.encodings[0].maxBitrate = 5000000;
+ p.encodings[0].scaleResolutionDownBy = 1.0;
+ sender.setParameters(p).catch(() => {});
+ }
+ } catch (e) {}
+ } else {
+ pc.addTransceiver("video", { direction: "recvonly" });
+ pc.addTransceiver("audio", { direction: "recvonly" });
+ }
+
+ this.netplay.peerConnections[peerId] = { pc: pc, dataChannel: dc };
+
+ let gotStream = false;
+ const streamTimeout = setTimeout(() => {
+ if (!gotStream && !this.netplay.owner) {
+ log("GUEST", "Stream timeout -> request renegotiate");
+ this.netplayRequestRenegotiate(peerId, "stream-timeout");
+ }
+ }, 15000);
+
+ pc.onicecandidate = (e) => {
+ if (e.candidate) {
+ this.netplay.socket.emit("webrtc-signal", { target: peerId, candidate: e.candidate });
+ }
+ };
+
+ pc.onconnectionstatechange = () => {
+ log(role, "Connection: " + pc.connectionState);
+
+ if (pc.connectionState === "connected") {
+ this.netplay.webRtcReady = true;
+ clearTimeout(this.netplay.connectionTimeout);
+ if (this.netplay._dcTimer) {
+ clearTimeout(this.netplay._dcTimer);
+ this.netplay._dcTimer = null;
+ }
+ return;
+ }
+
+ if (!this.netplay.owner) {
+ if (pc.connectionState === "failed") {
+ this.netplayRequestRenegotiate(peerId, "pc-failed");
+ return;
+ }
+ if (pc.connectionState === "disconnected") {
+ if (this.netplay._dcTimer) clearTimeout(this.netplay._dcTimer);
+ this.netplay._dcTimer = setTimeout(() => {
+ if (!this.isNetplay) return;
+ const pd = this.netplay.peerConnections[peerId];
+ if (!pd || !pd.pc) return;
+ if (pd.pc.connectionState === "disconnected") {
+ this.netplayRequestRenegotiate(peerId, "pc-disconnected");
+ }
+ }, 2500);
+ }
+ } else {
+ if (pc.connectionState === "failed") {
+ try {
+ pc.close();
+ } catch (e) {}
+ delete this.netplay.peerConnections[peerId];
+ setTimeout(() => {
+ this.netplayCreatePeerConnection(peerId);
+ }, 1500);
+ }
+ }
+ };
+
+ pc.ontrack = (e) => {
+ if (this.netplay.owner) return;
+
+ const t = e.track;
+ log("GUEST", "Track received: " + t.kind);
+
+ if (t.kind === "audio") {
+ try {
+ const stream = (e.streams && e.streams[0]) ? e.streams[0] : new MediaStream([t]);
+ const audioEl = this.netplayEnsureRemoteAudioElement(peerId);
+ audioEl.srcObject = stream;
+
+ const p = audioEl.play();
+ if (p && p.catch) {
+ p.catch(() => {
+ log("GUEST", "Audio autoplay blocked, arming user-gesture unlock");
+ this.netplayArmGuestAudioUnlock();
+ });
+ }
+ } catch (err) {
+ if (this.debug) console.error("[NETPLAY GUEST] Audio element error:", err);
+ }
+ return;
+ }
+
+ if (t.kind === "video") {
+ gotStream = true;
+ this.netplay._gotVideoEver = true;
+ clearTimeout(streamTimeout);
+ clearTimeout(this.netplay.connectionTimeout);
+ this.netplay.webRtcReady = true;
+
+ if (!this.netplay.video) {
+ this.netplay.video = document.createElement("video");
+ this.netplay.video.muted = true;
+ this.netplay.video.autoplay = true;
+ this.netplay.video.playsInline = true;
+ this.netplay.video.style.display = "none";
+ }
+
+ this.netplay.video.srcObject = (e.streams && e.streams[0]) ? e.streams[0] : new MediaStream([t]);
+ this.netplay.video.play().catch((err) => {
+ log("GUEST", "video.play() warning:", err);
+ });
+
+ this.drawVideoToCanvas();
+
+ t.onended = () => {
+ if (this.isNetplay) this.netplayRequestRenegotiate(peerId, "video-track-ended");
+ };
+ }
+ };
+
+ if (this.netplay.owner && this.netplay.localStream) {
+ log("HOST", "Creating offer...");
+ pc.createOffer()
+ .then((o) => pc.setLocalDescription(o))
+ .then(() => {
+ this.netplay.socket.emit("webrtc-signal", { target: peerId, offer: pc.localDescription });
+ })
+ .catch((err) => {
+ if (this.debug) console.error("[NETPLAY HOST] Offer error:", err);
+ });
+ }
+
+ return pc;
+ };
+
+ this.netplayChatAppend = (payload) => {
+ if (!this.netplay || !this.netplay.chatLog) return;
+
+ const name = payload && payload.player_name ? payload.player_name : "Player";
+ const msg = payload && payload.message ? payload.message : "";
+ const to = payload && payload.to ? payload.to : "all";
+
+ const line = document.createElement("div");
+
+ if (to && to !== "all") {
+ line.textContent = name + " (private): " + msg;
+ line.style.opacity = "0.95";
+ } else {
+ line.textContent = name + ": " + msg;
+ }
+
+ this.netplay.chatLog.appendChild(line);
+ this.netplay.chatLog.scrollTop = this.netplay.chatLog.scrollHeight;
+ };
+
+ this.netplayChatRefreshRecipients = () => {
+ if (!this.netplay || !this.netplay.chatTo) return;
+
+ const sel = this.netplay.chatTo;
+ const prev = sel.value || "all";
+
+ sel.innerHTML = "";
+ const optAll = document.createElement("option");
+ optAll.value = "all";
+ optAll.innerText = this.localization("Everyone");
+ sel.appendChild(optAll);
+
+ const players = this.netplay.players || {};
+ Object.keys(players).forEach((userid) => {
+ const p = players[userid];
+ const opt = document.createElement("option");
+ opt.value = userid;
+ opt.innerText = p.player_name || "Player";
+ sel.appendChild(opt);
+ });
+
+ const stillExists = Array.from(sel.options).some((o) => o.value === prev);
+ sel.value = stillExists ? prev : "all";
+ };
+
+ this.netplayChatSend = () => {
+ if (!this.netplay || !this.netplay.socket || !this.netplay.socket.connected) return;
+ if (!this.netplay.chatInput || !this.netplay.chatTo) return;
+
+ const message = String(this.netplay.chatInput.value || "").trim();
+ if (!message) return;
+
+ const to = this.netplay.chatTo.value || "all";
+ this.netplay.chatInput.value = "";
+
+ const chatPayload = {
+ player_name: this.netplay.name || "Player",
+ message: message,
+ to: to,
+ from: this.netplay.playerID
+ };
+
+ this.netplayChatAppend(chatPayload);
+
+ this.netplaySendMessage({ "chat-message": chatPayload });
+ };
+
+ this.netplayBindChatUI = () => {
+ if (!this.netplay || this.netplay._chatBound) return;
+ if (!this.netplay.chatSend || !this.netplay.chatInput) return;
+
+ this.netplay._chatBound = true;
+
+ this.addEventListener(this.netplay.chatSend, "click", () => {
+ this.netplayChatSend();
+ });
+ this.addEventListener(this.netplay.chatInput, "keydown", (e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ this.netplayChatSend();
+ }
+ });
+ };
+
+ this.netplayLeaveRoom = () => {
+ if (this.netplay._leaving) return;
+ this.netplay._leaving = true;
+
+ log(this.netplay.owner ? "HOST" : "GUEST", "Leaving room");
+
+ if (this.updateNetplayUI) this.updateNetplayUI(false);
+ clearTimeout(this.netplay.connectionTimeout);
+ if (this.netplay.stopDrawLoop) this.netplay.stopDrawLoop();
+
+ this.netplayUnfreezeGuest();
+
+ this.netplay.captureRunning = false;
+
+ if (this.netplay.captureCanvas && this.netplay.captureCanvas.parentNode) {
+ try {
+ this.netplay.captureCanvas.parentNode.removeChild(this.netplay.captureCanvas);
+ } catch (e) {}
+ }
+ this.netplay.captureCanvas = null;
+
+ if (this.netplay._hostSourceVideo) {
+ try {
+ this.netplay._hostSourceVideo.srcObject = null;
+ } catch (e) {}
+ if (this.netplay._hostSourceVideo.parentNode) {
+ try {
+ this.netplay._hostSourceVideo.parentNode.removeChild(this.netplay._hostSourceVideo);
+ } catch (e) {}
+ }
+ this.netplay._hostSourceVideo = null;
+ }
+
+ if (this.netplay._hostRawStream) {
+ try {
+ this.netplay._hostRawStream.getTracks().forEach((tr) => {
+ tr.stop();
+ });
+ } catch (e) {}
+ this.netplay._hostRawStream = null;
+ }
+ if (this.netplay._hostOutStream) {
+ try {
+ this.netplay._hostOutStream.getTracks().forEach((tr) => {
+ tr.stop();
+ });
+ } catch (e) {}
+ this.netplay._hostOutStream = null;
+ }
+
+ this.netplayRestoreGuestUIZ();
+
+ this.netplayDestroyOverlay();
+
+ if (this.netplayCanvas && this.netplayCanvas.parentNode) {
+ try {
+ this.netplayCanvas.parentNode.removeChild(this.netplayCanvas);
+ } catch (e) {}
+ }
+ this.netplayCanvas = null;
+
+ if (this.canvas && this.netplay._restoreCanvasStyle) {
+ this.canvas.style.opacity = this.netplay._restoreCanvasStyle.opacity || "";
+ this.canvas.style.pointerEvents = this.netplay._restoreCanvasStyle.pointerEvents || "";
+ this.canvas.style.visibility = this.netplay._restoreCanvasStyle.visibility || "";
+ this.netplay._restoreCanvasStyle = null;
+ } else if (this.canvas) {
+ this.canvas.style.opacity = "";
+ }
+
+ if (this.netplay.remoteAudioContext) {
+ try {
+ this.netplay.remoteAudioContext.close();
+ } catch (e) {}
+ this.netplay.remoteAudioContext = null;
+ this.netplay.remoteGainNode = null;
+ }
+
+ try {
+ const els = document.querySelectorAll("audio[id^=\"ejs-remote-audio-\"]");
+ els.forEach((a) => {
+ try {
+ a.pause();
+ } catch (e) {}
+ try {
+ a.srcObject = null;
+ } catch (e) {}
+ try {
+ a.remove();
+ } catch (e) {}
+ });
+ } catch (e) {}
+ if (this.netplay.remoteAudioElements) this.netplay.remoteAudioElements = {};
+ if (this.netplay._audioUnlockCleanup) {
+ try {
+ this.netplay._audioUnlockCleanup();
+ } catch (e) {}
+ this.netplay._audioUnlockCleanup = null;
+ }
+ this.netplay._audioUnlockArmed = false;
+
+ if (this.netplay.owner && this.netplaySendMessage) {
+ try {
+ this.netplaySendMessage({ type: "host-left" });
+ } catch (e) {}
+ }
+
+ if (this.netplay.socket) {
+ try {
+ if (this.netplay.socket.connected) this.netplay.socket.emit("leave-room");
+ this.netplay.socket.disconnect();
+ } catch (e) {}
+ this.netplay.socket = null;
+ }
+
+ if (this.netplay.localStream) {
+ try {
+ this.netplay.localStream.getTracks().forEach((tr) => {
+ tr.stop();
+ });
+ } catch (e) {}
+ this.netplay.localStream = null;
+ }
+
+ const pcs = this.netplay.peerConnections || {};
+ for (const key in pcs) {
+ if (pcs[key] && pcs[key].pc) {
+ try {
+ pcs[key].pc.close();
+ } catch (e) {}
+ }
+ }
+ this.netplay.peerConnections = {};
+
+ if (this.netplay.video) {
+ try {
+ this.netplay.video.srcObject = null;
+ } catch (e) {}
+ this.netplay.video = null;
+ }
+
+ if (this.netplay.createButton) this.netplay.createButton.innerText = this.localization("Create Room");
+ if (this.netplay.tabs) {
+ this.netplay.tabs[0].style.display = "";
+ this.netplay.tabs[1].style.display = "none";
+ }
+ if (this.netplay.roomNameElem) this.netplay.roomNameElem.innerText = "";
+ if (this.netplay.passwordElem) this.netplay.passwordElem.style.display = "none";
+ if (this.netplay.playerTable) this.netplay.playerTable.innerHTML = "";
+
+ if (this.elements.bottomBar && this.elements.bottomBar.cheat && this.elements.bottomBar.cheat[0]) {
+ this.elements.bottomBar.cheat[0].style.display = this.netplay.oldCheatDisplay || "";
+ }
+
+ if (this.netplay.originalSimulateInput && this.gameManager && this.gameManager.functions) {
+ this.gameManager.functions.simulateInput = this.netplay.originalSimulateInput;
+ }
+
+ this.isNetplay = false;
+ this.netplay.owner = false;
+ this.netplay.players = {};
+ this.netplay.playerID = null;
+ this.netplay.webRtcReady = false;
+
+ if (this.originalControls) {
+ this.controls = JSON.parse(JSON.stringify(this.originalControls));
+ this.originalControls = null;
+ }
+
+ setTimeout(() => {
+ if (this.handleResize) this.handleResize();
+ }, 100);
+
+ this.displayMessage("Left room", 3000);
+ this.netplay._leaving = false;
+ };
+
+ this.netplay.simulateInput = (player, index, value) => {
+ if (!this.isNetplay || !this.gameManager || !this.gameManager.functions || !this.gameManager.functions.simulateInput) return;
+ const pidx = this.netplayGetUserIndex();
+ const f = this.netplay.currentFrame || 0;
+ if (this.netplay.owner) {
+ this.netplay.inputsData[f] = this.netplay.inputsData[f] || [];
+ this.netplay.inputsData[f].push({ frame: f, connected_input: [pidx, index, value] });
+ this.gameManager.functions.simulateInput(pidx, index, value);
+ } else {
+ this.gameManager.functions.simulateInput(pidx, index, value);
+ if (this.netplaySendMessage) {
+ this.netplaySendMessage({ "sync-control": [{ frame: f + 20, connected_input: [pidx, index, value] }] });
+ }
+ }
+ };
+
+ this.netplayGetOpenRooms = () => {
+ if (!this.netplay || !this.netplay.url) return Promise.resolve({});
+ return fetch(this.netplay.url + "/list?domain=" + window.location.host + "&game_id=" + this.config.gameId)
+ .then((res) => res.text())
+ .then((text) => JSON.parse(text))
+ .catch(() => ({}));
+ };
+
+ this.netplayUpdateTableList = () => {
+ if (!this.netplay || !this.netplay.table) return Promise.resolve();
+ return this.netplayGetOpenRooms().then((rooms) => {
+ this.netplay.table.innerHTML = "";
+ for (const k in rooms) {
+ ((id, r) => {
+ const row = this.createElement("tr");
+ row.classList.add("ejs_netplay_table_row");
+ const c1 = this.createElement("td");
+ c1.innerText = r.room_name;
+ c1.style.textAlign = "left";
+ c1.style.padding = "10px 0";
+ const c2 = this.createElement("td");
+ c2.innerText = r.current + "/" + r.max;
+ c2.style.width = "80px";
+ c2.style.textAlign = "center";
+ const c3 = this.createElement("td");
+ c3.style.width = "80px";
+ if (r.current < r.max) {
+ const btn = this.createElement("button");
+ btn.classList.add("ejs_netplay_join_button", "ejs_button_button");
+ btn.style.backgroundColor = "rgba(var(--ejs-primary-color),1)";
+ btn.innerText = this.localization("Join");
+ c3.appendChild(btn);
+ this.addEventListener(btn, "click", () => {
+ if (r.hasPassword) {
+ this.netplayShowJoinPasswordDialog(id, r.room_name, r.max);
+ } else {
+ this.netplayJoinRoom(id, r.room_name, r.max, null);
+ }
+ });
+ }
+ row.appendChild(c1);
+ row.appendChild(c2);
+ row.appendChild(c3);
+ this.netplay.table.appendChild(row);
+ })(k, rooms[k]);
+ }
+ }).catch(() => {});
+ };
+
+ this.netplayUpdateListStart = () => {
+ this.netplay.updateListInterval = setInterval(() => {
+ this.netplayUpdateTableList();
+ }, 1000);
+ };
+
+ this.netplayUpdateListStop = () => {
+ clearInterval(this.netplay.updateListInterval);
+ };
+
+ this.netplayShowOpenRoomDialog = () => {
+ if (!this.createSubPopup) return;
+ this.originalControls = JSON.parse(JSON.stringify(this.controls));
+ const popups = this.createSubPopup();
+ this.netplayMenu.appendChild(popups[0]);
+ popups[1].classList.add("ejs_cheat_parent");
+ const title = this.createElement("h2");
+ title.innerText = this.localization("Create a room");
+ title.classList.add("ejs_netplay_name_heading");
+ popups[1].appendChild(title);
+ const form = this.createElement("div");
+ form.classList.add("ejs_netplay_header");
+ const ni = this.createElement("input");
+ ni.type = "text";
+ ni.maxLength = 20;
+ const ms = this.createElement("select");
+ ["2", "3", "4"].forEach((v) => {
+ const o = document.createElement("option");
+ o.value = v;
+ o.innerText = v;
+ ms.appendChild(o);
+ });
+ const pw = this.createElement("input");
+ pw.type = "text";
+ pw.maxLength = 20;
+ [["Room Name", ni], ["Max Players", ms], ["Password (optional)", pw]].forEach((item) => {
+ const s = this.createElement("strong");
+ s.innerText = this.localization(item[0]);
+ form.appendChild(s);
+ form.appendChild(this.createElement("br"));
+ form.appendChild(item[1]);
+ });
+ popups[1].appendChild(form);
+ const sub = this.createElement("button");
+ sub.classList.add("ejs_button_button", "ejs_popup_submit");
+ sub.style.backgroundColor = "rgba(var(--ejs-primary-color),1)";
+ sub.style.margin = "10px";
+ sub.innerText = this.localization("Submit");
+ this.addEventListener(sub, "click", () => {
+ const n = ni.value.trim();
+ if (n) {
+ this.netplayOpenRoom(n, parseInt(ms.value, 10), pw.value.trim());
+ popups[0].remove();
+ }
+ });
+ const cls = this.createElement("button");
+ cls.classList.add("ejs_button_button", "ejs_popup_submit");
+ cls.style.margin = "10px";
+ cls.innerText = this.localization("Close");
+ this.addEventListener(cls, "click", () => {
+ popups[0].remove();
+ });
+ popups[1].appendChild(sub);
+ popups[1].appendChild(cls);
+ };
+
+ this.netplayShowJoinPasswordDialog = (roomId, roomName, maxPlayers) => {
+ if (!this.createSubPopup) return;
+
+ const popups = this.createSubPopup();
+ this.netplayMenu.appendChild(popups[0]);
+ popups[1].classList.add("ejs_cheat_parent");
+
+ const title = this.createElement("h2");
+ title.innerText = this.localization("Enter Password");
+ title.classList.add("ejs_netplay_name_heading");
+ popups[1].appendChild(title);
+
+ const form = this.createElement("div");
+ form.classList.add("ejs_netplay_header");
+
+ const roomLabel = this.createElement("div");
+ roomLabel.classList.add("ejs_netplay_dialog_label");
+ roomLabel.innerText = this.localization("Room") + ": " + roomName;
+ form.appendChild(roomLabel);
+
+ const pwLabel = this.createElement("strong");
+ pwLabel.innerText = this.localization("Password");
+ form.appendChild(pwLabel);
+ form.appendChild(this.createElement("br"));
+
+ const pwInput = this.createElement("input");
+ pwInput.type = "password";
+ pwInput.maxLength = 20;
+ pwInput.placeholder = this.localization("Enter room password");
+ form.appendChild(pwInput);
+
+ popups[1].appendChild(form);
+
+ const buttonRow = this.createElement("div");
+ buttonRow.classList.add("ejs_netplay_dialog_buttons");
+
+ const joinBtn = this.createElement("button");
+ joinBtn.classList.add("ejs_button_button", "ejs_popup_submit");
+ joinBtn.style.backgroundColor = "rgba(var(--ejs-primary-color),1)";
+ joinBtn.innerText = this.localization("Join");
+
+ const cancelBtn = this.createElement("button");
+ cancelBtn.classList.add("ejs_button_button", "ejs_popup_submit");
+ cancelBtn.innerText = this.localization("Cancel");
+
+ this.addEventListener(joinBtn, "click", () => {
+ const pw = pwInput.value.trim();
+ popups[0].remove();
+ if (pw) {
+ this.netplayJoinRoom(roomId, roomName, maxPlayers, pw);
+ }
+ });
+
+ this.addEventListener(cancelBtn, "click", () => {
+ popups[0].remove();
+ });
+
+ this.addEventListener(pwInput, "keydown", (e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ const pw = pwInput.value.trim();
+ popups[0].remove();
+ if (pw) {
+ this.netplayJoinRoom(roomId, roomName, maxPlayers, pw);
+ }
+ }
+ if (e.key === "Escape") {
+ popups[0].remove();
+ }
+ });
+
+ buttonRow.appendChild(joinBtn);
+ buttonRow.appendChild(cancelBtn);
+ popups[1].appendChild(buttonRow);
+
+ setTimeout(() => {
+ pwInput.focus();
+ }, 50);
+ };
+
+ this.netplayShowJoinErrorDialog = (roomId, roomName, maxPlayers, errorMessage, hadPassword) => {
+ if (!this.createSubPopup) {
+ this.displayMessage(this.localization("Join error") + ": " + errorMessage, 5000);
+ return;
+ }
+
+ const popups = this.createSubPopup();
+ this.netplayMenu.appendChild(popups[0]);
+ popups[1].classList.add("ejs_cheat_parent");
+
+ const title = this.createElement("h2");
+ title.innerText = this.localization("Unable to Join");
+ title.classList.add("ejs_netplay_name_heading");
+ popups[1].appendChild(title);
+
+ const content = this.createElement("div");
+ content.classList.add("ejs_netplay_header");
+
+ const roomLabel = this.createElement("div");
+ roomLabel.classList.add("ejs_netplay_dialog_label");
+ roomLabel.innerText = this.localization("Room") + ": " + roomName;
+ content.appendChild(roomLabel);
+
+ const errorBox = this.createElement("div");
+ errorBox.classList.add("ejs_netplay_error_box");
+ errorBox.innerText = errorMessage;
+ content.appendChild(errorBox);
+
+ popups[1].appendChild(content);
+
+ const buttonRow = this.createElement("div");
+ buttonRow.classList.add("ejs_netplay_dialog_buttons");
+
+ if (hadPassword) {
+ const retryBtn = this.createElement("button");
+ retryBtn.classList.add("ejs_button_button", "ejs_popup_submit");
+ retryBtn.style.backgroundColor = "rgba(var(--ejs-primary-color),1)";
+ retryBtn.innerText = this.localization("Try Again");
+
+ this.addEventListener(retryBtn, "click", () => {
+ popups[0].remove();
+ this.netplayShowJoinPasswordDialog(roomId, roomName, maxPlayers);
+ });
+
+ buttonRow.appendChild(retryBtn);
+ }
+
+ const closeBtn = this.createElement("button");
+ closeBtn.classList.add("ejs_button_button", "ejs_popup_submit");
+ closeBtn.innerText = this.localization("Close");
+
+ this.addEventListener(closeBtn, "click", () => {
+ popups[0].remove();
+ });
+
+ buttonRow.appendChild(closeBtn);
+ popups[1].appendChild(buttonRow);
+ };
+
+ this.netplayStartSocketIO = (cb) => {
+ this.netplayUnlockMobileAudio = this.netplayUnlockMobileAudio || (() => {
+ const ctx = this.Module && this.Module.AL && this.Module.AL.currentCtx && this.Module.AL.currentCtx.audioCtx;
+ if (!ctx) return;
+
+ try {
+ if (ctx.state !== "running") ctx.resume().catch(() => {});
+ } catch (e) {}
+
+ try {
+ const b = ctx.createBuffer(1, 1, ctx.sampleRate);
+ const s = ctx.createBufferSource();
+ s.buffer = b;
+ s.connect(ctx.destination);
+ s.start(0);
+ s.stop(0);
+ } catch (e) {}
+ });
+
+ this.netplayChatAppend = this.netplayChatAppend || ((payload) => {
+ if (!this.netplay || !this.netplay.chatLog) return;
+
+ const name = (payload && payload.player_name) ? payload.player_name : "Player";
+ const msg = (payload && payload.message) ? payload.message : "";
+ const to = (payload && payload.to) ? payload.to : "all";
+ const isPrivate = to && to !== "all";
+
+ const line = document.createElement("div");
+ line.style.margin = "2px 0";
+
+ line.textContent = isPrivate ? (name + " (private): " + msg) : (name + ": " + msg);
+
+ this.netplay.chatLog.appendChild(line);
+ this.netplay.chatLog.scrollTop = this.netplay.chatLog.scrollHeight;
+ });
+
+ this.netplayChatRefreshRecipients = this.netplayChatRefreshRecipients || (() => {
+ if (!this.netplay || !this.netplay.chatTo) return;
+
+ const sel = this.netplay.chatTo;
+ const prev = sel.value || "all";
+
+ sel.innerHTML = "";
+
+ const optAll = document.createElement("option");
+ optAll.value = "all";
+ optAll.innerText = this.localization ? this.localization("Everyone") : "Everyone";
+ sel.appendChild(optAll);
+
+ const players = (this.netplay && this.netplay.players) ? this.netplay.players : {};
+ Object.keys(players).forEach((userid) => {
+ const p = players[userid];
+ const opt = document.createElement("option");
+ opt.value = userid;
+ opt.innerText = (p && p.player_name) ? p.player_name : "Player";
+ sel.appendChild(opt);
+ });
+
+ const exists = Array.from(sel.options).some((o) => o.value === prev);
+ sel.value = exists ? prev : "all";
+ });
+
+ this.netplayBindChatUI = this.netplayBindChatUI || (() => {
+ if (!this.netplay || this.netplay._chatBound) return;
+ if (!this.netplay.chatSend || !this.netplay.chatInput) return;
+
+ this.netplay._chatBound = true;
+
+ this.addEventListener(this.netplay.chatSend, "click", () => {
+ this.netplayChatSend();
+ });
+
+ this.addEventListener(this.netplay.chatInput, "keydown", (e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ this.netplayChatSend();
+ }
+ });
+ });
+
+ if (typeof io === "undefined") {
+ this.displayMessage("Socket.IO unavailable", 5000);
+ return;
+ }
+ if (this.netplay.socket && this.netplay.socket.connected) {
+ cb();
+ return;
+ }
+ if (!this.netplay.url) {
+ this.displayMessage("Network error", 5000);
+ return;
+ }
+
+ this.netplay.previousPlayers = {};
+ this.netplay.socket = io(this.netplay.url);
+
+ this.netplay.socket.on("connect", () => {
+ this.netplayBindChatUI();
+ cb();
+ });
+
+ this.netplay.socket.on("connect_error", (e) => {
+ this.displayMessage("Connect error: " + e.message, 5000);
+ });
+
+ this.netplay.socket.on("disconnect", () => {
+ this.netplayLeaveRoom();
+ });
+
+ this.netplay.socket.on("users-updated", (users) => {
+ const pv = Object.keys(this.netplay.previousPlayers || {});
+ const cu = Object.keys(users || {});
+ cu.forEach((id) => {
+ if (pv.indexOf(id) === -1 && id !== this.netplay.playerID) {
+ this.displayMessage((users[id].player_name || "Player") + " joined");
+ }
+ });
+ pv.forEach((id) => {
+ if (cu.indexOf(id) === -1) {
+ this.displayMessage((this.netplay.previousPlayers[id].player_name || "Player") + " left");
+ }
+ });
+
+ this.netplay.previousPlayers = users;
+ this.netplay.players = users;
+
+ this.netplayUpdatePlayersTable();
+
+ this.netplayChatRefreshRecipients();
+
+ if (this.netplay.owner) {
+ this.netplayInitWebRTCStream().then(() => {
+ Object.keys(users).forEach((pid) => {
+ if (pid !== this.netplay.playerID) {
+ const sid = users[pid].socketId;
+ if (sid && !this.netplay.peerConnections[sid]) {
+ this.netplayCreatePeerConnection(sid);
+ }
+ }
+ });
+ });
+ }
+ });
+
+ this.netplay.socket.on("data-message", (d) => {
+ this.netplayDataMessage(d);
+ });
+
+ this.netplay.socket.on("webrtc-signal", (data) => {
+ const sender = data.sender;
+ const offer = data.offer;
+ const answer = data.answer;
+ const candidate = data.candidate;
+ const requestRenegotiate = data.requestRenegotiate;
+
+ if (requestRenegotiate && this.netplay.owner && sender) {
+ if (this.debug) console.log("[NETPLAY HOST] Renegotiate requested by " + sender + " (" + (data.reason || "") + ")");
+ try {
+ if (this.netplay.peerConnections[sender] && this.netplay.peerConnections[sender].pc) {
+ this.netplay.peerConnections[sender].pc.close();
+ }
+ } catch (e) {}
+ delete this.netplay.peerConnections[sender];
+
+ this.netplayInitWebRTCStream().then(() => {
+ this.netplayCreatePeerConnection(sender);
+ });
+ return;
+ }
+
+ if (!sender) return;
+
+ let pd = this.netplay.peerConnections[sender];
+ if (!pd) {
+ pd = { pc: this.netplayCreatePeerConnection(sender), iceCandidateQueue: [] };
+ this.netplay.peerConnections[sender] = pd;
+ }
+ pd.iceCandidateQueue = pd.iceCandidateQueue || [];
+ const pc = pd.pc;
+
+ if (offer) {
+ pc.setRemoteDescription(new RTCSessionDescription(offer)).then(() => {
+ pd.iceCandidateQueue.forEach((c) => {
+ pc.addIceCandidate(new RTCIceCandidate(c));
+ });
+ pd.iceCandidateQueue = [];
+ return pc.createAnswer();
+ }).then((ans) => pc.setLocalDescription(ans))
+ .then(() => {
+ this.netplay.socket.emit("webrtc-signal", { target: sender, answer: pc.localDescription });
+ }).catch((err) => {
+ if (this.debug) console.error("[NETPLAY GUEST] Answer error:", err);
+ });
+
+ } else if (answer) {
+ pc.setRemoteDescription(new RTCSessionDescription(answer)).then(() => {
+ pd.iceCandidateQueue.forEach((c) => {
+ pc.addIceCandidate(new RTCIceCandidate(c));
+ });
+ pd.iceCandidateQueue = [];
+ }).catch((err) => {
+ if (this.debug) console.error("[NETPLAY HOST] Set answer error:", err);
+ });
+
+ } else if (candidate) {
+ if (pc.remoteDescription) {
+ pc.addIceCandidate(new RTCIceCandidate(candidate)).catch(() => {});
+ } else {
+ pd.iceCandidateQueue.push(candidate);
+ }
+ }
+ });
+ };
+
+ this.netplayUpdatePlayersTable = () => {
+ if (!this.netplay.playerTable) return;
+ this.netplay.playerTable.innerHTML = "";
+ let i = 0;
+ const keys = Object.keys(this.netplay.players || {});
+ keys.forEach((k) => {
+ const row = this.createElement("tr");
+ const values = [i + 1, this.netplay.players[k].player_name || "Unknown", i === 0 ? keys.length + "/" + (this.netplay.maxPlayers || "?") : ""];
+ values.forEach((t) => {
+ const td = this.createElement("td");
+ td.innerText = t;
+ row.appendChild(td);
+ });
+ this.netplay.playerTable.appendChild(row);
+ i++;
+ });
+ this.netplayChatRefreshRecipients();
+ };
+
+ this.netplayOpenRoom = (rn, mp, pw) => {
+ if (this.netplayUnlockMobileAudio) this.netplayUnlockMobileAudio();
+
+ if (this.Module && this.Module.AL && this.Module.AL.currentCtx && this.Module.AL.currentCtx.audioCtx) {
+ this.Module.AL.currentCtx.audioCtx.resume().catch(() => {});
+ }
+
+ const sid = guid();
+ this.netplay.playerID = guid();
+ this.netplay.players = {};
+ this.netplay.maxPlayers = mp;
+ this.netplay.extra = {
+ domain: window.location.host,
+ game_id: this.config.gameId,
+ room_name: rn,
+ player_name: this.netplay.name,
+ userid: this.netplay.playerID,
+ sessionid: sid
+ };
+ this.netplay.players[this.netplay.playerID] = this.netplay.extra;
+ this.netplay.owner = true;
+
+ this.netplayStartSocketIO(() => {
+ this.netplay.socket.emit("open-room", { extra: this.netplay.extra, maxPlayers: mp, password: pw }, (e) => {
+ if (e) {
+ this.displayMessage("Room error: " + e, 5000);
+ return;
+ }
+ this.netplayRoomJoined(true, rn, pw, sid);
+ });
+ });
+ };
+
+ this.netplayJoinRoom = (sid, rn, mp, pw) => {
+ if (this.netplayUnlockMobileAudio) this.netplayUnlockMobileAudio();
+ this.netplay.playerID = guid();
+ this.netplay.players = {};
+ this.netplay.maxPlayers = mp;
+ this.netplay.extra = {
+ domain: window.location.host,
+ game_id: this.config.gameId,
+ room_name: rn,
+ player_name: this.netplay.name,
+ userid: this.netplay.playerID,
+ sessionid: sid
+ };
+ this.netplay.players[this.netplay.playerID] = this.netplay.extra;
+ this.netplay.owner = false;
+
+ this.netplayStartSocketIO(() => {
+ this.netplay.socket.emit("join-room", { extra: this.netplay.extra, password: pw }, (e, u) => {
+ if (e) {
+ this.netplayShowJoinErrorDialog(sid, rn, mp, e, !!pw);
+ return;
+ }
+ this.netplay.players = u;
+ this.netplayRoomJoined(false, rn, pw, sid);
+ });
+ });
+ };
+
+ this.netplayDataMessage = (d) => {
+ if (d["chat-message"]) {
+ const chat = d["chat-message"];
+ const to = chat.to || "all";
+ const from = chat.from || "";
+
+ if (to !== "all" && to !== this.netplay.playerID) return;
+
+ if (from === this.netplay.playerID) return;
+
+ this.netplayChatAppend(chat);
+
+ try {
+ const name = chat.player_name || "Player";
+ const msg = chat.message || "";
+ const typing = this.netplay &&
+ this.netplay.chatInput &&
+ document.activeElement === this.netplay.chatInput;
+
+ if (!typing && this.displayMessage) {
+ const prefix = (to !== "all") ? "(private) " : "";
+ this.displayMessage(prefix + name + ": " + msg, 4500);
+ }
+ } catch (e) {}
+ return;
+ }
+
+ if (d["sync-control"]) {
+ d["sync-control"].forEach((v) => {
+ const f = parseInt(v.frame, 10);
+ if (!v.connected_input || v.connected_input[0] < 0) return;
+ this.netplay.inputsData[f] = this.netplay.inputsData[f] || [];
+ this.netplay.inputsData[f].push(v);
+ this.netplaySendMessage({ frameAck: f });
+ if (this.netplay.owner && this.gameManager && this.gameManager.functions && this.gameManager.functions.simulateInput) {
+ this.gameManager.functions.simulateInput(v.connected_input[0], v.connected_input[1], v.connected_input[2]);
+ }
+ });
+ }
+ };
+
+ this.netplaySendMessage = (d) => {
+ if (this.netplay.socket && this.netplay.socket.connected) {
+ this.netplay.socket.emit("data-message", d);
+ }
+ };
+
+ this.netplayReset = () => {
+ this.netplay.init_frame = this.gameManager ? this.gameManager.getFrameNum() : 0;
+ this.netplay.currentFrame = 0;
+ this.netplay.inputsData = {};
+ };
+
+ this.netplayInitModulePostMainLoop = () => {
+ if (this.isNetplay && !this.netplay.owner) return;
+ this.netplay.currentFrame = (this.gameManager ? this.gameManager.getFrameNum() : 0) - (this.netplay.init_frame || 0);
+ if (!this.isNetplay || !this.netplay.owner) return;
+
+ const i = this.netplay.currentFrame;
+ if (this.netplay.inputsData[i]) {
+ const ts = this.netplay.inputsData[i].map((v) => {
+ if (this.gameManager && this.gameManager.functions && this.gameManager.functions.simulateInput) {
+ this.gameManager.functions.simulateInput(v.connected_input[0], v.connected_input[1], v.connected_input[2]);
+ }
+ return { frame: i + 20, connected_input: v.connected_input };
+ });
+ this.netplaySendMessage({ "sync-control": ts });
+ delete this.netplay.inputsData[i];
+ }
+ };
+
+ this.netplay.updateList = { start: this.netplayUpdateListStart.bind(this), stop: this.netplayUpdateListStop.bind(this) };
+ this.netplay.showOpenRoomDialog = this.netplayShowOpenRoomDialog.bind(this);
+ this.netplay.openRoom = this.netplayOpenRoom.bind(this);
+ this.netplay.joinRoom = this.netplayJoinRoom.bind(this);
+ this.netplay.leaveRoom = this.netplayLeaveRoom.bind(this);
+ this.netplay.sendMessage = this.netplaySendMessage.bind(this);
+ this.netplay.updatePlayersTable = this.netplayUpdatePlayersTable.bind(this);
+ this.netplay.createPeerConnection = this.netplayCreatePeerConnection.bind(this);
+ this.netplay.initWebRTCStream = this.netplayInitWebRTCStream.bind(this);
+ this.netplay.roomJoined = this.netplayRoomJoined.bind(this);
+ this.netplay.reset = this.netplayReset.bind(this);
+
+ this.netplay.init_frame = 0;
+ this.netplay.currentFrame = 0;
+ this.netplay.inputsData = {};
+ this.netplay.webRtcReady = false;
+ this.netplay.peerConnections = {};
+
+ this.netplay.url = this.config.netplayUrl || window.EJS_netplayUrl;
+ if (!this.netplay.url) {
+ this.displayMessage("Netplay URL not configured", 5000);
+ return;
+ }
+ while (this.netplay.url.endsWith("/")) {
+ this.netplay.url = this.netplay.url.slice(0, -1);
+ }
+
+ if (this.gameManager && this.gameManager.Module) {
+ this.gameManager.Module.postMainLoop = this.netplayInitModulePostMainLoop.bind(this);
+ } else if (this.Module) {
+ this.Module.postMainLoop = this.netplayInitModulePostMainLoop.bind(this);
+ }
+ }
+
+};
\ No newline at end of file