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