import { BrowserWindow } from 'electron' import type { GameProvider, MatchEndPayload } from './provider' import { Dota2Provider } from './dota2' import { startGsiServer, stopGsiServer } from './gsi-server' import { onLaunchOptionsApplied } from './steam-launch-options' import { IPC } from '@shared/ipc' import type { ChallengeResult, GameId, GameStatus, MatchSummary } from '@shared/types' import { STAT_LABELS } from '@shared/types' import { getChallenges, getGamesEnabled } from '../store' import { fireMatchSummary } from '../notifications' import { log } from '../logger' const providers: Record = { dota2: new Dota2Provider() } let running = false async function onMatchEnd( gameId: GameId, payload: MatchEndPayload ): Promise { const provider = providers[gameId] const allChallenges = getChallenges().filter((c) => c.gameId === gameId) const enabledChallenges = allChallenges.filter((c) => c.enabled) log.info( `[games] match_end gameId=${gameId} stats=${JSON.stringify( payload.stats )} challenges=${enabledChallenges.length}/${allChallenges.length} (enabled/total)` ) const results: ChallengeResult[] = [] for (const ch of enabledChallenges) { const statValue = payload.stats[ch.stat] ?? 0 const reps = Math.round(statValue * ch.multiplier) if (reps <= 0) { log.debug( `[games] skip challenge "${ch.name}": ${ch.stat}=${statValue} × ${ch.multiplier} = ${reps}` ) continue } results.push({ challengeId: ch.id, name: ch.name, icon: ch.icon, exerciseName: ch.exerciseName, reps, statValue, statLabel: STAT_LABELS[ch.stat], stat: ch.stat }) } if (results.length === 0) { log.warn( `[games] match_end produced no reps (no enabled challenges matched stats). ` + `Enabled challenges: ${enabledChallenges.length}, stats keys: ${Object.keys( payload.stats ).join(',')}` ) return } log.info( `[games] firing match summary: ${results.length} challenges, total reps ${results.reduce( (s, r) => s + r.reps, 0 )}` ) const summary: MatchSummary = { gameId, gameName: provider.displayName, durationMs: payload.durationMs, won: payload.won, results } fireMatchSummary(summary) } export async function startGamesRegistry(): Promise { if (running) return running = true try { await startGsiServer() log.info('[games] GSI server started on port 4701') } catch (err) { log.error('[games] GSI server failed to start', err) return } onLaunchOptionsApplied(() => { // When Steam closed and we flushed queued ops, refresh statuses. void listGamesStatus().then((games) => broadcastGames(games)) }) const enabled = getGamesEnabled() for (const id of Object.keys(providers) as GameId[]) { const provider = providers[id] // Reconcile launch options on boot (handles cases where Steam // overwrote our flag, or user removed it manually). try { await provider.reconcile?.() } catch (err) { log.error(`[games] reconcile failed for ${id}`, err) } if (!enabled[id]) continue await provider.start((e) => { if (e.type === 'match_end') void onMatchEnd(id, e.payload) }) } } export async function stopGamesRegistry(): Promise { if (!running) return running = false for (const id of Object.keys(providers) as GameId[]) { await providers[id].stop() } stopGsiServer() } export async function listGamesStatus(): Promise { const enabled = getGamesEnabled() const out: GameStatus[] = [] for (const id of Object.keys(providers) as GameId[]) { const s = await providers[id].detect() out.push({ ...s, enabled: !!enabled[id] }) } return out } export async function installGame(id: GameId): Promise { const provider = providers[id] await provider.install() return { ...(await provider.detect()), enabled: !!getGamesEnabled()[id] } } export async function uninstallGame(id: GameId): Promise { const provider = providers[id] await provider.uninstall() await provider.stop() return { ...(await provider.detect()), enabled: !!getGamesEnabled()[id] } } export async function toggleGame(id: GameId, enabled: boolean): Promise { const provider = providers[id] if (enabled) { await provider.start((e) => { if (e.type === 'match_end') void onMatchEnd(id, e.payload) }) } else { await provider.stop() } } export function broadcastGames(games: GameStatus[]): void { for (const win of BrowserWindow.getAllWindows()) { if (!win.isDestroyed()) win.webContents.send(IPC.evtGamesChanged, games) } } // Simulate a match-end for debugging (called from IPC in dev). export function simulateMatchEnd( id: GameId, stats: Partial> ): void { void onMatchEnd(id, { durationMs: (stats.duration_min ?? 35) * 60_000, won: stats.won === 1, stats: stats as Partial> }) } export function getProviders(): Record { return providers }