diff --git a/backend/config/config_manager.py b/backend/config/config_manager.py index bba192195..05eb04137 100644 --- a/backend/config/config_manager.py +++ b/backend/config/config_manager.py @@ -1,6 +1,6 @@ import json import sys -from typing import Final +from typing import Final, NotRequired, TypedDict import pydash import yaml @@ -30,6 +30,11 @@ ROMM_USER_CONFIG_FILE: Final = f"{ROMM_USER_CONFIG_PATH}/config.yml" SQLITE_DB_BASE_PATH: Final = f"{ROMM_BASE_PATH}/database" +class EjsControlsButton(TypedDict): + value: NotRequired[str] + value2: NotRequired[str] + + class Config: EXCLUDED_PLATFORMS: list[str] EXCLUDED_SINGLE_EXT: list[str] @@ -41,7 +46,13 @@ class Config: PLATFORMS_VERSIONS: dict[str, str] ROMS_FOLDER_NAME: str FIRMWARE_FOLDER_NAME: str - EJS_CORE_OPTIONS: dict[str, dict[str, str]] + EJS_DEBUG: bool + EJS_OPTIONS: dict[ + str, dict[str, str] + ] # dict[core_name, dict[option_name, option_value]] + EJS_CONTROLS: dict[ + str, dict[int, dict[int, EjsControlsButton]] + ] # dict[core_name, dict[player_number, dict[button_number, EjsControlsButton]]] HIGH_PRIO_STRUCTURE_PATH: str def __init__(self, **entries): @@ -150,7 +161,9 @@ class ConfigManager: FIRMWARE_FOLDER_NAME=pydash.get( self._raw_config, "filesystem.firmware_folder", "bios" ), - EJS_CORE_OPTIONS=pydash.get(self._raw_config, "emulatorjs", {}), + EJS_DEBUG=pydash.get(self._raw_config, "emulatorjs.debug", False), + EJS_OPTIONS=pydash.get(self._raw_config, "emulatorjs.options", {}), + EJS_CONTROLS=pydash.get(self._raw_config, "emulatorjs.controls", {}), ) def _validate_config(self): @@ -233,17 +246,46 @@ class ConfigManager: ) sys.exit(3) - if not isinstance(self.config.EJS_CORE_OPTIONS, dict): + if not isinstance(self.config.EJS_DEBUG, bool): + log.critical("Invalid config.yml: emulatorjs.debug must be a boolean") + sys.exit(3) + + if not isinstance(self.config.EJS_OPTIONS, dict): log.critical("Invalid config.yml: emulatorjs must be a dictionary") sys.exit(3) else: - for core, options in self.config.EJS_CORE_OPTIONS.items(): + for core, options in self.config.EJS_OPTIONS.items(): if not isinstance(options, dict): log.critical( f"Invalid config.yml: emulatorjs.{core} must be a dictionary" ) sys.exit(3) + if not isinstance(self.config.EJS_CONTROLS, dict): + log.critical("Invalid config.yml: emulatorjs.controls must be a dictionary") + sys.exit(3) + else: + for core, controls in self.config.EJS_CONTROLS.items(): + if not isinstance(controls, dict): + log.critical( + f"Invalid config.yml: emulatorjs.controls.{core} must be a dictionary" + ) + sys.exit(3) + + for player, buttons in controls.items(): + if not isinstance(buttons, dict): + log.critical( + f"Invalid config.yml: emulatorjs.controls.{core}.{player} must be a dictionary" + ) + sys.exit(3) + + for button, value in buttons.items(): + if not isinstance(value, dict): + log.critical( + f"Invalid config.yml: emulatorjs.controls.{core}.{player}.{button} must be a dictionary" + ) + sys.exit(3) + def get_config(self) -> Config: with open(self.config_file) as config_file: self._raw_config = yaml.load(config_file, Loader=SafeLoader) or {} diff --git a/backend/endpoints/configs.py b/backend/endpoints/configs.py index eef5fc9bb..b54c516b6 100644 --- a/backend/endpoints/configs.py +++ b/backend/endpoints/configs.py @@ -35,7 +35,9 @@ def get_config() -> ConfigResponse: EXCLUDED_MULTI_PARTS_FILES=cfg.EXCLUDED_MULTI_PARTS_FILES, PLATFORMS_BINDING=cfg.PLATFORMS_BINDING, PLATFORMS_VERSIONS=cfg.PLATFORMS_VERSIONS, - EJS_CORE_OPTIONS=cfg.EJS_CORE_OPTIONS, + EJS_DEBUG=cfg.EJS_DEBUG, + EJS_CONTROLS=cfg.EJS_CONTROLS, + EJS_OPTIONS=cfg.EJS_OPTIONS, ) except ConfigNotReadableException as exc: log.critical(exc.message) diff --git a/backend/endpoints/responses/config.py b/backend/endpoints/responses/config.py index 3fcb28b40..072f40305 100644 --- a/backend/endpoints/responses/config.py +++ b/backend/endpoints/responses/config.py @@ -1,5 +1,7 @@ from typing import TypedDict +from config.config_manager import EjsControlsButton + class ConfigResponse(TypedDict): EXCLUDED_PLATFORMS: list[str] @@ -10,4 +12,6 @@ class ConfigResponse(TypedDict): EXCLUDED_MULTI_PARTS_FILES: list[str] PLATFORMS_BINDING: dict[str, str] PLATFORMS_VERSIONS: dict[str, str] - EJS_CORE_OPTIONS: dict[str, dict[str, str]] + EJS_DEBUG: bool + EJS_OPTIONS: dict[str, dict[str, str]] + EJS_CONTROLS: dict[str, dict[int, dict[int, EjsControlsButton]]] diff --git a/backend/tests/config/fixtures/config/config.yml b/backend/tests/config/fixtures/config/config.yml index 7bcce587a..fbe513fcc 100644 --- a/backend/tests/config/fixtures/config/config.yml +++ b/backend/tests/config/fixtures/config/config.yml @@ -31,7 +31,15 @@ filesystem: firmware_folder: "BIOS" emulatorjs: - parallel_n64: - vsync: disable - snes9x: - snes9x_region: ntsc + debug: true + options: + parallel_n64: + vsync: disable + snes9x: + snes9x_region: ntsc + controls: + snes9x: + 0: + 0: + value: x + value2: BUTTON_2 diff --git a/backend/tests/config/test_config_loader.py b/backend/tests/config/test_config_loader.py index fdeb2c5bf..1b36b9f5b 100644 --- a/backend/tests/config/test_config_loader.py +++ b/backend/tests/config/test_config_loader.py @@ -19,10 +19,16 @@ def test_config_loader(): assert loader.config.PLATFORMS_VERSIONS == {"naomi": "arcade"} assert loader.config.ROMS_FOLDER_NAME == "ROMS" assert loader.config.FIRMWARE_FOLDER_NAME == "BIOS" - assert loader.config.EJS_CORE_OPTIONS == { + assert loader.config.EJS_DEBUG + assert loader.config.EJS_OPTIONS == { "parallel_n64": {"vsync": "disable"}, "snes9x": {"snes9x_region": "ntsc"}, } + assert loader.config.EJS_CONTROLS == { + "snes9x": { + 0: {0: {"value": "x", "value2": "BUTTON_2"}}, + }, + } def test_empty_config_loader(): @@ -42,4 +48,6 @@ def test_empty_config_loader(): assert loader.config.PLATFORMS_VERSIONS == {} assert loader.config.ROMS_FOLDER_NAME == "roms" assert loader.config.FIRMWARE_FOLDER_NAME == "bios" - assert loader.config.EJS_CORE_OPTIONS == {} + assert not loader.config.EJS_DEBUG + assert loader.config.EJS_OPTIONS == {} + assert loader.config.EJS_CONTROLS == {} diff --git a/examples/config.example.yml b/examples/config.example.yml index 10d5be04c..5dd6a41b3 100644 --- a/examples/config.example.yml +++ b/examples/config.example.yml @@ -49,9 +49,20 @@ filesystem: {} # { roms_folder: 'roms' } For example if your folder structure is # EmulatorJS per-core options emulatorjs: - parallel_n64: # Use the exact core name - vsync: disable - snes9x: - snes9x_region: ntsc - default: # These settings apply to all cores - fps: show + debug: true # Available options will be logged to the browser consolewhen the emulator is started + options: + parallel_n64: # Use the exact core name + vsync: disable + snes9x: + snes9x_region: ntsc + default: # These settings apply to all cores + fps: show + controls: # https://emulatorjs.org/docs4devs/control-mapping/ + snes9x: + 0: # Player 1 + 0: + value: x # Default mapping for keyboard + value2: BUTTON_2 # Default mapping for connected controller + 1: # Player 2 + 2: # Player 3 + 3: # Player 4 diff --git a/frontend/src/__generated__/index.ts b/frontend/src/__generated__/index.ts index ba9eba5b1..0828b737d 100644 --- a/frontend/src/__generated__/index.ts +++ b/frontend/src/__generated__/index.ts @@ -24,6 +24,7 @@ export type { ConfigResponse } from './models/ConfigResponse'; export type { CustomLimitOffsetPage_SimpleRomSchema_ } from './models/CustomLimitOffsetPage_SimpleRomSchema_'; export type { DetailedRomSchema } from './models/DetailedRomSchema'; export type { EarnedAchievement } from './models/EarnedAchievement'; +export type { EjsControlsButton } from './models/EjsControlsButton'; export type { EmulationDict } from './models/EmulationDict'; export type { FilesystemDict } from './models/FilesystemDict'; export type { FirmwareSchema } from './models/FirmwareSchema'; diff --git a/frontend/src/__generated__/models/ConfigResponse.ts b/frontend/src/__generated__/models/ConfigResponse.ts index 09718a036..c2f30eb5b 100644 --- a/frontend/src/__generated__/models/ConfigResponse.ts +++ b/frontend/src/__generated__/models/ConfigResponse.ts @@ -2,6 +2,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { EjsControlsButton } from './EjsControlsButton'; export type ConfigResponse = { EXCLUDED_PLATFORMS: Array; EXCLUDED_SINGLE_EXT: Array; @@ -11,6 +12,8 @@ export type ConfigResponse = { EXCLUDED_MULTI_PARTS_FILES: Array; PLATFORMS_BINDING: Record; PLATFORMS_VERSIONS: Record; - EJS_CORE_OPTIONS: Record>; + EJS_DEBUG: boolean; + EJS_OPTIONS: Record>; + EJS_CONTROLS: Record>>; }; diff --git a/frontend/src/__generated__/models/EjsControlsButton.ts b/frontend/src/__generated__/models/EjsControlsButton.ts new file mode 100644 index 000000000..959fd55e3 --- /dev/null +++ b/frontend/src/__generated__/models/EjsControlsButton.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type EjsControlsButton = { + value?: string; + value2?: string; +}; + diff --git a/frontend/src/stores/config.ts b/frontend/src/stores/config.ts index e3ac454c6..95137ad35 100644 --- a/frontend/src/stores/config.ts +++ b/frontend/src/stores/config.ts @@ -1,6 +1,6 @@ import { defineStore } from "pinia"; -import type { ConfigResponse } from "@/__generated__"; +import type { ConfigResponse, EjsControlsButton } from "@/__generated__"; import api from "@/services/api"; type ExclusionTypes = @@ -20,7 +20,9 @@ const defaultConfig = { EXCLUDED_MULTI_PARTS_FILES: [], PLATFORMS_BINDING: {}, PLATFORMS_VERSIONS: {}, - EJS_CORE_OPTIONS: {}, + EJS_DEBUG: false, + EJS_OPTIONS: {}, + EJS_CONTROLS: {}, } as ConfigResponse; export default defineStore("config", { @@ -68,11 +70,21 @@ export default defineStore("config", { return Object.keys(this.config).includes(type); }, getEJSCoreOptions(core: string | null): Record { - const defaultOptions = this.config.EJS_CORE_OPTIONS["default"]; + const defaultOptions = this.config.EJS_OPTIONS["default"] || {}; if (!core) return defaultOptions; return { ...defaultOptions, - ...this.config.EJS_CORE_OPTIONS[core], + ...this.config.EJS_OPTIONS[core], + }; + }, + getEJSControls( + core: string | null, + ): Record> { + const defaultControls = this.config.EJS_CONTROLS["default"] || {}; + if (!core) return defaultControls; + return { + ...defaultControls, + ...this.config.EJS_CONTROLS[core], }; }, reset() {}, diff --git a/frontend/src/views/Player/EmulatorJS/Player.vue b/frontend/src/views/Player/EmulatorJS/Player.vue index a69b93c14..3d350b95e 100644 --- a/frontend/src/views/Player/EmulatorJS/Player.vue +++ b/frontend/src/views/Player/EmulatorJS/Player.vue @@ -56,7 +56,6 @@ declare global { EJS_player: string; EJS_pathtodata: string; EJS_color: string; - EJS_defaultOptions: object; EJS_gameID: number; EJS_gameName: string; EJS_backgroundImage: string; @@ -72,6 +71,8 @@ declare global { EJS_fullscreenOnLoaded: boolean; EJS_threads: boolean; EJS_controlScheme: string | null; + EJS_defaultOptions: object; + EJS_defaultControls: object; EJS_emulator: any; // eslint-disable-line @typescript-eslint/no-explicit-any EJS_language: string; EJS_disableAutoLang: boolean; @@ -124,13 +125,14 @@ window.EJS_defaultOptions = { rewindEnabled: "enabled", ...coreOptions, }; +window.EJS_defaultControls = configStore.getEJSControls(props.core); // Set a valid game name window.EJS_gameName = romRef.value.fs_name_no_tags .replace(INVALID_CHARS_REGEX, "") .trim(); window.EJS_language = selectedLanguage.value.value.replace("_", "-"); window.EJS_disableAutoLang = true; -window.EJS_DEBUG_XX = true; +window.EJS_DEBUG_XX = configStore.config.EJS_DEBUG; onMounted(() => { window.scrollTo(0, 0);