diff --git a/server/src/gamedata/parse.ts b/server/src/gamedata/parse.ts index ccb86d7..1cda9d6 100644 --- a/server/src/gamedata/parse.ts +++ b/server/src/gamedata/parse.ts @@ -8,6 +8,14 @@ import { gamedataFilePath } from './download.js'; * attributes appear as "@name" keys, and any child element may be an object or an array). */ +/** + * Bump whenever the parsing logic changes in a way that alters stored items/recipes. + * The server re-parses on boot when the DB's stored version differs, so a deploy that + * fixes parsing auto-heals existing data without a manual "re-download". + * v2: enchanted resource/ingredient ids carry @ (e.g. T6_SHOES_PLATE_SET1@4) + */ +export const PARSER_VERSION = 2; + type Raw = Record; function asArray(v: T | T[] | undefined | null): T[] { @@ -246,6 +254,7 @@ export async function parseGameData(): Promise { }; await setMeta('gamedata_parsed_at', new Date().toISOString()); await setMeta('gamedata_stats', JSON.stringify(stats)); + await setMeta('gamedata_parser_version', String(PARSER_VERSION)); cacheClear(); // items/recipes changed → drop every cached read return stats; } diff --git a/server/src/index.ts b/server/src/index.ts index 3e9e89c..14fefd0 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -26,22 +26,31 @@ const app = Fastify({ logger: { level: 'info' } }); await initDb(); // create schema on startup -// First-boot convenience: if no game data is loaded yet, download + parse it in -// the background so a fresh deployment is usable without a manual step. +// On boot: (re)parse game data if the DB is empty OR was parsed by an older parser +// version — so a deploy that fixes parsing auto-heals existing data. Runs in the +// background so it doesn't block startup. void (async () => { try { - const { get } = await import('./db/index.js'); - const row = await get<{ n: number }>('SELECT COUNT(*) AS n FROM items'); - if ((row?.n ?? 0) === 0) { - app.log.info('No game data found — downloading and parsing (first boot)...'); - const { downloadGameData } = await import('./gamedata/download.js'); - const { parseGameData } = await import('./gamedata/parse.js'); - await downloadGameData(false); - const stats = await parseGameData(); - app.log.info(`Game data loaded: ${stats.items} items, ${stats.recipes} recipes.`); - } + const { get, getMeta } = await import('./db/index.js'); + const { PARSER_VERSION, parseGameData } = await import('./gamedata/parse.js'); + const { downloadGameData } = await import('./gamedata/download.js'); + + const count = (await get<{ n: number }>('SELECT COUNT(*) AS n FROM items'))?.n ?? 0; + const storedVersion = Number((await getMeta('gamedata_parser_version')) ?? 0); + const empty = count === 0; + const stale = storedVersion !== PARSER_VERSION; + if (!empty && !stale) return; + + app.log.info( + empty + ? 'No game data found — downloading and parsing (first boot)...' + : `Game data parsed by an older version (v${storedVersion} → v${PARSER_VERSION}) — re-parsing...`, + ); + await downloadGameData(false); // uses cached files if already present + const stats = await parseGameData(); + app.log.info(`Game data loaded: ${stats.items} items, ${stats.recipes} recipes.`); } catch (e) { - app.log.error(`First-boot game data load failed: ${e instanceof Error ? e.message : e}`); + app.log.error(`Startup game data load failed: ${e instanceof Error ? e.message : e}`); } })();