feat: migrating games to leveldb

This commit is contained in:
Chubby Granny Chaser
2025-01-19 17:59:39 +00:00
parent c115040e90
commit 1f0e195854
34 changed files with 410 additions and 343 deletions

View File

@@ -1,19 +1,14 @@
import { registerEvent } from "../register-event";
import type { GameShop } from "@types";
import { Ludusavi } from "@main/services";
import { gameRepository } from "@main/repository";
import { gamesSublevel, levelKeys } from "@main/level";
const getGameBackupPreview = async (
_event: Electron.IpcMainInvokeEvent,
objectId: string,
shop: GameShop
) => {
const game = await gameRepository.findOne({
where: {
objectID: objectId,
shop,
},
});
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
return Ludusavi.getBackupPreview(shop, objectId, game?.winePrefixPath);
};

View File

@@ -10,7 +10,8 @@ import os from "node:os";
import { backupsPath } from "@main/constants";
import { app } from "electron";
import { normalizePath } from "@main/helpers";
import { gameRepository } from "@main/repository";
import { gamesSublevel } from "@main/level";
import { levelKeys } from "@main/level";
const bundleBackup = async (
shop: GameShop,
@@ -46,12 +47,7 @@ const uploadSaveGame = async (
shop: GameShop,
downloadOptionTitle: string | null
) => {
const game = await gameRepository.findOne({
where: {
objectID: objectId,
shop,
},
});
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
const bundleLocation = await bundleBackup(
shop,

View File

@@ -2,18 +2,19 @@ import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import type { GameShop } from "@types";
import type { Game, GameShop } from "@types";
import { steamGamesWorker } from "@main/workers";
import { createGame } from "@main/services/library-sync";
import { steamUrlBuilder } from "@shared";
import { updateLocalUnlockedAchivements } from "@main/services/achievements/update-local-unlocked-achivements";
import { gamesSublevel, levelKeys } from "@main/level";
const addGameToLibrary = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string,
title: string,
shop: GameShop
title: string
) => {
return gameRepository
.update(
@@ -36,17 +37,15 @@ const addGameToLibrary = async (
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
: null;
await gameRepository.insert({
const game: Game = {
title,
iconUrl,
objectID: objectId,
objectId,
shop,
});
}
};
const game = await gameRepository.findOne({
where: { objectID: objectId },
});
await gamesSublevel.put(levelKeys.game(shop, objectId), game);
}
updateLocalUnlockedAchivements(game!);

View File

@@ -1,10 +1,11 @@
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { logger } from "@main/services";
import sudo from "sudo-prompt";
import { app } from "electron";
import { PythonRPC } from "@main/services/python-rpc";
import { ProcessPayload } from "@main/services/download/types";
import { gamesSublevel, levelKeys } from "@main/level";
import { GameShop } from "@types";
const getKillCommand = (pid: number) => {
if (process.platform == "win32") {
@@ -16,15 +17,14 @@ const getKillCommand = (pid: number) => {
const closeGame = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
shop: GameShop,
objectId: string
) => {
const processes =
(await PythonRPC.rpc.get<ProcessPayload[] | null>("/process-list")).data ||
[];
const game = await gameRepository.findOne({
where: { id: gameId, isDeleted: false },
});
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
if (!game) return;

View File

@@ -1,18 +1,18 @@
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { IsNull, Not } from "typeorm";
import createDesktopShortcut from "create-desktop-shortcuts";
import path from "node:path";
import { app } from "electron";
import { removeSymbolsFromName } from "@shared";
import { GameShop } from "@types";
import { gamesSublevel, levelKeys } from "@main/level";
const createGameShortcut = async (
_event: Electron.IpcMainInvokeEvent,
id: number
shop: GameShop,
objectId: string
): Promise<boolean> => {
const game = await gameRepository.findOne({
where: { id, executablePath: Not(IsNull()) },
});
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
if (game) {
const filePath = game.executablePath;

View File

@@ -1,37 +1,27 @@
import path from "node:path";
import fs from "node:fs";
import { gameRepository } from "@main/repository";
import { getDownloadsPath } from "../helpers/get-downloads-path";
import { logger } from "@main/services";
import { registerEvent } from "../register-event";
import { GameShop } from "@types";
import { downloadsSublevel, levelKeys } from "@main/level";
const deleteGameFolder = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
shop: GameShop,
objectId: string
): Promise<void> => {
const game = await gameRepository.findOne({
where: [
{
id: gameId,
isDeleted: false,
status: "removed",
},
{
id: gameId,
progress: 1,
isDeleted: false,
},
],
});
const downloadKey = levelKeys.game(shop, objectId);
if (!game) return;
const download = await downloadsSublevel.get(downloadKey);
if (game.folderName) {
if (!download) return;
if (download.folderName) {
const folderPath = path.join(
game.downloadPath ?? (await getDownloadsPath()),
game.folderName
download.downloadPath ?? (await getDownloadsPath()),
download.folderName
);
if (fs.existsSync(folderPath)) {
@@ -52,10 +42,7 @@ const deleteGameFolder = async (
}
}
await gameRepository.update(
{ id: gameId },
{ downloadPath: null, folderName: null, status: null, progress: 0 }
);
await downloadsSublevel.del(downloadKey);
};
registerEvent("deleteGameFolder", deleteGameFolder);

View File

@@ -1,16 +1,17 @@
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { levelKeys } from "@main/level";
import { gamesSublevel } from "@main/level";
import type { GameShop } from "@types";
const getGameByObjectId = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string
) =>
gameRepository.findOne({
where: {
objectID: objectId,
isDeleted: false,
},
});
) => {
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
return game;
};
registerEvent("getGameByObjectId", getGameByObjectId);

View File

@@ -1,17 +1,14 @@
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { gamesSublevel } from "@main/level";
const getLibrary = async () =>
gameRepository.find({
where: {
isDeleted: false,
},
relations: {
downloadQueue: true,
},
order: {
createdAt: "desc",
},
});
const getLibrary = async () => {
// TODO: Add sorting
return gamesSublevel
.values()
.all()
.then((results) => {
return results.filter((game) => game.isDeleted === false);
});
};
registerEvent("getLibrary", getLibrary);

View File

@@ -1,14 +1,14 @@
import { shell } from "electron";
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { gamesSublevel, levelKeys } from "@main/level";
import { GameShop } from "@types";
const openGameExecutablePath = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
shop: GameShop,
objectId: string
) => {
const game = await gameRepository.findOne({
where: { id: gameId, isDeleted: false },
});
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
if (!game || !game.executablePath) return;

View File

@@ -1,16 +1,16 @@
import { shell } from "electron";
import path from "node:path";
import { gameRepository } from "@main/repository";
import { getDownloadsPath } from "../helpers/get-downloads-path";
import { registerEvent } from "../register-event";
import { GameShop } from "@types";
import { gamesSublevel, levelKeys } from "@main/level";
const openGameInstallerPath = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
shop: GameShop,
objectId: string
) => {
const game = await gameRepository.findOne({
where: { id: gameId, isDeleted: false },
});
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
if (!game || !game.folderName || !game.downloadPath) return true;

View File

@@ -4,11 +4,11 @@ import fs from "node:fs";
import { writeFile } from "node:fs/promises";
import { spawnSync, exec } from "node:child_process";
import { gameRepository } from "@main/repository";
import { generateYML } from "../helpers/generate-lutris-yaml";
import { getDownloadsPath } from "../helpers/get-downloads-path";
import { registerEvent } from "../register-event";
import { gamesSublevel, levelKeys } from "@main/level";
import { GameShop } from "@types";
const executeGameInstaller = (filePath: string) => {
if (process.platform === "win32") {
@@ -26,13 +26,12 @@ const executeGameInstaller = (filePath: string) => {
const openGameInstaller = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
shop: GameShop,
objectId: string
) => {
const game = await gameRepository.findOne({
where: { id: gameId, isDeleted: false },
});
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
if (!game || !game.folderName) return true;
if (!game || game.isDeleted || !game.folderName) return true;
const gamePath = path.join(
game.downloadPath ?? (await getDownloadsPath()),
@@ -40,7 +39,8 @@ const openGameInstaller = async (
);
if (!fs.existsSync(gamePath)) {
await gameRepository.update({ id: gameId }, { status: null });
// TODO: LEVELDB Remove download?
// await gameRepository.update({ id: gameId }, { status: null });
return true;
}

View File

@@ -1,26 +1,27 @@
import { registerEvent } from "../register-event";
import { gameRepository } from "../../repository";
import { HydraApi, logger } from "@main/services";
import { HydraApi } from "@main/services";
import { levelKeys } from "@main/level";
import { gamesSublevel } from "@main/level";
import type { GameShop } from "@types";
const removeGameFromLibrary = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
shop: GameShop,
objectId: string
) => {
gameRepository.update(
{ id: gameId },
{ isDeleted: true, executablePath: null }
);
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
removeRemoveGameFromLibrary(gameId).catch((err) => {
logger.error("removeRemoveGameFromLibrary", err);
});
};
if (game) {
await gamesSublevel.put(gameKey, {
...game,
isDeleted: true,
executablePath: null,
});
const removeRemoveGameFromLibrary = async (gameId: number) => {
const game = await gameRepository.findOne({ where: { id: gameId } });
if (game?.remoteId) {
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
if (game?.remoteId) {
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
}
}
};

View File

@@ -1,21 +1,15 @@
import { registerEvent } from "../register-event";
import { gameRepository } from "../../repository";
import { downloadsSublevel } from "@main/level";
import { GameShop } from "@types";
import { levelKeys } from "@main/level";
const removeGame = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
shop: GameShop,
objectId: string
) => {
await gameRepository.update(
{
id: gameId,
},
{
status: "removed",
downloadPath: null,
bytesDownloaded: 0,
progress: 0,
}
);
const downloadKey = levelKeys.game(shop, objectId);
await downloadsSublevel.del(downloadKey);
};
registerEvent("removeGame", removeGame);

View File

@@ -1,17 +1,22 @@
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { findAchievementFiles } from "@main/services/achievements/find-achivement-files";
import fs from "fs";
import { achievementsLogger, HydraApi, WindowManager } from "@main/services";
import { getUnlockedAchievements } from "../user/get-unlocked-achievements";
import { gameAchievementsSublevel, levelKeys } from "@main/level";
import {
gameAchievementsSublevel,
gamesSublevel,
levelKeys,
} from "@main/level";
import type { GameShop } from "@types";
const resetGameAchievements = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
shop: GameShop,
objectId: string
) => {
try {
const game = await gameRepository.findOne({ where: { id: gameId } });
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
if (!game) return;
@@ -24,37 +29,34 @@ const resetGameAchievements = async (
}
}
const levelKey = levelKeys.game(game.shop, game.objectID);
const levelKey = levelKeys.game(game.shop, game.objectId);
await gameAchievementsSublevel
.get(levelKey)
.then(async (gameAchievements) => {
if (gameAchievements) {
await gameAchievementsSublevel.put(
levelKeys.game(game.shop, game.objectID),
{
...gameAchievements,
unlockedAchievements: [],
}
);
await gameAchievementsSublevel.put(levelKey, {
...gameAchievements,
unlockedAchievements: [],
});
}
});
await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then(
() =>
achievementsLogger.log(
`Deleted achievements from ${game.remoteId} - ${game.objectID} - ${game.title}`
`Deleted achievements from ${game.remoteId} - ${game.objectId} - ${game.title}`
)
);
const gameAchievements = await getUnlockedAchievements(
game.objectID,
game.objectId,
game.shop,
true
);
WindowManager.mainWindow?.webContents.send(
`on-update-achievements-${game.objectID}-${game.shop}`,
`on-update-achievements-${game.objectId}-${game.shop}`,
gameAchievements
);
} catch (error) {

View File

@@ -1,13 +1,24 @@
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { gamesSublevel } from "@main/level";
import { levelKeys } from "@main/level";
import type { GameShop } from "@types";
const selectGameWinePrefix = async (
_event: Electron.IpcMainInvokeEvent,
id: number,
shop: GameShop,
objectId: string,
winePrefixPath: string | null
) => {
return gameRepository.update({ id }, { winePrefixPath: winePrefixPath });
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
if (!game) return;
await gamesSublevel.put(gameKey, {
...game,
winePrefixPath: winePrefixPath,
});
};
registerEvent("selectGameWinePrefix", selectGameWinePrefix);

View File

@@ -1,25 +1,27 @@
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { parseExecutablePath } from "../helpers/parse-executable-path";
import { gamesSublevel, levelKeys } from "@main/level";
import type { GameShop } from "@types";
const updateExecutablePath = async (
_event: Electron.IpcMainInvokeEvent,
id: number,
shop: GameShop,
objectId: string,
executablePath: string | null
) => {
const parsedPath = executablePath
? parseExecutablePath(executablePath)
: null;
return gameRepository.update(
{
id,
},
{
executablePath: parsedPath,
}
);
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
if (!game) return;
await gamesSublevel.put(gameKey, {
...game,
executablePath: parsedPath,
});
};
registerEvent("updateExecutablePath", updateExecutablePath);

View File

@@ -1,19 +1,24 @@
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { levelKeys } from "@main/level";
import { gamesSublevel } from "@main/level";
import { GameShop } from "@types";
const updateLaunchOptions = async (
_event: Electron.IpcMainInvokeEvent,
id: number,
shop: GameShop,
objectId: string,
launchOptions: string | null
) => {
return gameRepository.update(
{
id,
},
{
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
if (game) {
await gamesSublevel.put(gameKey, {
...game,
launchOptions: launchOptions?.trim() != "" ? launchOptions : null,
}
);
});
}
};
registerEvent("updateLaunchOptions", updateLaunchOptions);

View File

@@ -1,13 +1,17 @@
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { gamesSublevel } from "@main/level";
const verifyExecutablePathInUse = async (
_event: Electron.IpcMainInvokeEvent,
executablePath: string
) => {
return gameRepository.findOne({
where: { executablePath },
});
for await (const game of gamesSublevel.values()) {
if (game.executablePath === executablePath) {
return true;
}
}
return false;
};
registerEvent("verifyExecutablePathInUse", verifyExecutablePathInUse);

View File

@@ -1,3 +1,4 @@
export * from "./downloads";
export * from "./games";
export * from "./game-shop-cache";
export * from "./game-achievements";

View File

@@ -9,4 +9,5 @@ export const levelKeys = {
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
`${shop}:${objectId}:${language}`,
gameAchievements: "gameAchievements",
downloads: "downloads",
};

View File

@@ -1,16 +1,13 @@
import { DownloadManager, Ludusavi, startMainLoop } from "./services";
import {
downloadQueueRepository,
gameRepository,
userPreferencesRepository,
} from "./repository";
import { userPreferencesRepository } from "./repository";
import { UserPreferences } from "./entity";
import { RealDebridClient } from "./services/download/real-debrid";
import { HydraApi } from "./services/hydra-api";
import { uploadGamesBatch } from "./services/library-sync";
import { Aria2 } from "./services/aria2";
import { downloadsSublevel } from "./level/sublevels/downloads";
import { sortBy } from "lodash-es";
import { Downloader } from "@shared";
import { IsNull, Not } from "typeorm";
const loadState = async (userPreferences: UserPreferences | null) => {
import("./events");
@@ -27,25 +24,24 @@ const loadState = async (userPreferences: UserPreferences | null) => {
uploadGamesBatch();
});
const [nextQueueItem] = await downloadQueueRepository.find({
order: {
id: "DESC",
},
relations: {
game: true,
},
});
const downloads = await downloadsSublevel
.values()
.all()
.then((games) => {
return sortBy(games, "timestamp", "DESC");
});
const seedList = await gameRepository.find({
where: {
shouldSeed: true,
downloader: Downloader.Torrent,
progress: 1,
uri: Not(IsNull()),
},
});
const [nextItemOnQueue] = downloads;
await DownloadManager.startRPC(nextQueueItem?.game, seedList);
const downloadsToSeed = downloads.filter(
(download) =>
download.shouldSeed &&
download.downloader === Downloader.Torrent &&
download.progress === 1 &&
download.uri !== null
);
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
startMainLoop();
};

View File

@@ -14,16 +14,16 @@ import { achievementsLogger } from "../logger";
import { Cracker } from "@shared";
import { IsNull, Not } from "typeorm";
import { publishCombinedNewAchievementNotification } from "../notifications";
import { gamesSublevel } from "@main/level";
const fileStats: Map<string, number> = new Map();
const fltFiles: Map<string, Set<string>> = new Map();
const watchAchievementsWindows = async () => {
const games = await gameRepository.find({
where: {
isDeleted: false,
},
});
const games = await gamesSublevel
.values()
.all()
.then((games) => games.filter((game) => !game.isDeleted));
if (games.length === 0) return;
@@ -32,7 +32,7 @@ const watchAchievementsWindows = async () => {
for (const game of games) {
const gameAchievementFiles: AchievementFile[] = [];
for (const objectId of getAlternativeObjectIds(game.objectID)) {
for (const objectId of getAlternativeObjectIds(game.objectId)) {
gameAchievementFiles.push(...(achievementFiles.get(objectId) || []));
gameAchievementFiles.push(

View File

@@ -3,8 +3,8 @@ import fs from "node:fs";
import { app } from "electron";
import type { AchievementFile } from "@types";
import { Cracker } from "@shared";
import { Game } from "@main/entity";
import { achievementsLogger } from "../logger";
import type { Game } from "@types";
const getAppDataPath = () => {
if (process.platform === "win32") {

View File

@@ -7,7 +7,7 @@ import {
userPreferencesRepository,
} from "@main/repository";
import { publishDownloadCompleteNotification } from "../notifications";
import type { DownloadProgress } from "@types";
import type { Download, DownloadProgress } from "@types";
import { GofileApi, QiwiApi, DatanodesApi } from "../hosters";
import { PythonRPC } from "../python-rpc";
import {
@@ -20,16 +20,20 @@ import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity
import { RealDebridClient } from "./real-debrid";
import path from "path";
import { logger } from "../logger";
import { downloadsSublevel, levelKeys } from "@main/level";
export class DownloadManager {
private static downloadingGameId: number | null = null;
public static async startRPC(game?: Game, initialSeeding?: Game[]) {
public static async startRPC(
download?: Download,
downloadsToSeed?: Download[]
) {
PythonRPC.spawn(
game?.status === "active"
? await this.getDownloadPayload(game).catch(() => undefined)
download?.status === "active"
? await this.getDownloadPayload(download).catch(() => undefined)
: undefined,
initialSeeding?.map((game) => ({
downloadsToSeed?.map((download) => ({
game_id: game.id,
url: game.uri!,
save_path: game.downloadPath!,
@@ -105,6 +109,7 @@ export class DownloadManager {
const game = await gameRepository.findOne({
where: { id: gameId, isDeleted: false },
});
const userPreferences = await userPreferencesRepository.findOneBy({
id: 1,
});
@@ -141,7 +146,8 @@ export class DownloadManager {
this.cancelDownload(gameId);
}
await downloadQueueRepository.delete({ game });
await downloadsSublevel.del(levelKeys.game(game.shop, game.objectId));
const [nextQueueItem] = await downloadQueueRepository.find({
order: {
id: "DESC",

View File

@@ -1,5 +1,16 @@
import { gameRepository } from "@main/repository";
import { gamesSublevel, levelKeys } from "@main/level";
export const clearGamesRemoteIds = () => {
return gameRepository.update({}, { remoteId: null });
export const clearGamesRemoteIds = async () => {
const games = await gamesSublevel.values().all();
await gamesSublevel.batch(
games.map((game) => ({
type: "put",
key: levelKeys.game(game.shop, game.objectId),
value: {
...game,
remoteId: null,
},
}))
);
};

View File

@@ -1,19 +1,21 @@
import { Game } from "@main/entity";
import type { Game } from "@types";
import { HydraApi } from "../hydra-api";
import { gameRepository } from "@main/repository";
import { gamesSublevel, levelKeys } from "@main/level";
export const createGame = async (game: Game) => {
return HydraApi.post(`/profile/games`, {
objectId: game.objectID,
objectId: game.objectId,
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
shop: game.shop,
lastTimePlayed: game.lastTimePlayed,
}).then((response) => {
const { id: remoteId, playTimeInMilliseconds, lastTimePlayed } = response;
gameRepository.update(
{ objectID: game.objectID },
{ remoteId, playTimeInMilliseconds, lastTimePlayed }
);
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...game,
remoteId,
playTimeInMilliseconds,
lastTimePlayed,
});
});
};

View File

@@ -1,4 +1,4 @@
import { Game } from "@main/entity";
import type { Game } from "@types";
import { HydraApi } from "../hydra-api";
export const updateGamePlaytime = async (

View File

@@ -1,15 +1,19 @@
import { gameRepository } from "@main/repository";
import { chunk } from "lodash-es";
import { IsNull } from "typeorm";
import { HydraApi } from "../hydra-api";
import { mergeWithRemoteGames } from "./merge-with-remote-games";
import { WindowManager } from "../window-manager";
import { AchievementWatcherManager } from "../achievements/achievement-watcher-manager";
import { gamesSublevel } from "@main/level";
export const uploadGamesBatch = async () => {
const games = await gameRepository.find({
where: { remoteId: IsNull(), isDeleted: false },
});
const games = await gamesSublevel
.values()
.all()
.then((results) => {
return results.filter(
(game) => !game.isDeleted && game.remoteId === null
);
});
const gamesChunks = chunk(games, 200);
@@ -18,7 +22,7 @@ export const uploadGamesBatch = async () => {
"/profile/games/batch",
chunk.map((game) => {
return {
objectId: game.objectID,
objectId: game.objectId,
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
shop: game.shop,
lastTimePlayed: game.lastTimePlayed,

View File

@@ -1,12 +1,11 @@
import { gameRepository } from "@main/repository";
import { WindowManager } from "./window-manager";
import { createGame, updateGamePlaytime } from "./library-sync";
import type { GameRunning } from "@types";
import type { Game, GameRunning } from "@types";
import { PythonRPC } from "./python-rpc";
import { Game } from "@main/entity";
import axios from "axios";
import { exec } from "child_process";
import { ProcessPayload } from "./download/types";
import { gamesSublevel, levelKeys } from "@main/level";
const commands = {
findWineDir: `lsof -c wine 2>/dev/null | grep '/drive_c/windows$' | head -n 1 | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`,
@@ -14,7 +13,7 @@ const commands = {
};
export const gamesPlaytime = new Map<
number,
string,
{ lastTick: number; firstTick: number; lastSyncTick: number }
>();
@@ -82,23 +81,28 @@ const findGamePathByProcess = (
const pathSet = processMap.get(executable.exe);
if (pathSet) {
pathSet.forEach((path) => {
pathSet.forEach(async (path) => {
if (path.toLowerCase().endsWith(executable.name)) {
gameRepository.update(
{ objectID: gameId, shop: "steam" },
{ executablePath: path }
);
const gameKey = levelKeys.game("steam", gameId);
const game = await gamesSublevel.get(gameKey);
if (game) {
gamesSublevel.put(gameKey, {
...game,
executablePath: path,
});
}
if (isLinuxPlatform) {
exec(commands.findWineDir, (err, out) => {
if (err) return;
gameRepository.update(
{ objectID: gameId, shop: "steam" },
{
if (game) {
gamesSublevel.put(gameKey, {
...game,
winePrefixPath: out.trim().replace("/drive_c/windows", ""),
}
);
});
}
});
}
}
@@ -159,11 +163,12 @@ const getSystemProcessMap = async () => {
};
export const watchProcesses = async () => {
const games = await gameRepository.find({
where: {
isDeleted: false,
},
});
const games = await gamesSublevel
.values()
.all()
.then((results) => {
return results.filter((game) => game.isDeleted === false);
});
if (!games.length) return;
@@ -172,8 +177,8 @@ export const watchProcesses = async () => {
for (const game of games) {
const executablePath = game.executablePath;
if (!executablePath) {
if (gameExecutables[game.objectID]) {
findGamePathByProcess(processMap, game.objectID);
if (gameExecutables[game.objectId]) {
findGamePathByProcess(processMap, game.objectId);
}
continue;
}
@@ -185,12 +190,12 @@ export const watchProcesses = async () => {
const hasProcess = processMap.get(executable)?.has(executablePath);
if (hasProcess) {
if (gamesPlaytime.has(game.id)) {
if (gamesPlaytime.has(`${game.shop}-${game.objectId}`)) {
onTickGame(game);
} else {
onOpenGame(game);
}
} else if (gamesPlaytime.has(game.id)) {
} else if (gamesPlaytime.has(`${game.shop}-${game.objectId}`)) {
onCloseGame(game);
}
}
@@ -215,7 +220,7 @@ export const watchProcesses = async () => {
function onOpenGame(game: Game) {
const now = performance.now();
gamesPlaytime.set(game.id, {
gamesPlaytime.set(`${game.shop}-${game.objectId}`, {
lastTick: now,
firstTick: now,
lastSyncTick: now,
@@ -230,16 +235,23 @@ function onOpenGame(game: Game) {
function onTickGame(game: Game) {
const now = performance.now();
const gamePlaytime = gamesPlaytime.get(game.id)!;
const gamePlaytime = gamesPlaytime.get(`${game.shop}-${game.objectId}`)!;
const delta = now - gamePlaytime.lastTick;
gameRepository.update(game.id, {
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...game,
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
lastTimePlayed: new Date(),
});
gamesPlaytime.set(game.id, {
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...game,
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
lastTimePlayed: new Date(),
});
gamesPlaytime.set(`${game.shop}-${game.objectId}`, {
...gamePlaytime,
lastTick: now,
});
@@ -255,7 +267,7 @@ function onTickGame(game: Game) {
gamePromise
.then(() => {
gamesPlaytime.set(game.id, {
gamesPlaytime.set(`${game.shop}-${game.objectId}`, {
...gamePlaytime,
lastSyncTick: now,
});
@@ -265,8 +277,8 @@ function onTickGame(game: Game) {
}
const onCloseGame = (game: Game) => {
const gamePlaytime = gamesPlaytime.get(game.id)!;
gamesPlaytime.delete(game.id);
const gamePlaytime = gamesPlaytime.get(`${game.shop}-${game.objectId}`)!;
gamesPlaytime.delete(`${game.shop}-${game.objectId}`);
if (game.remoteId) {
updateGamePlaytime(