Initial commit

This commit is contained in:
AnRil
2026-05-16 13:43:29 +07:00
commit 688a86b611
208 changed files with 44350 additions and 0 deletions

148
src/main/games/registry.ts Normal file
View 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
}