diff --git a/README.md b/README.md index 7c5f029..18ec075 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ La descarga de assets usa **únicamente HTTP**. Debes configurar un servidor pro ```text games/ + games.json (opcional, para nombres visibles personalizados) street-fighter-6/ street-fighter-6.png manifest.json @@ -59,6 +60,26 @@ games/ ... ``` +`games/games.json` es opcional y permite mapear `slug -> nombre visible`. + +Formato objeto: + +```json +{ + "2xko": "2XKO", + "tekken-8": "Tekken 8" +} +``` + +También se acepta array de objetos: + +```json +[ + { "slug": "2xko", "title": "2XKO" }, + { "slug": "tekken-8", "title": "Tekken 8" } +] +``` + ## Logos en servidor HTTP (sin logos locales en el bundle) La vista de "Game Assets" carga los logos directamente desde: diff --git a/src/dashboard/scoreko-dev/components/ScoreboardPanel.vue b/src/dashboard/scoreko-dev/components/ScoreboardPanel.vue index b3cfd0e..390fe55 100644 --- a/src/dashboard/scoreko-dev/components/ScoreboardPanel.vue +++ b/src/dashboard/scoreko-dev/components/ScoreboardPanel.vue @@ -33,8 +33,12 @@ const rightCharacterInput = ref(''); const gameInput = ref(''); const charactersByGame = ref>({}); +const gameTitleBySlug = computed(() => new Map( + gameAssetsStore.availableGames.map((game) => [game.slug, game.title] as const), +)); + const allFightingGameOptions = computed(() => gameAssetsStore.installedGames.map((game) => ({ - label: game, + label: gameTitleBySlug.value.get(game) ?? game, value: game, }))); diff --git a/src/extension/game-assets.ts b/src/extension/game-assets.ts index 41398ff..ced286a 100644 --- a/src/extension/game-assets.ts +++ b/src/extension/game-assets.ts @@ -5,6 +5,7 @@ import { nodecg } from './util/nodecg.js'; const CHARACTER_NAMES_FILE = 'fighting-characters.json'; const LOCAL_MANIFEST_FILE = 'manifest.json'; +const GAME_TITLES_FILE = 'games.json'; type RemoteGame = { title: string; @@ -25,6 +26,13 @@ type HttpManifestEntry = string | { url?: unknown; }; +type HttpGameTitleEntry = { + slug?: unknown; + title?: unknown; +}; + +type HttpGameTitlesFile = Record | HttpGameTitleEntry[]; + const extensionDir = path.dirname(fileURLToPath(import.meta.url)); const bundleRoot = path.resolve(extensionDir, '..'); const assetsRoot = path.join(bundleRoot, 'game-assets'); @@ -124,9 +132,61 @@ const titleFromSlug = (slug: string) => slug .map((segment) => segment[0].toUpperCase() + segment.slice(1)) .join(' '); +const parseGameTitlesMap = (payload: unknown): Map => { + const map = new Map(); + + if (Array.isArray(payload)) { + for (const entry of payload) { + const parsedEntry = entry as HttpGameTitleEntry; + if ( + typeof entry === 'object' + && entry !== null + && typeof parsedEntry.slug === 'string' + && typeof parsedEntry.title === 'string' + ) { + const slug = parsedEntry.slug.trim(); + const title = parsedEntry.title.trim(); + if (slug && title) { + map.set(slug, title); + } + } + } + return map; + } + + if (typeof payload === 'object' && payload !== null) { + for (const [slug, value] of Object.entries(payload)) { + if (typeof value !== 'string') { + continue; + } + + const normalizedSlug = slug.trim(); + const title = value.trim(); + if (normalizedSlug && title) { + map.set(normalizedSlug, title); + } + } + } + + return map; +}; + +const fetchCustomGameTitles = async (): Promise> => { + const baseUrl = getConfiguredAssetsBaseUrl(); + const url = `${baseUrl}/games/${GAME_TITLES_FILE}`; + + try { + const payload = await fetchJson(url); + return parseGameTitlesMap(payload); + } catch { + return new Map(); + } +}; + const listRemoteGames = async (): Promise => { const baseUrl = getConfiguredAssetsBaseUrl(); const gamesIndexUrl = `${baseUrl}/games/`; + const customTitles = await fetchCustomGameTitles(); const response = await fetch(gamesIndexUrl, { headers: requestHeaders }); if (!response.ok) { throw new Error(`Error HTTP (${response.status}) al solicitar ${gamesIndexUrl}`); @@ -153,7 +213,7 @@ const listRemoteGames = async (): Promise => { return uniqueSlugs.map((slug) => ({ slug, repoFolder: slug, - title: titleFromSlug(slug), + title: customTitles.get(slug) ?? titleFromSlug(slug), logoFile: `${slug}.png`, })); }; @@ -217,10 +277,11 @@ const listInstalledCharacterNamesByGame = async () => { }; const downloadGameAssets = async (gameSlug: string) => { + const customTitles = await fetchCustomGameTitles(); const game: RemoteGame = { slug: gameSlug, repoFolder: gameSlug, - title: titleFromSlug(gameSlug), + title: customTitles.get(gameSlug) ?? titleFromSlug(gameSlug), logoFile: `${gameSlug}.png`, }; @@ -269,11 +330,12 @@ const downloadGameAssets = async (gameSlug: string) => { }; const removeGameAssets = async (gameSlug: string) => { + const customTitles = await fetchCustomGameTitles(); const destinationFolder = path.join(assetsRoot, gameSlug); await rm(destinationFolder, { recursive: true, force: true }); return { - title: titleFromSlug(gameSlug), + title: customTitles.get(gameSlug) ?? titleFromSlug(gameSlug), slug: gameSlug, }; };