// The plugin's configuration model (design §9) — the single source of truth for the sync engine, // editable by hand (headless boxes) or by the web UI. Everything the engine needs is derivable from // this file plus a directory scan; the `resolveConfig` helper fills defaults so the rest of the code // works against a fully-populated shape. import type { EmulatorDef } from "./emulators.js"; import type { Platform } from "./platforms.js"; /** A ROM root: a directory paired with the platform its files belong to (design §5). */ export interface RomRoot { /** Absolute directory to scan. */ dir: string; /** Platform id (a key into the resolved platform registry). */ platform: string; /** Per-root glob ignore patterns (`*.sav`, `bios/**`). */ excludes?: string[]; } /** A per-platform launch override — replaces the platform's built-in default emulator/core. */ export interface PlatformLaunch { emulator: string; core?: string; extraArgs?: string; } /** A per-game override, keyed by `external_id` (`snes/Chrono Trigger (USA).sfc`). */ export interface GameOverride { /** Skip this ROM entirely. */ exclude?: boolean; /** Override the resolved emulator/core/args for just this title. */ emulator?: string; core?: string; extraArgs?: string; /** Override the displayed title. */ title?: string; /** Override the box-art portrait URL. */ art?: string; } export interface SyncOptions { /** Interval poll in minutes (watchers are unreliable on SMB/NFS — poll is the primary trigger). */ pollMinutes: number; /** Enable fs watching (an accelerator on top of the poll). */ watch: boolean; /** Debounce window for coalescing watch/poll-triggered rescans (ms). */ debounceMs: number; /** Platform ids to region-dedupe ("one entry per title", design §5) — default none. */ dedupeRegions: string[]; /** Region preference order for dedupe (best first). */ regionPriority: string[]; /** Warn in the UI above this entry count (design §8 scale guard). */ warnEntries: number; /** Hard cap — truncate with a loud report above this many entries. */ maxEntries: number; /** Attach `prep`/`undo` that kills the emulator at session end ("close emulator when stream ends"). */ closeOnEnd: boolean; } /** Which art source to use (design §7, revised): SteamGridDB (like Steam ROM Manager) or the keyless * libretro-thumbnails fallback. `auto` = SteamGridDB when a key is set, else libretro. */ export type ArtProviderChoice = "auto" | "steamgriddb" | "libretro"; export interface ArtOptions { /** Fetch box art at all. */ enabled: boolean; /** Art source selection (default `auto`). */ provider: ArtProviderChoice; /** * SteamGridDB API key (free, from steamgriddb.com profile preferences). Unlocks full portrait + * hero + logo + header art with fuzzy title matching. Absent → the keyless libretro fallback. */ steamGridDbKey?: string; } export interface UiOptions { /** * Fallback standalone mode (design §9): serve the UI on a fixed, possibly non-loopback port behind * a password instead of via the console proxy. For host-only installs without the console. */ standalone: boolean; /** Standalone bind port. */ port: number; /** Standalone bind address (defaults to loopback; a non-loopback bind REQUIRES a password). */ bind: string; /** scrypt password hash (`scrypt$$`), required for a non-loopback standalone bind. */ passwordHash?: string; } /** The on-disk config as authored (all optional — `resolveConfig` fills the rest). */ export interface RawConfig { roots?: RomRoot[]; /** Per-platform launch overrides, keyed by platform id. */ platformLaunch?: Record; /** Per-game overrides, keyed by `external_id`. */ gameOverrides?: Record; /** Operator-added / overriding platform defs (merged over the built-ins by id). */ platforms?: Platform[]; /** Operator-added / overriding emulator defs (merged over the built-ins by id). */ emulators?: EmulatorDef[]; sync?: Partial; art?: Partial; ui?: Partial; /** Dev/M0 acceptance flag: reconcile one hardcoded entry so the round-trip is observable. */ devEntry?: boolean; } /** The fully-resolved config the engine consumes. */ export interface Config { roots: RomRoot[]; platformLaunch: Record; gameOverrides: Record; platforms?: Platform[]; emulators?: EmulatorDef[]; sync: SyncOptions; art: ArtOptions; ui: UiOptions; devEntry: boolean; } export const DEFAULT_SYNC: SyncOptions = { pollMinutes: 15, watch: true, debounceMs: 2000, dedupeRegions: [], regionPriority: ["USA", "World", "Europe", "Japan"], warnEntries: 2000, maxEntries: 5000, closeOnEnd: false, }; export const DEFAULT_UI: UiOptions = { standalone: false, port: 47993, bind: "127.0.0.1", }; /** Fill defaults over an authored config so the rest of the engine sees a total shape. */ export const resolveConfig = (raw: RawConfig): Config => ({ roots: raw.roots ?? [], platformLaunch: raw.platformLaunch ?? {}, gameOverrides: raw.gameOverrides ?? {}, platforms: raw.platforms, emulators: raw.emulators, sync: { ...DEFAULT_SYNC, ...raw.sync }, art: { enabled: true, provider: "auto", ...raw.art }, ui: { ...DEFAULT_UI, ...raw.ui }, devEntry: raw.devEntry ?? false, }); /** The empty starting config for a fresh install (no roots yet). */ export const emptyConfig = (): Config => resolveConfig({});