Initial commit
This commit is contained in:
148
src/main/games/registry.ts
Normal file
148
src/main/games/registry.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
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 {
|
||||
Challenge,
|
||||
ChallengeResult,
|
||||
GameId,
|
||||
GameStatus,
|
||||
MatchSummary
|
||||
} from '@shared/types'
|
||||
import { STAT_LABELS } from '@shared/types'
|
||||
import { getChallenges, getGamesEnabled } from '../store'
|
||||
import { fireMatchSummary } from '../notifications'
|
||||
|
||||
const providers: Record<GameId, GameProvider> = {
|
||||
dota2: new Dota2Provider()
|
||||
}
|
||||
|
||||
let running = false
|
||||
|
||||
async function onMatchEnd(gameId: GameId, payload: MatchEndPayload): Promise<void> {
|
||||
const provider = providers[gameId]
|
||||
const challenges = getChallenges().filter(
|
||||
(c) => c.gameId === gameId && c.enabled
|
||||
)
|
||||
const results: ChallengeResult[] = []
|
||||
for (const ch of challenges) {
|
||||
const statValue = payload.stats[ch.stat] ?? 0
|
||||
const reps = Math.round(statValue * ch.multiplier)
|
||||
if (reps <= 0) continue
|
||||
results.push({
|
||||
challengeId: ch.id,
|
||||
name: ch.name,
|
||||
icon: ch.icon,
|
||||
exerciseName: ch.exerciseName,
|
||||
reps,
|
||||
statValue,
|
||||
statLabel: STAT_LABELS[ch.stat]
|
||||
})
|
||||
}
|
||||
if (results.length === 0) return
|
||||
|
||||
const summary: MatchSummary = {
|
||||
gameId,
|
||||
gameName: provider.displayName,
|
||||
durationMs: payload.durationMs,
|
||||
won: payload.won,
|
||||
results
|
||||
}
|
||||
fireMatchSummary(summary)
|
||||
}
|
||||
|
||||
export async function startGamesRegistry(): Promise<void> {
|
||||
if (running) return
|
||||
running = true
|
||||
try {
|
||||
await startGsiServer()
|
||||
} catch (err) {
|
||||
console.error('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) {
|
||||
console.error('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<void> {
|
||||
if (!running) return
|
||||
running = false
|
||||
for (const id of Object.keys(providers) as GameId[]) {
|
||||
await providers[id].stop()
|
||||
}
|
||||
stopGsiServer()
|
||||
}
|
||||
|
||||
export async function listGamesStatus(): Promise<GameStatus[]> {
|
||||
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<GameStatus> {
|
||||
const provider = providers[id]
|
||||
await provider.install()
|
||||
return { ...(await provider.detect()), enabled: !!getGamesEnabled()[id] }
|
||||
}
|
||||
|
||||
export async function uninstallGame(id: GameId): Promise<GameStatus> {
|
||||
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<void> {
|
||||
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<Record<string, number>>): void {
|
||||
void onMatchEnd(id, {
|
||||
durationMs: (stats.duration_min ?? 35) * 60_000,
|
||||
won: stats.won === 1,
|
||||
stats: stats as Partial<Record<import('@shared/types').GameStat, number>>
|
||||
})
|
||||
}
|
||||
|
||||
export function getProviders(): Record<GameId, GameProvider> {
|
||||
return providers
|
||||
}
|
||||
Reference in New Issue
Block a user