Initial commit
This commit is contained in:
102
src/main/games/steam.ts
Normal file
102
src/main/games/steam.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { exec } from 'node:child_process'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import { parseVdf, type VdfNode } from './vdf'
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
async function regQuery(key: string, valueName: string): Promise<string | undefined> {
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`reg query "${key}" /v ${valueName}`,
|
||||
{ windowsHide: true }
|
||||
)
|
||||
const m = stdout.match(/REG_SZ\s+(.+)/)
|
||||
return m?.[1]?.trim()
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSteamPath(): Promise<string | undefined> {
|
||||
const fromUser = await regQuery('HKCU\\Software\\Valve\\Steam', 'SteamPath')
|
||||
if (fromUser && existsSync(fromUser)) return fromUser.replace(/\//g, '\\')
|
||||
const fromMachine = await regQuery(
|
||||
'HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam',
|
||||
'InstallPath'
|
||||
)
|
||||
if (fromMachine && existsSync(fromMachine)) return fromMachine
|
||||
// Common fallbacks
|
||||
for (const p of [
|
||||
'C:\\Program Files (x86)\\Steam',
|
||||
'C:\\Program Files\\Steam'
|
||||
]) {
|
||||
if (existsSync(p)) return p
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export type SteamLibrary = {
|
||||
path: string
|
||||
apps: Set<string>
|
||||
}
|
||||
|
||||
export async function getSteamLibraries(): Promise<SteamLibrary[]> {
|
||||
const steamPath = await getSteamPath()
|
||||
if (!steamPath) return []
|
||||
|
||||
const libsFile = join(steamPath, 'config', 'libraryfolders.vdf')
|
||||
if (!existsSync(libsFile)) {
|
||||
// Bare minimum: the install itself is a library.
|
||||
return [{ path: steamPath, apps: new Set() }]
|
||||
}
|
||||
|
||||
const raw = readFileSync(libsFile, 'utf-8')
|
||||
const parsed = parseVdf(raw)
|
||||
const libs: SteamLibrary[] = []
|
||||
|
||||
const root = (parsed['libraryfolders'] as VdfNode) ?? parsed
|
||||
for (const key of Object.keys(root)) {
|
||||
const entry = root[key]
|
||||
if (typeof entry !== 'object') continue
|
||||
const path = (entry['path'] as string) ?? ''
|
||||
if (!path) continue
|
||||
const apps = new Set<string>()
|
||||
const appsNode = entry['apps']
|
||||
if (typeof appsNode === 'object') {
|
||||
for (const appId of Object.keys(appsNode)) apps.add(appId)
|
||||
}
|
||||
libs.push({ path: path.replace(/\\\\/g, '\\'), apps })
|
||||
}
|
||||
return libs
|
||||
}
|
||||
|
||||
export async function findGameInstall(
|
||||
appId: string,
|
||||
installDirName: string
|
||||
): Promise<string | undefined> {
|
||||
const libs = await getSteamLibraries()
|
||||
for (const lib of libs) {
|
||||
// Prefer libraries that claim this app, but also probe by directory.
|
||||
const candidate = join(lib.path, 'steamapps', 'common', installDirName)
|
||||
if (existsSync(candidate)) return candidate
|
||||
}
|
||||
// Fallback: scan all libs for matching appmanifest.
|
||||
for (const lib of libs) {
|
||||
const manifest = join(lib.path, 'steamapps', `appmanifest_${appId}.acf`)
|
||||
if (!existsSync(manifest)) continue
|
||||
try {
|
||||
const acf = parseVdf(readFileSync(manifest, 'utf-8'))
|
||||
const appState = acf['AppState'] as VdfNode | undefined
|
||||
const installdir = appState?.['installdir'] as string | undefined
|
||||
if (installdir) {
|
||||
const candidate = join(lib.path, 'steamapps', 'common', installdir)
|
||||
if (existsSync(candidate)) return candidate
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
Reference in New Issue
Block a user