Merge pull request #152 from Pandipipas/add-slug-formatting-for-game-names

Soportar nombres visibles de juegos mediante `games/games.json`
This commit is contained in:
Pandipipas
2026-03-03 23:04:16 +01:00
committed by GitHub
3 changed files with 91 additions and 4 deletions
+21
View File
@@ -48,6 +48,7 @@ La descarga de assets usa **únicamente HTTP**. Debes configurar un servidor pro
```text ```text
games/ games/
games.json (opcional, para nombres visibles personalizados)
street-fighter-6/ street-fighter-6/
street-fighter-6.png street-fighter-6.png
manifest.json 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) ## Logos en servidor HTTP (sin logos locales en el bundle)
La vista de "Game Assets" carga los logos directamente desde: La vista de "Game Assets" carga los logos directamente desde:
@@ -33,8 +33,12 @@ const rightCharacterInput = ref('');
const gameInput = ref(''); const gameInput = ref('');
const charactersByGame = ref<Record<string, { leftCharacter: string; rightCharacter: string }>>({}); const charactersByGame = ref<Record<string, { leftCharacter: string; rightCharacter: string }>>({});
const gameTitleBySlug = computed(() => new Map(
gameAssetsStore.availableGames.map((game) => [game.slug, game.title] as const),
));
const allFightingGameOptions = computed(() => gameAssetsStore.installedGames.map((game) => ({ const allFightingGameOptions = computed(() => gameAssetsStore.installedGames.map((game) => ({
label: game, label: gameTitleBySlug.value.get(game) ?? game,
value: game, value: game,
}))); })));
+65 -3
View File
@@ -5,6 +5,7 @@ import { nodecg } from './util/nodecg.js';
const CHARACTER_NAMES_FILE = 'fighting-characters.json'; const CHARACTER_NAMES_FILE = 'fighting-characters.json';
const LOCAL_MANIFEST_FILE = 'manifest.json'; const LOCAL_MANIFEST_FILE = 'manifest.json';
const GAME_TITLES_FILE = 'games.json';
type RemoteGame = { type RemoteGame = {
title: string; title: string;
@@ -25,6 +26,13 @@ type HttpManifestEntry = string | {
url?: unknown; url?: unknown;
}; };
type HttpGameTitleEntry = {
slug?: unknown;
title?: unknown;
};
type HttpGameTitlesFile = Record<string, unknown> | HttpGameTitleEntry[];
const extensionDir = path.dirname(fileURLToPath(import.meta.url)); const extensionDir = path.dirname(fileURLToPath(import.meta.url));
const bundleRoot = path.resolve(extensionDir, '..'); const bundleRoot = path.resolve(extensionDir, '..');
const assetsRoot = path.join(bundleRoot, 'game-assets'); const assetsRoot = path.join(bundleRoot, 'game-assets');
@@ -124,9 +132,61 @@ const titleFromSlug = (slug: string) => slug
.map((segment) => segment[0].toUpperCase() + segment.slice(1)) .map((segment) => segment[0].toUpperCase() + segment.slice(1))
.join(' '); .join(' ');
const parseGameTitlesMap = (payload: unknown): Map<string, string> => {
const map = new Map<string, string>();
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<Map<string, string>> => {
const baseUrl = getConfiguredAssetsBaseUrl();
const url = `${baseUrl}/games/${GAME_TITLES_FILE}`;
try {
const payload = await fetchJson<HttpGameTitlesFile>(url);
return parseGameTitlesMap(payload);
} catch {
return new Map<string, string>();
}
};
const listRemoteGames = async (): Promise<RemoteGame[]> => { const listRemoteGames = async (): Promise<RemoteGame[]> => {
const baseUrl = getConfiguredAssetsBaseUrl(); const baseUrl = getConfiguredAssetsBaseUrl();
const gamesIndexUrl = `${baseUrl}/games/`; const gamesIndexUrl = `${baseUrl}/games/`;
const customTitles = await fetchCustomGameTitles();
const response = await fetch(gamesIndexUrl, { headers: requestHeaders }); const response = await fetch(gamesIndexUrl, { headers: requestHeaders });
if (!response.ok) { if (!response.ok) {
throw new Error(`Error HTTP (${response.status}) al solicitar ${gamesIndexUrl}`); throw new Error(`Error HTTP (${response.status}) al solicitar ${gamesIndexUrl}`);
@@ -153,7 +213,7 @@ const listRemoteGames = async (): Promise<RemoteGame[]> => {
return uniqueSlugs.map((slug) => ({ return uniqueSlugs.map((slug) => ({
slug, slug,
repoFolder: slug, repoFolder: slug,
title: titleFromSlug(slug), title: customTitles.get(slug) ?? titleFromSlug(slug),
logoFile: `${slug}.png`, logoFile: `${slug}.png`,
})); }));
}; };
@@ -217,10 +277,11 @@ const listInstalledCharacterNamesByGame = async () => {
}; };
const downloadGameAssets = async (gameSlug: string) => { const downloadGameAssets = async (gameSlug: string) => {
const customTitles = await fetchCustomGameTitles();
const game: RemoteGame = { const game: RemoteGame = {
slug: gameSlug, slug: gameSlug,
repoFolder: gameSlug, repoFolder: gameSlug,
title: titleFromSlug(gameSlug), title: customTitles.get(gameSlug) ?? titleFromSlug(gameSlug),
logoFile: `${gameSlug}.png`, logoFile: `${gameSlug}.png`,
}; };
@@ -269,11 +330,12 @@ const downloadGameAssets = async (gameSlug: string) => {
}; };
const removeGameAssets = async (gameSlug: string) => { const removeGameAssets = async (gameSlug: string) => {
const customTitles = await fetchCustomGameTitles();
const destinationFolder = path.join(assetsRoot, gameSlug); const destinationFolder = path.join(assetsRoot, gameSlug);
await rm(destinationFolder, { recursive: true, force: true }); await rm(destinationFolder, { recursive: true, force: true });
return { return {
title: titleFromSlug(gameSlug), title: customTitles.get(gameSlug) ?? titleFromSlug(gameSlug),
slug: gameSlug, slug: gameSlug,
}; };
}; };