hydra/src/main/services/process-watcher.ts

229 lines
5.2 KiB
TypeScript

import { IsNull, Not } from "typeorm";
import { gameRepository } from "@main/repository";
import { WindowManager } from "./window-manager";
import { createGame, updateGamePlaytime } from "./library-sync";
import type { GameRunning } from "@types";
import { PythonInstance } from "./download";
import { Game } from "@main/entity";
import axios from "axios";
export const gamesPlaytime = new Map<
number,
{ lastTick: number; firstTick: number; lastSyncTick: number }
>();
interface ExecutableInfo {
name: string;
os: string;
}
interface GameExecutables {
[key: string]: ExecutableInfo[];
}
const TICKS_TO_UPDATE_API = 120;
let currentTick = 1;
const gameExecutables = (
await axios
.get("https://assets.hydralauncher.gg/game-executables.json")
.catch(() => {
return { data: {} };
})
).data as GameExecutables;
const gamesIdWithoutPath = async () => {
const games = await gameRepository.find({
where: {
executablePath: IsNull(),
isDeleted: false,
},
select: {
objectID: true,
},
});
const gameExecutableIds: string[] = [];
for (const game of games) {
const has = gameExecutables[game.objectID];
if (has) {
gameExecutableIds.push(game.objectID);
}
}
return gameExecutableIds;
};
const findGamePathByProcess = (
processMap: Map<string, Set<string>>,
gameIds: string[]
) => {
for (const id of gameIds) {
if (process.platform === "win32") {
const executables = gameExecutables[id].filter(
(info) => info.os === "win32"
);
for (const executable of executables) {
const exe = getExecutable(executable.name);
if (!exe) continue;
const hasProcess = processMap.get(exe);
if (hasProcess) {
for (const path of [...hasProcess]) {
if (path.toLowerCase().endsWith(executable.name)) {
gameRepository.update(
{ objectID: id, shop: "steam" },
{ executablePath: path }
);
}
}
}
}
}
}
};
const getSystemProcessMap = async () => {
const processes = await PythonInstance.getProcessList();
const map = new Map<string, Set<string>>();
processes.forEach((process) => {
const [key, value] = [process.name?.toLowerCase(), process.exe];
if (!key || !value) return;
map.set(key, (map.get(key) ?? new Set()).add(value));
});
return map;
};
const getExecutable = (path: string) => {
return path.slice(path.lastIndexOf("/") + 1);
};
export const watchProcesses = async () => {
const gameIds = await gamesIdWithoutPath();
const games = await gameRepository.find({
where: {
executablePath: Not(IsNull()),
isDeleted: false,
},
});
if (!games.length && !gameIds.length) return;
const processMap = await getSystemProcessMap();
findGamePathByProcess(processMap, gameIds);
if (!games.length) return;
for (const game of games) {
if (!game.executablePath) continue;
const executable = getExecutable(game.executablePath);
const gameProcess = processMap.get(executable)?.has(game.executablePath);
if (gameProcess) {
if (gamesPlaytime.has(game.id)) {
onTickGame(game);
} else {
onOpenGame(game);
}
} else if (gamesPlaytime.has(game.id)) {
onCloseGame(game);
}
}
currentTick++;
if (WindowManager.mainWindow) {
const gamesRunning = Array.from(gamesPlaytime.entries()).map((entry) => {
return {
id: entry[0],
sessionDurationInMillis: performance.now() - entry[1].firstTick,
};
});
WindowManager.mainWindow.webContents.send(
"on-games-running",
gamesRunning as Pick<GameRunning, "id" | "sessionDurationInMillis">[]
);
}
};
function onOpenGame(game: Game) {
const now = performance.now();
gamesPlaytime.set(game.id, {
lastTick: now,
firstTick: now,
lastSyncTick: now,
});
if (game.remoteId) {
updateGamePlaytime(game, 0, new Date()).catch(() => {});
} else {
createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {});
}
}
function onTickGame(game: Game) {
const now = performance.now();
const gamePlaytime = gamesPlaytime.get(game.id)!;
const delta = now - gamePlaytime.lastTick;
gameRepository.update(game.id, {
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
lastTimePlayed: new Date(),
});
gamesPlaytime.set(game.id, {
...gamePlaytime,
lastTick: now,
});
if (currentTick % TICKS_TO_UPDATE_API === 0) {
const gamePromise = game.remoteId
? updateGamePlaytime(
game,
now - gamePlaytime.lastSyncTick,
game.lastTimePlayed!
)
: createGame(game);
gamePromise
.then(() => {
gamesPlaytime.set(game.id, {
...gamePlaytime,
lastSyncTick: now,
});
})
.catch(() => {});
}
}
const onCloseGame = (game: Game) => {
const gamePlaytime = gamesPlaytime.get(game.id)!;
gamesPlaytime.delete(game.id);
if (game.remoteId) {
updateGamePlaytime(
game,
performance.now() - gamePlaytime.lastSyncTick,
game.lastTimePlayed!
).catch(() => {});
} else {
createGame(game).catch(() => {});
}
};