diff --git a/src/dashboard/scoreko-dev/stores/game-assets.ts b/src/dashboard/scoreko-dev/stores/game-assets.ts index e801877..ef3968e 100644 --- a/src/dashboard/scoreko-dev/stores/game-assets.ts +++ b/src/dashboard/scoreko-dev/stores/game-assets.ts @@ -70,8 +70,13 @@ export const useGameAssetsStore = defineStore('game-assets', () => { }; const refreshInstalledGames = async () => { - const availableResponse = await sendNodecgMessage('scoreko-assets:listRemoteGames'); - availableGames.value = Array.isArray(availableResponse) ? availableResponse : []; + try { + const availableResponse = await sendNodecgMessage('scoreko-assets:listRemoteGames'); + availableGames.value = Array.isArray(availableResponse) ? availableResponse : []; + } catch { + availableGames.value = []; + } + const response = await sendNodecgMessage('scoreko-assets:listInstalled'); installedGames.value = Array.isArray(response) ? response : []; const configResponse = await sendNodecgMessage<{ assetsBaseUrl?: string }>('scoreko-assets:getAssetsBaseUrl'); diff --git a/src/extension/game-assets.ts b/src/extension/game-assets.ts index ced286a..950b1c9 100644 --- a/src/extension/game-assets.ts +++ b/src/extension/game-assets.ts @@ -1,4 +1,4 @@ -import { mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'; +import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { nodecg } from './util/nodecg.js'; @@ -35,7 +35,36 @@ type HttpGameTitlesFile = Record | HttpGameTitleEntry[]; const extensionDir = path.dirname(fileURLToPath(import.meta.url)); const bundleRoot = path.resolve(extensionDir, '..'); -const assetsRoot = path.join(bundleRoot, 'game-assets'); +const legacyAssetsRoot = path.join(bundleRoot, 'game-assets'); +const nodecgRoot = path.resolve(bundleRoot, '..', '..'); +const assetsRoot = path.join(nodecgRoot, 'db', `${nodecg.bundleName}-game-assets`); + +let assetsStorageReady = false; + +const ensureAssetsStorageReady = async () => { + if (assetsStorageReady) { + return; + } + + await mkdir(path.dirname(assetsRoot), { recursive: true }); + + const [currentStats, legacyStats] = await Promise.all([ + stat(assetsRoot).catch(() => null), + stat(legacyAssetsRoot).catch(() => null), + ]); + + if (!currentStats && legacyStats?.isDirectory()) { + await rename(legacyAssetsRoot, assetsRoot).catch(async () => { + await mkdir(assetsRoot, { recursive: true }); + }); + } else { + await mkdir(assetsRoot, { recursive: true }); + } + + assetsStorageReady = true; +}; + +void ensureAssetsStorageReady(); const assetsRouter = nodecg.Router(); @@ -240,10 +269,21 @@ const listHttpFiles = async (game: RemoteGame): Promise => { }; const listInstalledGames = async () => { + await ensureAssetsStorageReady(); const entries = await readdir(assetsRoot, { withFileTypes: true }).catch(() => []); return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right)); }; +const listInstalledGamesAsRemote = async (): Promise => { + const installedGames = await listInstalledGames(); + return installedGames.map((slug) => ({ + slug, + repoFolder: slug, + title: titleFromSlug(slug), + logoFile: `${slug}.png`, + })); +}; + const parseCharacterNames = (content: string, gameTitle: string) => { const parsed = JSON.parse(content) as unknown; const names = Array.isArray(parsed) @@ -277,6 +317,7 @@ const listInstalledCharacterNamesByGame = async () => { }; const downloadGameAssets = async (gameSlug: string) => { + await ensureAssetsStorageReady(); const customTitles = await fetchCustomGameTitles(); const game: RemoteGame = { slug: gameSlug, @@ -330,6 +371,7 @@ const downloadGameAssets = async (gameSlug: string) => { }; const removeGameAssets = async (gameSlug: string) => { + await ensureAssetsStorageReady(); const customTitles = await fetchCustomGameTitles(); const destinationFolder = path.join(assetsRoot, gameSlug); await rm(destinationFolder, { recursive: true, force: true }); @@ -346,8 +388,19 @@ nodecg.listenFor('scoreko-assets:listRemoteGames', async (_payload: unknown, ack } try { - ack(null, await listRemoteGames()); + const remoteGames = await listRemoteGames(); + ack(null, remoteGames); } catch (error) { + try { + const installedGames = await listInstalledGamesAsRemote(); + if (installedGames.length > 0) { + ack(null, installedGames); + return; + } + } catch { + // noop + } + ack((error as Error).message); } });