refactor: remove patch_detector and use gameVersion field in match_collector
All checks were successful
pipeline / lint-and-format (push) Successful in 4m20s
pipeline / build-and-push-images (push) Successful in 1m20s

This commit is contained in:
2026-04-30 10:37:42 +02:00
parent e1ab81854a
commit 7051ace13f
16 changed files with 379 additions and 2617 deletions

View File

@@ -6,10 +6,21 @@ type Match = {
}
info: {
endOfGameResult: string
frameInterval: number
gameCreation: number
gameDuration: number
gameEndTimestamp: number
gameId: number
gameMode: string
gameName: string
gameStartTimestamp: number
gameType: string
gameVersion: string
mapId: number
participants: Participant[]
platformId: string
queueId: number
teams: Team[]
tournamentCode: string
}
timeline: Timeline
}

View File

@@ -0,0 +1,99 @@
import { writeFile, mkdir } from 'fs/promises'
import { existsSync } from 'fs'
import { join, resolve } from 'path'
import { fileURLToPath } from 'url'
import { dirname } from 'path'
// Get current directory for relative path resolution
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
// 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'
}
]
/**
* Download CDragon assets for a specific patch.
* This caches game data locally for faster access.
*/
async function downloadCDragonAssets(patch: string) {
// Convert patch format for CDragon: "16.4" -> "16.4" (already in correct format)
// CDragon uses patch format without the last minor version
const cdragonPatch = patch
console.log(`\n=== Downloading CDragon assets for patch ${cdragonPatch} ===`)
// Get cache directory from environment or use default
// In development, use a local directory relative to project root; in production (Docker), use /cdragon
const defaultCacheDir =
process.env.NODE_ENV === 'development'
? resolve(__dirname, '../../dev/data/cdragon')
: '/cdragon'
const cacheDir = process.env.CDRAGON_CACHE_DIR || defaultCacheDir
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!')
}
export { downloadCDragonAssets }

View File

@@ -6,9 +6,56 @@ import { MongoClient } from 'mongodb'
import champion_stat from './champion_stat'
import { Match } from './api'
import { PLATFORMS, getPlatformBaseUrl, getRegionalBaseUrl, getRegionForPlatform } from './platform'
import { downloadCDragonAssets } from './cdragon_cache'
main()
/**
* Extract patch version from gameVersion string.
* gameVersion format is like "15.1.123.4567" -> we want "15.1"
*/
function extractPatchFromGameVersion(gameVersion: string): string {
const parts = gameVersion.split('.')
if (parts.length >= 2) {
return `${parts[0]}.${parts[1]}`
}
return gameVersion
}
/**
* Get the latest patch from existing match collections in the database.
* Collections are named like "15.1_EUW1", "15.2_NA1", etc.
*/
async function getLatestPatchFromCollections(client: MongoClient): Promise<string | null> {
const matchesDb = client.db('matches')
const collections = await matchesDb.listCollections().toArray()
const collectionNames = collections.map(c => c.name)
// Extract unique patch versions from collection names
const patches = new Set<string>()
for (const name of collectionNames) {
// Collection names are either "patch_platform" or just "patch"
const patch = name.split('_')[0]
if (patch && /^\d+\.\d+$/.test(patch)) {
patches.add(patch)
}
}
if (patches.size === 0) {
return null
}
// Sort patches and return the latest (highest version number)
const sortedPatches = Array.from(patches).sort((a, b) => {
const [aMajor, aMinor] = a.split('.').map(Number)
const [bMajor, bMinor] = b.split('.').map(Number)
if (aMajor !== bMajor) return bMajor - aMajor
return bMinor - aMinor
})
return sortedPatches[0]
}
async function main() {
// Check if we're in development mode with pre-loaded data
if (process.env.NODE_ENV === 'development' && process.env.USE_IMPORTED_DATA === 'true') {
@@ -17,13 +64,10 @@ async function main() {
return
}
// Original production mode
// Production mode: collect matches and organize by their gameVersion
console.log('MatchCollector - Hello !')
const client = await connectToDatabase()
const [latestPatch, latestPatchTime] = await fetchLatestPatchDate(client)
console.log(
'Connected to database, latest patch ' + latestPatch + ' was epoch: ' + latestPatchTime
)
console.log('Connected to database')
console.log('Using RIOT_API_KEY: ' + api_key)
if (api_key != null && api_key != undefined && api_key != '') {
@@ -31,20 +75,22 @@ async function main() {
for (const [platform, region] of Object.entries(PLATFORMS)) {
console.log(`\n=== Processing platform: ${platform} (region: ${region}) ===`)
const alreadySeenGameList = await alreadySeenGames(client, latestPatch, platform)
console.log(
'We already have ' + alreadySeenGameList.length + ' matches for this patch/platform !'
)
// Get already seen games for all patches (we'll check by gameVersion when saving)
const alreadySeenGameList = await alreadySeenGamesAllPatches(client, platform)
console.log('We already have ' + alreadySeenGameList.length + ' matches for this platform !')
const challengerLeague = await fetchChallengerLeague(platform)
console.log('ChallengerLeague: got ' + challengerLeague.entries.length + ' entries')
// Use 30 days ago as start time for collecting matches
const startTime = Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60
const gameList: string[] = []
let i = 0
for (const challenger of challengerLeague.entries) {
console.log('Entry ' + i + '/' + challengerLeague.entries.length + ' ...')
const puuid = challenger.puuid
const challengerGameList = await summonerGameList(puuid, latestPatchTime, region)
const challengerGameList = await summonerGameList(puuid, startTime, region)
for (const game of challengerGameList) {
if (!gameList.includes(game) && !alreadySeenGameList.includes(game)) {
gameList.push(game)
@@ -64,14 +110,25 @@ async function main() {
const gameMatch = await match(game, matchRegion)
const gameTimeline = await matchTimeline(game, matchRegion)
gameMatch.timeline = gameTimeline
await saveMatch(client, gameMatch, latestPatch, platform)
// Extract patch from gameVersion and save to appropriate collection
const patch = extractPatchFromGameVersion(gameMatch.info.gameVersion)
await saveMatch(client, gameMatch, patch, platform)
i++
}
}
}
console.log('Generating stats...')
await champion_stat.makeChampionsStats(client, latestPatch, Object.keys(PLATFORMS))
// Get the latest patch from collections and generate stats for it
const latestPatch = await getLatestPatchFromCollections(client)
if (latestPatch) {
console.log(`Generating stats for latest patch: ${latestPatch}...`)
await champion_stat.makeChampionsStats(client, latestPatch, Object.keys(PLATFORMS))
// Download CDragon assets for the latest patch
await downloadCDragonAssets(latestPatch)
} else {
console.log('No matches found in database, skipping stat generation')
}
console.log('All done. Closing client.')
await client.close()
@@ -117,13 +174,6 @@ async function connectToDatabase() {
return client
}
async function fetchLatestPatchDate(client: MongoClient) {
const database = client.db('patches')
const patches = database.collection('patches')
const latestPatch = await patches.find().limit(1).sort({ date: -1 }).next()
return [latestPatch!.patch, Math.floor(latestPatch!.date.valueOf() / 1000)]
}
async function fetchChallengerLeague(platform: string) {
const queue = 'RANKED_SOLO_5x5'
const endpoint = `/lol/league/v4/challengerleagues/by-queue/${queue}`
@@ -138,7 +188,7 @@ async function fetchChallengerLeague(platform: string) {
return challengerLeague
}
async function summonerGameList(puuid: string, startTime: string, region: string) {
async function summonerGameList(puuid: string, startTime: number, region: string) {
const baseUrl = getRegionalBaseUrl(region)
const endpoint = `/lol/match/v5/matches/by-puuid/${puuid}/ids`
const url = `${baseUrl}${endpoint}?queue=420&type=ranked&startTime=${startTime}&api_key=${api_key}`
@@ -174,18 +224,31 @@ async function matchTimeline(matchId: string, region: string) {
return timeline
}
async function alreadySeenGames(client: MongoClient, latestPatch: string, platform: string) {
const database = client.db('matches')
const collectionName = `${latestPatch}_${platform}`
const matches = database.collection(collectionName)
/**
* Get already seen games across all patches for a specific platform.
* This is used when we don't know the patch beforehand (we get it from gameVersion).
*/
async function alreadySeenGamesAllPatches(client: MongoClient, platform: string) {
const matchesDb = client.db('matches')
const collections = await matchesDb.listCollections().toArray()
const collectionNames = collections.map(c => c.name)
const alreadySeen = await matches.distinct('metadata.matchId')
return alreadySeen
// Find all collections for this platform (format: "patch_platform")
const platformCollections = collectionNames.filter(name => name.endsWith(`_${platform}`))
const allSeen: string[] = []
for (const collectionName of platformCollections) {
const matches = matchesDb.collection(collectionName)
const seen = await matches.distinct('metadata.matchId')
allSeen.push(...seen)
}
return allSeen
}
async function saveMatch(client: MongoClient, match: Match, latestPatch: string, platform: string) {
async function saveMatch(client: MongoClient, match: Match, patch: string, platform: string) {
const database = client.db('matches')
const collectionName = `${latestPatch}_${platform}`
const collectionName = `${patch}_${platform}`
const matches = database.collection(collectionName)
await matches.insertOne(match)
}
@@ -198,7 +261,16 @@ async function runWithPreloadedData() {
const client = await connectToDatabase()
try {
const [latestPatch] = await fetchLatestPatchDate(client)
// Get the latest patch from collections instead of patches database
const latestPatch = await getLatestPatchFromCollections(client)
if (!latestPatch) {
console.error('❌ No match data found in database')
console.log('💡 Please run the data import script first:')
console.log(' node dev/scripts/setup-db.js')
return
}
console.log(`Latest patch: ${latestPatch}`)
// Check if we have matches for this patch (including platform-specific collections)
@@ -235,6 +307,9 @@ async function runWithPreloadedData() {
await champion_stat.makeChampionsStats(client, latestPatch)
}
// Download CDragon assets for the latest patch
await downloadCDragonAssets(latestPatch)
console.log('🎉 All stats generated successfully!')
console.log('🚀 Your development database is ready for frontend testing!')
} catch (error) {