From 099d58b1e5d117a8fd4e2aa1b799178c0e07d1c6 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Wed, 26 Nov 2025 15:19:45 -0500 Subject: [PATCH] add config for netplay --- backend/config/config_manager.py | 19 ++++++++++++++++++ backend/endpoints/configs.py | 1 + backend/endpoints/responses/config.py | 3 ++- .../tests/config/fixtures/config/config.yml | 6 ++++++ backend/tests/config/test_config_loader.py | 9 +++++++++ examples/config.example.yml | 6 ++++++ frontend/src/__generated__/index.ts | 2 ++ .../__generated__/models/ConfigResponse.ts | 2 ++ .../__generated__/models/NetplayICEServer.ts | 10 ++++++++++ .../models/RomGamelistMetadata.ts | 10 +++++----- .../src/__generated__/models/RomSSMetadata.ts | 2 ++ .../src/__generated__/models/RoomsResponse.ts | 12 +++++++++++ .../__generated__/models/UserNoteSchema.ts | 5 +---- frontend/src/stores/config.ts | 1 + .../src/views/Player/EmulatorJS/Player.vue | 20 +++++++++---------- 15 files changed, 88 insertions(+), 20 deletions(-) create mode 100644 frontend/src/__generated__/models/NetplayICEServer.ts create mode 100644 frontend/src/__generated__/models/RoomsResponse.ts diff --git a/backend/config/config_manager.py b/backend/config/config_manager.py index 829d3f920..2fbbf9681 100644 --- a/backend/config/config_manager.py +++ b/backend/config/config_manager.py @@ -61,6 +61,12 @@ class EjsControls(TypedDict): EjsOption = dict[str, str] # option_name -> option_value +class NetplayICEServer(TypedDict): + urls: str + username: NotRequired[str] + credential: NotRequired[str] + + class Config: CONFIG_FILE_MOUNTED: bool CONFIG_FILE_WRITABLE: bool @@ -78,6 +84,7 @@ class Config: HIGH_PRIO_STRUCTURE_PATH: str EJS_DEBUG: bool EJS_CACHE_LIMIT: int | None + EJS_NETPLAY_ICE_SERVERS: list[NetplayICEServer] EJS_SETTINGS: dict[str, EjsOption] # core_name -> EjsOption EJS_CONTROLS: dict[str, EjsControls] # core_name -> EjsControls SCAN_METADATA_PRIORITY: list[str] @@ -220,6 +227,9 @@ class ConfigManager: EJS_CACHE_LIMIT=pydash.get( self._raw_config, "emulatorjs.cache_limit", None ), + EJS_NETPLAY_ICE_SERVERS=pydash.get( + self._raw_config, "emulatorjs.netplay.ice_servers", [] + ), EJS_SETTINGS=pydash.get(self._raw_config, "emulatorjs.settings", {}), EJS_CONTROLS=self._get_ejs_controls(), SCAN_METADATA_PRIORITY=pydash.get( @@ -399,6 +409,12 @@ class ConfigManager: ) sys.exit(3) + if not isinstance(self.config.EJS_NETPLAY_ICE_SERVERS, list): + log.critical( + "Invalid config.yml: emulatorjs.netplay.ice_servers must be a list" + ) + sys.exit(3) + if not isinstance(self.config.EJS_SETTINGS, dict): log.critical("Invalid config.yml: emulatorjs.settings must be a dictionary") sys.exit(3) @@ -507,6 +523,9 @@ class ConfigManager: "emulatorjs": { "debug": self.config.EJS_DEBUG, "cache_limit": self.config.EJS_CACHE_LIMIT, + "netplay": { + "ice_servers": self.config.EJS_NETPLAY_ICE_SERVERS, + }, "settings": self.config.EJS_SETTINGS, "controls": self._format_ejs_controls_for_yaml(), }, diff --git a/backend/endpoints/configs.py b/backend/endpoints/configs.py index c223f637f..bce908d4c 100644 --- a/backend/endpoints/configs.py +++ b/backend/endpoints/configs.py @@ -37,6 +37,7 @@ def get_config() -> ConfigResponse: SKIP_HASH_CALCULATION=cfg.SKIP_HASH_CALCULATION, EJS_DEBUG=cfg.EJS_DEBUG, EJS_CACHE_LIMIT=cfg.EJS_CACHE_LIMIT, + EJS_NETPLAY_ICE_SERVERS=cfg.EJS_NETPLAY_ICE_SERVERS, EJS_CONTROLS=cfg.EJS_CONTROLS, EJS_SETTINGS=cfg.EJS_SETTINGS, SCAN_METADATA_PRIORITY=cfg.SCAN_METADATA_PRIORITY, diff --git a/backend/endpoints/responses/config.py b/backend/endpoints/responses/config.py index 30bf94290..3779a2546 100644 --- a/backend/endpoints/responses/config.py +++ b/backend/endpoints/responses/config.py @@ -1,6 +1,6 @@ from typing import TypedDict -from config.config_manager import EjsControls +from config.config_manager import EjsControls, NetplayICEServer class ConfigResponse(TypedDict): @@ -17,6 +17,7 @@ class ConfigResponse(TypedDict): SKIP_HASH_CALCULATION: bool EJS_DEBUG: bool EJS_CACHE_LIMIT: int | None + EJS_NETPLAY_ICE_SERVERS: list[NetplayICEServer] EJS_SETTINGS: dict[str, dict[str, str]] EJS_CONTROLS: dict[str, EjsControls] SCAN_METADATA_PRIORITY: list[str] diff --git a/backend/tests/config/fixtures/config/config.yml b/backend/tests/config/fixtures/config/config.yml index d8e949556..415c7fba4 100644 --- a/backend/tests/config/fixtures/config/config.yml +++ b/backend/tests/config/fixtures/config/config.yml @@ -55,6 +55,12 @@ scan: emulatorjs: debug: true cache_limit: 1000 + netplay: + ice_servers: + - urls: "stun:stun.relay.metered.ca:80" + - urls: "turn:global.relay.metered.ca:80" + username: "user" + credential: "password" settings: parallel_n64: vsync: disabled diff --git a/backend/tests/config/test_config_loader.py b/backend/tests/config/test_config_loader.py index 5767c3b1e..6ca9e0a12 100644 --- a/backend/tests/config/test_config_loader.py +++ b/backend/tests/config/test_config_loader.py @@ -22,6 +22,14 @@ def test_config_loader(): assert loader.config.SKIP_HASH_CALCULATION assert loader.config.EJS_DEBUG assert loader.config.EJS_CACHE_LIMIT == 1000 + assert loader.config.EJS_NETPLAY_ICE_SERVERS == [ + {"urls": "stun:stun.relay.metered.ca:80"}, + { + "urls": "turn:global.relay.metered.ca:80", + "username": "user", + "credential": "password", + }, + ] assert loader.config.EJS_SETTINGS == { "parallel_n64": {"vsync": "disabled"}, "snes9x": {"snes9x_region": "ntsc"}, @@ -60,5 +68,6 @@ def test_empty_config_loader(): assert not loader.config.SKIP_HASH_CALCULATION assert not loader.config.EJS_DEBUG assert loader.config.EJS_CACHE_LIMIT is None + assert loader.config.EJS_NETPLAY_ICE_SERVERS == [] assert loader.config.EJS_SETTINGS == {} assert loader.config.EJS_CONTROLS == {} diff --git a/examples/config.example.yml b/examples/config.example.yml index 61a0e4c2c..c8816f920 100644 --- a/examples/config.example.yml +++ b/examples/config.example.yml @@ -115,6 +115,12 @@ filesystem: {} # { roms_folder: 'roms' } For example if your folder structure is # snes9x_region: ntsc # default: # These settings apply to all cores # fps: show +# netplay: +# ice_servers: +# - urls: "stun:stun.relay.metered.ca:80" +# - urls: "turn:global.relay.metered.ca:80" +# username: "" +# credential: "" # controls: # https://emulatorjs.org/docs4devs/control-mapping/ # snes9x: # 0: # Player 1 diff --git a/frontend/src/__generated__/index.ts b/frontend/src/__generated__/index.ts index fd4fd806d..55c0fa3d1 100644 --- a/frontend/src/__generated__/index.ts +++ b/frontend/src/__generated__/index.ts @@ -51,6 +51,7 @@ export type { JobStatus } from './models/JobStatus'; export type { LaunchboxImage } from './models/LaunchboxImage'; export type { MetadataSourcesDict } from './models/MetadataSourcesDict'; export type { MobyMetadataPlatform } from './models/MobyMetadataPlatform'; +export type { NetplayICEServer } from './models/NetplayICEServer'; export type { OIDCDict } from './models/OIDCDict'; export type { PlatformSchema } from './models/PlatformSchema'; export type { RAGameRomAchievement } from './models/RAGameRomAchievement'; @@ -71,6 +72,7 @@ export type { RomRAMetadata } from './models/RomRAMetadata'; export type { RomSSMetadata } from './models/RomSSMetadata'; export type { RomUserSchema } from './models/RomUserSchema'; export type { RomUserStatus } from './models/RomUserStatus'; +export type { RoomsResponse } from './models/RoomsResponse'; export type { SaveSchema } from './models/SaveSchema'; export type { ScanStats } from './models/ScanStats'; export type { ScanTaskMeta } from './models/ScanTaskMeta'; diff --git a/frontend/src/__generated__/models/ConfigResponse.ts b/frontend/src/__generated__/models/ConfigResponse.ts index 0ee03b7c6..55b7d480d 100644 --- a/frontend/src/__generated__/models/ConfigResponse.ts +++ b/frontend/src/__generated__/models/ConfigResponse.ts @@ -3,6 +3,7 @@ /* tslint:disable */ /* eslint-disable */ import type { EjsControls } from './EjsControls'; +import type { NetplayICEServer } from './NetplayICEServer'; export type ConfigResponse = { CONFIG_FILE_MOUNTED: boolean; CONFIG_FILE_WRITABLE: boolean; @@ -17,6 +18,7 @@ export type ConfigResponse = { SKIP_HASH_CALCULATION: boolean; EJS_DEBUG: boolean; EJS_CACHE_LIMIT: (number | null); + EJS_NETPLAY_ICE_SERVERS: Array; EJS_SETTINGS: Record>; EJS_CONTROLS: Record; SCAN_METADATA_PRIORITY: Array; diff --git a/frontend/src/__generated__/models/NetplayICEServer.ts b/frontend/src/__generated__/models/NetplayICEServer.ts new file mode 100644 index 000000000..3e02f538e --- /dev/null +++ b/frontend/src/__generated__/models/NetplayICEServer.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type NetplayICEServer = { + urls: string; + username?: string; + credential?: string; +}; + diff --git a/frontend/src/__generated__/models/RomGamelistMetadata.ts b/frontend/src/__generated__/models/RomGamelistMetadata.ts index e7702cdbd..7000eb61f 100644 --- a/frontend/src/__generated__/models/RomGamelistMetadata.ts +++ b/frontend/src/__generated__/models/RomGamelistMetadata.ts @@ -16,11 +16,6 @@ export type RomGamelistMetadata = { thumbnail_url?: (string | null); title_screen_url?: (string | null); video_url?: (string | null); - box3d_path?: (string | null); - miximage_path?: (string | null); - physical_path?: (string | null); - marquee_path?: (string | null); - video_path?: (string | null); rating?: (number | null); first_release_date?: (string | null); companies?: (Array | null); @@ -28,5 +23,10 @@ export type RomGamelistMetadata = { genres?: (Array | null); player_count?: (string | null); md5_hash?: (string | null); + box3d_path?: (string | null); + miximage_path?: (string | null); + physical_path?: (string | null); + marquee_path?: (string | null); + video_path?: (string | null); }; diff --git a/frontend/src/__generated__/models/RomSSMetadata.ts b/frontend/src/__generated__/models/RomSSMetadata.ts index c966dd5e0..2bbb83469 100644 --- a/frontend/src/__generated__/models/RomSSMetadata.ts +++ b/frontend/src/__generated__/models/RomSSMetadata.ts @@ -21,7 +21,9 @@ export type RomSSMetadata = { video_url?: (string | null); video_normalized_url?: (string | null); bezel_path?: (string | null); + box2d_back_path?: (string | null); box3d_path?: (string | null); + fanart_path?: (string | null); miximage_path?: (string | null); physical_path?: (string | null); marquee_path?: (string | null); diff --git a/frontend/src/__generated__/models/RoomsResponse.ts b/frontend/src/__generated__/models/RoomsResponse.ts new file mode 100644 index 000000000..6c35ab671 --- /dev/null +++ b/frontend/src/__generated__/models/RoomsResponse.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type RoomsResponse = { + room_name: string; + current: number; + max: number; + player_name: string; + hasPassword: boolean; +}; + diff --git a/frontend/src/__generated__/models/UserNoteSchema.ts b/frontend/src/__generated__/models/UserNoteSchema.ts index adf1f5810..b4ef9a531 100644 --- a/frontend/src/__generated__/models/UserNoteSchema.ts +++ b/frontend/src/__generated__/models/UserNoteSchema.ts @@ -7,10 +7,7 @@ export type UserNoteSchema = { title: string; content: string; is_public: boolean; - tags?: Array | null; - metadata?: Record | null; - shared_with_users?: Array | null; - collaboration_level?: string | null; + tags?: (Array | null); created_at: string; updated_at: string; user_id: number; diff --git a/frontend/src/stores/config.ts b/frontend/src/stores/config.ts index ba2135395..632b7302f 100644 --- a/frontend/src/stores/config.ts +++ b/frontend/src/stores/config.ts @@ -25,6 +25,7 @@ const defaultConfig = { SKIP_HASH_CALCULATION: false, EJS_DEBUG: false, EJS_CACHE_LIMIT: null, + EJS_NETPLAY_ICE_SERVERS: [], EJS_SETTINGS: {}, EJS_CONTROLS: {}, SCAN_METADATA_PRIORITY: [], diff --git a/frontend/src/views/Player/EmulatorJS/Player.vue b/frontend/src/views/Player/EmulatorJS/Player.vue index afb12253f..f2f0b0179 100644 --- a/frontend/src/views/Player/EmulatorJS/Player.vue +++ b/frontend/src/views/Player/EmulatorJS/Player.vue @@ -4,7 +4,12 @@ import { storeToRefs } from "pinia"; import { inject, onBeforeUnmount, onMounted, onUnmounted, ref } from "vue"; import { useRouter } from "vue-router"; import { useTheme } from "vuetify"; -import type { FirmwareSchema, SaveSchema, StateSchema } from "@/__generated__"; +import type { + FirmwareSchema, + SaveSchema, + StateSchema, + NetplayICEServer, +} from "@/__generated__"; import { ROUTES } from "@/plugins/router"; import { saveApi as api } from "@/services/api/save"; import storeConfig from "@/stores/config"; @@ -53,12 +58,6 @@ const { selectedLanguage } = storeToRefs(languageStore); // Declare global variables for EmulatorJS declare global { - interface NetplayICEServer { - urls: string; - username?: string; - credential?: string; - } - interface Window { EJS_core: string; EJS_biosUrl: string; @@ -145,10 +144,11 @@ window.EJS_gameName = romRef.value.fs_name_no_tags window.EJS_language = selectedLanguage.value.value.replace("_", "-"); window.EJS_disableAutoLang = true; -window.EJS_netplayServer = window.location.host; -window.EJS_netplayICEServers = []; +const { EJS_DEBUG, EJS_CACHE_LIMIT, EJS_NETPLAY_ICE_SERVERS } = + configStore.config; -const { EJS_DEBUG, EJS_CACHE_LIMIT } = configStore.config; +window.EJS_netplayServer = window.location.host; +window.EJS_netplayICEServers = EJS_NETPLAY_ICE_SERVERS; if (EJS_CACHE_LIMIT !== null) window.EJS_CacheLimit = EJS_CACHE_LIMIT; window.EJS_DEBUG_XX = EJS_DEBUG;