From 005f8d41ba2532ff9df0bc20c149cbeec98812f2 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Fri, 27 Feb 2026 18:50:05 +0100 Subject: [PATCH] test commit - remove later --- dev/package.json | 3 +- dev/scripts/fetch-cdragon.js | 34 ++++ dev/scripts/setup-db.js | 26 ++- frontend/components/ChampionSelector.vue | 4 +- frontend/components/build/Viewer.vue | 21 +- frontend/components/item/Tree.vue | 2 +- frontend/components/item/Viewer.vue | 7 +- frontend/components/rune/Page.vue | 8 +- frontend/components/rune/Selector.vue | 9 +- frontend/pages/tierlist/[lane].vue | 4 +- .../server/api/cdragon/champion-summary.ts | 14 ++ frontend/server/api/cdragon/items.ts | 14 ++ frontend/server/api/cdragon/perks.ts | 14 ++ frontend/server/api/cdragon/perkstyles.ts | 14 ++ .../server/api/cdragon/summoner-spells.ts | 14 ++ frontend/server/api/routemap.ts | 8 +- frontend/server/utils/cdragon-cache.ts | 185 ++++++++++++++++++ patch_detector/index.ts | 128 +++++++++++- 18 files changed, 452 insertions(+), 57 deletions(-) create mode 100644 dev/scripts/fetch-cdragon.js create mode 100644 frontend/server/api/cdragon/champion-summary.ts create mode 100644 frontend/server/api/cdragon/items.ts create mode 100644 frontend/server/api/cdragon/perks.ts create mode 100644 frontend/server/api/cdragon/perkstyles.ts create mode 100644 frontend/server/api/cdragon/summoner-spells.ts create mode 100644 frontend/server/utils/cdragon-cache.ts diff --git a/dev/package.json b/dev/package.json index d8b9c41..9fab5f1 100644 --- a/dev/package.json +++ b/dev/package.json @@ -9,7 +9,8 @@ "import-matches": "node scripts/setup-db.js import-matches", "import-patches": "node scripts/setup-db.js import-patches", "generate-stats": "node scripts/setup-db.js generate-stats", - "status": "node scripts/setup-db.js status" + "status": "node scripts/setup-db.js status", + "fetch-cdragon": "node scripts/fetch-cdragon.js" }, "dependencies": { "mongodb": "^6.10.0" diff --git a/dev/scripts/fetch-cdragon.js b/dev/scripts/fetch-cdragon.js new file mode 100644 index 0000000..703a180 --- /dev/null +++ b/dev/scripts/fetch-cdragon.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +import { spawn } from 'child_process'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Cache directory - use dev/cdragon by default +const cacheDir = process.env.CDRAGON_CACHE_DIR || path.join(__dirname, '..', 'data', 'cdragon'); + +// Dev MongoDB credentials (matching docker-compose.yml defaults) +const mongoUser = process.env.MONGO_USER || 'root'; +const mongoPass = process.env.MONGO_PASS || 'password'; +const mongoHost = process.env.MONGO_HOST || 'localhost:27017'; + +// Run patch_detector with the cache directory and dev MongoDB credentials +const patchDetector = spawn('npx', ['tsx', '../patch_detector/index.ts'], { + cwd: path.join(__dirname, '..'), + env: { + ...process.env, + NODE_ENV: 'development', + CDRAGON_CACHE_DIR: cacheDir, + MONGO_USER: mongoUser, + MONGO_PASS: mongoPass, + MONGO_HOST: mongoHost + }, + stdio: 'inherit' +}); + +patchDetector.on('close', (code) => { + process.exit(code || 0); +}); diff --git a/dev/scripts/setup-db.js b/dev/scripts/setup-db.js index b46eaf1..50747df 100644 --- a/dev/scripts/setup-db.js +++ b/dev/scripts/setup-db.js @@ -23,7 +23,7 @@ async function setupDatabase() { const patchFile = path.join(dataDir, "patches.json"); if(!fs.existsSync(dataDir) || !fs.existsSync(patchFile)) { fs.mkdirSync(dataDir, { recursive: true }); - console.log('📥 No data files found. Downloading latest snapshot...'); + console.log('🚫 No data files found. Downloading latest snapshot...'); await downloadAndExtractSnapshot(); } @@ -90,7 +90,11 @@ async function setupDatabase() { console.log('✅ Skipping matches import - sufficient data already present'); } - // 7. Run match collector to generate stats + // 7. Fetch CDragon data for the current patch + console.log('🎮 Fetching CDragon data...'); + await fetchCDragonData(); + + // 8. Run match collector to generate stats console.log('📊 Generating champion stats...'); await generateChampionStats(); @@ -322,6 +326,24 @@ async function generateChampionStats() { } } +async function fetchCDragonData() { + try { + console.log('🔄 Running CDragon fetcher...'); + + // Run the fetch-cdragon script + const fetchCDragonPath = path.join(__dirname, 'fetch-cdragon.js'); + execSync(`node ${fetchCDragonPath}`, { + stdio: 'inherit', + cwd: path.join(__dirname, '..') + }); + + console.log('✅ CDragon data fetched'); + } catch (error) { + console.error('❌ Failed to fetch CDragon data:', error); + throw error; + } +} + async function getMatchCount(patchVersion) { const client = new MongoClient(getMongoUri()); await client.connect(); diff --git a/frontend/components/ChampionSelector.vue b/frontend/components/ChampionSelector.vue index b0345a6..0ff464d 100644 --- a/frontend/components/ChampionSelector.vue +++ b/frontend/components/ChampionSelector.vue @@ -2,8 +2,6 @@ import { debounce, isEmpty } from '~/utils/helpers' // Constants -const CDRAGON_CHAMPIONS_URL = - CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json' const CHAMPIONS_API_URL = '/api/champions' // State @@ -11,7 +9,7 @@ const { data: championsData, pending: loadingChampions, error: championsError -} = useFetch(CDRAGON_CHAMPIONS_URL, { +} = useFetch('/api/cdragon/champion-summary', { key: 'champions-data', lazy: false, server: false // Disable server-side fetching to avoid hydration issues diff --git a/frontend/components/build/Viewer.vue b/frontend/components/build/Viewer.vue index e39a5a8..784f789 100644 --- a/frontend/components/build/Viewer.vue +++ b/frontend/components/build/Viewer.vue @@ -13,16 +13,11 @@ const props = defineProps<{ summonerSpells?: Array<{ id: number; count: number; pickrate: number }> // API data when available }>() -// Constants -const ITEMS_API_URL = CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/items.json' -const SUMMONER_SPELLS_URL = - CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/summoner-spells.json' - // State const currentlySelectedBuild = ref(0) -// Fetch items -const { data: items } = useFetch>(ITEMS_API_URL, { +// Fetch items from cached API +const { data: items } = useFetch>('/api/cdragon/items', { lazy: true, server: false }) @@ -44,8 +39,8 @@ watch( { immediate: true } ) -// Fetch summoner spells -const { data: summonerSpellsData } = useFetch>(SUMMONER_SPELLS_URL, { +// Fetch summoner spells from cached API +const { data: summonerSpellsData } = useFetch>('/api/cdragon/summoner-spells', { lazy: true, server: false }) @@ -143,17 +138,13 @@ const primaryStyles: Ref> = ref(Array(props.runes.length)) const secondaryStyles: Ref> = ref(Array(props.runes.length)) const keystoneIds: Ref> = ref(Array(props.runes.length)) -const { data: perks_data }: PerksResponse = await useFetch( - CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perks.json' -) +const { data: perks_data }: PerksResponse = await useFetch('/api/cdragon/perks') const perks = reactive(new Map()) for (const perk of perks_data.value) { perks.set(perk.id, perk) } -const { data: stylesData }: PerkStylesResponse = await useFetch( - CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json' -) +const { data: stylesData }: PerkStylesResponse = await useFetch('/api/cdragon/perkstyles') function refreshStylesKeystones() { for (const style of stylesData.value.styles) { diff --git a/frontend/components/item/Tree.vue b/frontend/components/item/Tree.vue index 973f40a..bd49a54 100644 --- a/frontend/components/item/Tree.vue +++ b/frontend/components/item/Tree.vue @@ -12,7 +12,7 @@ const emit = defineEmits<{ }>() const { data: items } = useFetch>( - CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/items.json', + '/api/cdragon/items', { lazy: true, // Don't block rendering server: false // Client-side only diff --git a/frontend/components/item/Viewer.vue b/frontend/components/item/Viewer.vue index f66f7db..1690d47 100644 --- a/frontend/components/item/Viewer.vue +++ b/frontend/components/item/Viewer.vue @@ -7,15 +7,12 @@ const props = defineProps<{ error?: boolean }>() -// Constants -const ITEMS_API_URL = CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/items.json' - -// State +// State - use cached API endpoint instead of direct CDragon fetch const { data: items, pending: loadingItems, error: itemsError -} = useFetch(ITEMS_API_URL, { +} = useFetch('/api/cdragon/items', { lazy: true, // Don't block rendering server: false // Client-side only }) diff --git a/frontend/components/rune/Page.vue b/frontend/components/rune/Page.vue index db7e57a..629f06a 100644 --- a/frontend/components/rune/Page.vue +++ b/frontend/components/rune/Page.vue @@ -8,17 +8,13 @@ const props = defineProps<{ const primaryStyle: Ref = ref({ id: 0, name: '', iconPath: '', slots: [] }) const secondaryStyle: Ref = ref({ id: 0, name: '', iconPath: '', slots: [] }) -const { data: perks_data }: PerksResponse = await useFetch( - CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perks.json' -) +const { data: perks_data }: PerksResponse = await useFetch('/api/cdragon/perks') const perks = reactive(new Map()) for (const perk of perks_data.value) { perks.set(perk.id, perk) } -const { data: stylesData }: PerkStylesResponse = await useFetch( - CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json' -) +const { data: stylesData }: PerkStylesResponse = await useFetch('/api/cdragon/perkstyles') watch( () => props.primaryStyleId, async (_newP, _oldP) => { diff --git a/frontend/components/rune/Selector.vue b/frontend/components/rune/Selector.vue index 0983564..3d00d07 100644 --- a/frontend/components/rune/Selector.vue +++ b/frontend/components/rune/Selector.vue @@ -14,17 +14,14 @@ const primaryStyles: Ref> = ref(Array(props.runes.length)) const secondaryStyles: Ref> = ref(Array(props.runes.length)) const keystoneIds: Ref> = ref(Array(props.runes.length)) -const { data: perks_data }: PerksResponse = await useFetch( - CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perks.json' -) +// Use cached API endpoints instead of direct CDragon fetch +const { data: perks_data }: PerksResponse = await useFetch('/api/cdragon/perks') const perks = reactive(new Map()) for (const perk of perks_data.value) { perks.set(perk.id, perk) } -const { data: stylesData }: PerkStylesResponse = await useFetch( - CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json' -) +const { data: stylesData }: PerkStylesResponse = await useFetch('/api/cdragon/perkstyles') watch( () => props.runes, (_newRunes, _oldRunes) => { diff --git a/frontend/pages/tierlist/[lane].vue b/frontend/pages/tierlist/[lane].vue index 95f1d3f..1eb12ce 100644 --- a/frontend/pages/tierlist/[lane].vue +++ b/frontend/pages/tierlist/[lane].vue @@ -4,9 +4,7 @@ import { LANE_IMAGES, lanePositionToIndex, POSITIONS_STR } from '~/utils/cdragon const route = useRoute() const lane = route.params.lane as string -const { data: championsData }: ChampionsResponse = await useFetch( - CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json' -) +const { data: championsData }: ChampionsResponse = await useFetch('/api/cdragon/champion-summary') const { data: championsLanes }: { data: Ref> } = await useFetch('/api/champions') diff --git a/frontend/server/api/cdragon/champion-summary.ts b/frontend/server/api/cdragon/champion-summary.ts new file mode 100644 index 0000000..6aef35f --- /dev/null +++ b/frontend/server/api/cdragon/champion-summary.ts @@ -0,0 +1,14 @@ +import { getChampionSummary } from '~/server/utils/cdragon-cache' + +export default defineEventHandler(async () => { + try { + const championSummary = await getChampionSummary() + return championSummary + } catch (error) { + console.error('Error fetching champion summary:', error) + throw createError({ + statusCode: 500, + statusMessage: 'Failed to fetch champion summary' + }) + } +}) \ No newline at end of file diff --git a/frontend/server/api/cdragon/items.ts b/frontend/server/api/cdragon/items.ts new file mode 100644 index 0000000..761d210 --- /dev/null +++ b/frontend/server/api/cdragon/items.ts @@ -0,0 +1,14 @@ +import { getItems } from '~/server/utils/cdragon-cache' + +export default defineEventHandler(async () => { + try { + const items = await getItems() + return items + } catch (error) { + console.error('Error fetching items:', error) + throw createError({ + statusCode: 500, + statusMessage: 'Failed to fetch items' + }) + } +}) \ No newline at end of file diff --git a/frontend/server/api/cdragon/perks.ts b/frontend/server/api/cdragon/perks.ts new file mode 100644 index 0000000..829399b --- /dev/null +++ b/frontend/server/api/cdragon/perks.ts @@ -0,0 +1,14 @@ +import { getPerks } from '~/server/utils/cdragon-cache' + +export default defineEventHandler(async () => { + try { + const perks = await getPerks() + return perks + } catch (error) { + console.error('Error fetching perks:', error) + throw createError({ + statusCode: 500, + statusMessage: 'Failed to fetch perks' + }) + } +}) \ No newline at end of file diff --git a/frontend/server/api/cdragon/perkstyles.ts b/frontend/server/api/cdragon/perkstyles.ts new file mode 100644 index 0000000..d715649 --- /dev/null +++ b/frontend/server/api/cdragon/perkstyles.ts @@ -0,0 +1,14 @@ +import { getPerkStyles } from '~/server/utils/cdragon-cache' + +export default defineEventHandler(async () => { + try { + const perkStyles = await getPerkStyles() + return perkStyles + } catch (error) { + console.error('Error fetching perk styles:', error) + throw createError({ + statusCode: 500, + statusMessage: 'Failed to fetch perk styles' + }) + } +}) \ No newline at end of file diff --git a/frontend/server/api/cdragon/summoner-spells.ts b/frontend/server/api/cdragon/summoner-spells.ts new file mode 100644 index 0000000..04eead9 --- /dev/null +++ b/frontend/server/api/cdragon/summoner-spells.ts @@ -0,0 +1,14 @@ +import { getSummonerSpells } from '~/server/utils/cdragon-cache' + +export default defineEventHandler(async () => { + try { + const summonerSpells = await getSummonerSpells() + return summonerSpells + } catch (error) { + console.error('Error fetching summoner spells:', error) + throw createError({ + statusCode: 500, + statusMessage: 'Failed to fetch summoner spells' + }) + } +}) \ No newline at end of file diff --git a/frontend/server/api/routemap.ts b/frontend/server/api/routemap.ts index 792834d..d75190c 100644 --- a/frontend/server/api/routemap.ts +++ b/frontend/server/api/routemap.ts @@ -1,11 +1,7 @@ -import { CDRAGON_BASE } from '~/utils/cdragon' +import { getChampionSummary } from '~/server/utils/cdragon-cache' async function championRoutes() { - const championsData: Array = await ( - await fetch( - CDRAGON_BASE + 'plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json' - ) - ).json() + const championsData = await getChampionSummary() const routes: Array = [] for (const champion of championsData) { diff --git a/frontend/server/utils/cdragon-cache.ts b/frontend/server/utils/cdragon-cache.ts new file mode 100644 index 0000000..8271604 --- /dev/null +++ b/frontend/server/utils/cdragon-cache.ts @@ -0,0 +1,185 @@ +import { readFile, existsSync } from 'fs' +import { join } from 'path' +import { promisify } from 'util' + +const readFileAsync = promisify(readFile) + +// CDragon base URL for fallback +const CDRAGON_BASE = 'https://raw.communitydragon.org/' + +// Cache directory - can be configured via environment variable +// Default to dev/cdragon for development +const getCacheDir = () => { + if (process.env.CDRAGON_CACHE_DIR) { + return process.env.CDRAGON_CACHE_DIR + } + // Default to dev/cdragon relative to project root + return join(process.cwd(), '..', 'dev', 'data', 'cdragon') +} + +/** + * Get the current patch from the patch.txt file or fallback to 'latest' + * Converts patch format for CDragon: "16.4.1" -> "16.4" + */ +async function getCurrentPatch(): Promise { + const cacheDir = getCacheDir() + const patchFile = join(cacheDir, 'latest', 'patch.txt') + + if (existsSync(patchFile)) { + const patch = await readFileAsync(patchFile, 'utf-8') + const trimmedPatch = patch.trim() + // Convert patch format for CDragon: "16.4.1" -> "16.4" + return trimmedPatch.split('.').slice(0, 2).join('.') + } + + // Fallback to 'latest' if no patch file exists + return 'latest' +} + +/** + * Cached CDragon data types + */ +interface CDragonCacheOptions { + patch?: string +} + +/** + * Fetch data from local cache with CDragon fallback + */ +async function fetchFromCache( + assetName: string, + cdragonPath: string, + options?: CDragonCacheOptions +): Promise { + const cacheDir = getCacheDir() + const patch = options?.patch || (await getCurrentPatch()) + const cachePath = join(cacheDir, patch, assetName) + + // Try to read from cache first + if (existsSync(cachePath)) { + try { + const data = await readFileAsync(cachePath, 'utf-8') + return JSON.parse(data) as T + } catch (error) { + console.error(`Error reading cache file ${cachePath}:`, error) + } + } + + // Fallback to CDragon + console.log(`Cache miss for ${assetName}, fetching from CDragon...`) + const url = `${CDRAGON_BASE}${patch}/${cdragonPath}` + const response = await fetch(url) + + if (!response.ok) { + throw new Error(`Failed to fetch ${assetName} from CDragon: ${response.status}`) + } + + return (await response.json()) as T +} + +/** + * Get items data from cache + */ +async function getItems(patch?: string): Promise { + return fetchFromCache('items.json', 'plugins/rcp-be-lol-game-data/global/default/v1/items.json', { patch }) +} + +/** + * Get perks (runes) data from cache + */ +async function getPerks(patch?: string): Promise { + return fetchFromCache('perks.json', 'plugins/rcp-be-lol-game-data/global/default/v1/perks.json', { patch }) +} + +/** + * Get perk styles data from cache + */ +async function getPerkStyles(patch?: string): Promise { + return fetchFromCache('perkstyles.json', 'plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json', { patch }) +} + +/** + * Get summoner spells data from cache + */ +async function getSummonerSpells(patch?: string): Promise { + return fetchFromCache('summoner-spells.json', 'plugins/rcp-be-lol-game-data/global/default/v1/summoner-spells.json', { patch }) +} + +/** + * Get champion summary data from cache + */ +async function getChampionSummary(patch?: string): Promise { + return fetchFromCache('champion-summary.json', 'plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json', { patch }) +} + +// Type definitions for CDragon data +interface CDragonItem { + id: number + name: string + iconPath: string + description?: string + plaintext?: string + into?: number[] + from?: number[] + gold?: { + base: number + total: number + sell: number + } +} + +interface CDragonPerk { + id: number + name: string + iconPath: string + shortDesc?: string + longDesc?: string +} + +interface CDragonPerkStyle { + id: number + name: string + iconPath: string + slots: Array<{ + type: string + perks: number[] + }> +} + +interface CDragonPerkStyles { + styles: CDragonPerkStyle[] +} + +interface CDragonSummonerSpell { + id: number + name: string + iconPath: string + description?: string +} + +interface CDragonChampionSummary { + id: number + name: string + alias: string + squarePortraitPath: string + roles?: string[] +} + +export { + getItems, + getPerks, + getPerkStyles, + getSummonerSpells, + getChampionSummary, + getCurrentPatch, + CDRAGON_BASE +} + +export type { + CDragonItem, + CDragonPerk, + CDragonPerkStyle, + CDragonPerkStyles, + CDragonSummonerSpell, + CDragonChampionSummary +} \ No newline at end of file diff --git a/patch_detector/index.ts b/patch_detector/index.ts index 1b34f5e..0ccf215 100644 --- a/patch_detector/index.ts +++ b/patch_detector/index.ts @@ -1,18 +1,66 @@ import { MongoClient } from 'mongodb' +import { writeFile, mkdir } from 'fs/promises' +import { existsSync } from 'fs' +import { join } from 'path' + +// CDragon base URL for specific patch +const CDRAGON_BASE = 'https://raw.communitydragon.org/' + +// Assets to cache for each patch +const CDRAGON_ASSETS = [ + { + name: 'items.json', + path: 'plugins/rcp-be-lol-game-data/global/default/v1/items.json' + }, + { + name: 'perks.json', + path: 'plugins/rcp-be-lol-game-data/global/default/v1/perks.json' + }, + { + name: 'perkstyles.json', + path: 'plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json' + }, + { + name: 'summoner-spells.json', + path: 'plugins/rcp-be-lol-game-data/global/default/v1/summoner-spells.json' + }, + { + name: 'champion-summary.json', + path: 'plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json' + } +] main() async function main() { - const client = await connectToDatabase() - const newPatch = await fetchLatestPatch() - - console.log('Latest patch is: ' + newPatch) - - const newDate = new Date() - if (!(await compareLatestSavedPatch(client, newPatch, newDate))) { - downloadAssets() + // In dev mode, get patch from database (the one we have match data for) + if (process.env.NODE_ENV === 'development') { + console.log('Development mode: downloading cache for database patch') + const client = await connectToDatabase() + const dbPatch = await getLatestPatchFromDatabase(client) + await client.close() + + if (dbPatch) { + console.log('Latest patch in database: ' + dbPatch) + await downloadAssets(dbPatch) + } else { + console.log('No patch found in database, fetching latest from game...') + const latestPatch = await fetchLatestPatch() + console.log('Latest game patch: ' + latestPatch) + await downloadAssets(latestPatch) + } + return } + // Production mode: check database and update if new patch + const newPatch = await fetchLatestPatch() + console.log('Latest patch is: ' + newPatch) + + const client = await connectToDatabase() + const newDate = new Date() + if (!(await compareLatestSavedPatch(client, newPatch, newDate))) { + await downloadAssets(newPatch) + } await client.close() } @@ -58,4 +106,66 @@ async function compareLatestSavedPatch(client: MongoClient, newPatch: string, ne return true } -async function downloadAssets() {} +async function getLatestPatchFromDatabase(client: MongoClient): Promise { + const database = client.db('patches') + const patches = database.collection('patches') + const latestPatch = await patches.find().limit(1).sort({ date: -1 }).next() + + if (latestPatch == null) { + return null + } + + return latestPatch.patch +} + +async function downloadAssets(patch: string) { + // Convert patch format for CDragon: "16.4.1" -> "16.4" + // CDragon uses patch format without the last minor version + const cdragonPatch = patch.split('.').slice(0, 2).join('.') + console.log(`Downloading CDragon assets for patch ${cdragonPatch} (from ${patch})...`) + + // Get cache directory from environment or use default + const cacheDir = process.env.CDRAGON_CACHE_DIR || '/cdragon' + const patchDir = join(cacheDir, cdragonPatch) + + // Create patch directory if it doesn't exist + if (!existsSync(patchDir)) { + await mkdir(patchDir, { recursive: true }) + console.log(`Created directory: ${patchDir}`) + } + + // Download each asset + for (const asset of CDRAGON_ASSETS) { + const url = `${CDRAGON_BASE}${cdragonPatch}/${asset.path}` + const filePath = join(patchDir, asset.name) + + try { + console.log(`Downloading ${asset.name}...`) + const response = await fetch(url) + + if (!response.ok) { + console.error(`Failed to download ${asset.name}: ${response.status} ${response.statusText}`) + continue + } + + const data = await response.json() + await writeFile(filePath, JSON.stringify(data, null, 2)) + console.log(`Saved ${asset.name} to ${filePath}`) + } catch (error) { + console.error(`Error downloading ${asset.name}:`, error) + } + } + + // Create a symlink or copy to 'latest' directory for easy access + const latestDir = join(cacheDir, 'latest') + const latestFile = join(latestDir, 'patch.txt') + + if (!existsSync(latestDir)) { + await mkdir(latestDir, { recursive: true }) + } + + await writeFile(latestFile, patch) + console.log(`Updated latest patch reference to ${patch}`) + + console.log('CDragon assets download complete!') +}