diff --git a/src/main/data-source.ts b/src/main/data-source.ts index 29c72f8c..9745abd8 100644 --- a/src/main/data-source.ts +++ b/src/main/data-source.ts @@ -7,6 +7,7 @@ import { Repack, UserPreferences, UserAuth, + GameAchievement, } from "@main/entity"; import { databasePath } from "./constants"; @@ -21,6 +22,7 @@ export const dataSource = new DataSource({ DownloadSource, DownloadQueue, UserAuth, + GameAchievement, ], synchronize: false, database: databasePath, diff --git a/src/main/entity/game-achievements.entity.ts b/src/main/entity/game-achievements.entity.ts new file mode 100644 index 00000000..e50d3294 --- /dev/null +++ b/src/main/entity/game-achievements.entity.ts @@ -0,0 +1,21 @@ +import { + Column, + Entity, + JoinColumn, + OneToOne, + PrimaryGeneratedColumn, +} from "typeorm"; +import { Game } from "./game.entity"; + +@Entity("game_achievement") +export class GameAchievement { + @PrimaryGeneratedColumn() + id: number; + + @OneToOne(() => Game) + @JoinColumn() + game: Game; + + @Column("text", { nullable: true }) + achievements: string; +} diff --git a/src/main/entity/index.ts b/src/main/entity/index.ts index 9cb4f044..7e52577c 100644 --- a/src/main/entity/index.ts +++ b/src/main/entity/index.ts @@ -2,6 +2,8 @@ export * from "./game.entity"; export * from "./repack.entity"; export * from "./user-preferences.entity"; export * from "./game-shop-cache.entity"; +export * from "./game.entity"; +export * from "./game-achievements.entity"; export * from "./download-source.entity"; export * from "./download-queue.entity"; export * from "./user-auth"; diff --git a/src/main/events/achievements/game-achievements-observer.ts b/src/main/events/achievements/game-achievements-observer.ts new file mode 100644 index 00000000..a0cdfaf4 --- /dev/null +++ b/src/main/events/achievements/game-achievements-observer.ts @@ -0,0 +1,89 @@ +import { watch } from "node:fs/promises"; +import { getGameAchievementsToWatch } from "./services/get-game-achievements-to-watch"; +import { checkUnlockedAchievements } from "./util/check-unlocked-achievements"; +import { parseAchievementFile } from "./util/parseAchievementFile"; +import { gameAchievementRepository } from "@main/repository"; + +type GameAchievementObserver = { + [id: number]: AbortController | null; +}; + +const gameAchievementObserver: GameAchievementObserver = {}; + +export const startGameAchievementObserver = async (gameId: number) => { + if ( + gameAchievementObserver[gameId] === null || + gameAchievementObserver[gameId] + ) { + return; + } + + console.log(`Starting: ${gameId}`); + + const achievementsToWatch = await getGameAchievementsToWatch(gameId); + + if (!achievementsToWatch) { + console.log("No achievements to observe"); + gameAchievementObserver[gameId] = null; + return; + } + + const { steamId, checkedAchievements, achievementFiles } = + achievementsToWatch; + + gameAchievementObserver[gameId] = new AbortController(); + + const achievements = checkedAchievements.all; + + for (const file of achievementFiles) { + const signal = gameAchievementObserver[gameId]?.signal; + if (!signal) return; + console.log(`cracker: ${file.type}, steamId: ${steamId}`); + (async () => { + try { + const watcher = watch(file.filePath, { + signal, + }); + for await (const event of watcher) { + if (event.eventType === "change") { + console.log("file modified"); + const localAchievementFile = await parseAchievementFile( + file.filePath + ); + + if (!localAchievementFile) continue; + + const checked = checkUnlockedAchievements( + file.type, + localAchievementFile, + achievements + ); + + if (checked.new) { + console.log(checked.new); + + gameAchievementRepository.update( + { + game: { id: steamId }, + }, + { + achievements: JSON.stringify(checked.all), + } + ); + } + } + } + } catch (err: any) { + console.log(`cracker: ${file.type}, steamId ${steamId}`); + if (err?.name === "AbortError") return; + throw err; + } + })(); + } +}; + +export const stopGameAchievementObserver = (gameId: number) => { + console.log(`Stopping: ${gameId}`); + gameAchievementObserver[gameId]?.abort(); + delete gameAchievementObserver[gameId]; +}; diff --git a/src/main/events/achievements/services/get-game-achievements-to-watch.ts b/src/main/events/achievements/services/get-game-achievements-to-watch.ts new file mode 100644 index 00000000..c44d7b6b --- /dev/null +++ b/src/main/events/achievements/services/get-game-achievements-to-watch.ts @@ -0,0 +1,59 @@ +import { gameRepository, gameAchievementRepository } from "@main/repository"; +import { steamGetAchivement } from "../steam/steam-get-achivement"; +import { steamFindGameAchievementFiles } from "../steam/steam-find-game-achivement-files"; +import { AchievementFile, CheckedAchievements } from "../types"; +import { parseAchievementFile } from "../util/parseAchievementFile"; +import { checkUnlockedAchievements } from "../util/check-unlocked-achievements"; + +export const getGameAchievementsToWatch = async ( + gameId: number +): Promise< + | { + steamId: number; + checkedAchievements: CheckedAchievements; + achievementFiles: AchievementFile[]; + } + | undefined +> => { + const game = await gameRepository.findOne({ where: { id: gameId } }); + + if (!game || game.shop !== "steam") return; + + const steamId = Number(game.objectID); + + const achievements = await steamGetAchivement(game); + console.log(achievements); + if (!achievements || !achievements.length) return; + + const achievementFiles = steamFindGameAchievementFiles(game.objectID)[ + steamId + ]; + console.log(achievementFiles); + if (!achievementFiles || !achievementFiles.length) return; + + const checkedAchievements: CheckedAchievements = { + all: achievements, + new: [], + }; + + for (const achievementFile of achievementFiles) { + const file = await parseAchievementFile(achievementFile.filePath); + + checkedAchievements.new.push( + ...checkUnlockedAchievements(achievementFile.type, file, achievements).new + ); + } + + if (checkedAchievements.new.length) { + await gameAchievementRepository.update( + { + game: { id: gameId }, + }, + { + achievements: JSON.stringify(checkedAchievements.all), + } + ); + } + + return { steamId, checkedAchievements, achievementFiles }; +}; diff --git a/src/main/events/achievements/services/save-all-local-steam-achivements.ts b/src/main/events/achievements/services/save-all-local-steam-achivements.ts new file mode 100644 index 00000000..cb28edbe --- /dev/null +++ b/src/main/events/achievements/services/save-all-local-steam-achivements.ts @@ -0,0 +1,74 @@ +import { gameAchievementRepository, gameRepository } from "@main/repository"; +import { steamFindGameAchievementFiles } from "../steam/steam-find-game-achivement-files"; +import { steamGlobalAchievementPercentages } from "../steam/steam-global-achievement-percentages"; +import { steamAchievementInfo } from "../steam/steam-achievement-info"; +import { steamAchievementMerge } from "../steam/steam-achievement-merge"; +import { parseAchievementFile } from "../util/parseAchievementFile"; +import { checkUnlockedAchievements } from "../util/check-unlocked-achievements"; +import { CheckedAchievements } from "../types"; + +export const saveAllLocalSteamAchivements = async () => { + const gameAchievementFiles = steamFindGameAchievementFiles(); + + for (const key of Object.keys(gameAchievementFiles)) { + const objectId = key; + + const game = await gameRepository.findOne({ + where: { objectID: objectId }, + }); + + if (!game) continue; + + const hasOnDb = await gameAchievementRepository.existsBy({ + game: game, + }); + + if (hasOnDb) continue; + + const achievementPercentage = + await steamGlobalAchievementPercentages(objectId); + + if (!achievementPercentage) { + await gameAchievementRepository.save({ + game, + achievements: "[]", + }); + continue; + } + + const achievementInfo = await steamAchievementInfo(objectId); + + if (!achievementInfo) continue; + + const achievements = steamAchievementMerge( + achievementPercentage, + achievementInfo + ); + + if (!achievements) continue; + + const checkedAchievements: CheckedAchievements = { + all: achievements, + new: [], + }; + + for (const achievementFile of gameAchievementFiles[key]) { + const localAchievementFile = await parseAchievementFile( + achievementFile.filePath + ); + + checkedAchievements.new.push( + ...checkUnlockedAchievements( + achievementFile.type, + localAchievementFile, + achievements + ).new + ); + } + + await gameAchievementRepository.save({ + game, + achievements: JSON.stringify(checkedAchievements.all), + }); + } +}; diff --git a/src/main/events/achievements/steam/steam-achievement-info.ts b/src/main/events/achievements/steam/steam-achievement-info.ts new file mode 100644 index 00000000..1c1a5686 --- /dev/null +++ b/src/main/events/achievements/steam/steam-achievement-info.ts @@ -0,0 +1,54 @@ +import { logger } from "@main/services"; +import { AchievementInfo } from "../types"; +import { JSDOM } from "jsdom"; + +export const steamAchievementInfo = async ( + objectId: string +): Promise => { + const fetchUrl = `https://steamcommunity.com/stats/${objectId}/achievements`; + + const achievementInfosHtmlText = await fetch(fetchUrl, { + method: "GET", + //headers: { "Accept-Language": "" }, + }) + .then((res) => { + if (res.status === 200) return res.text(); + throw new Error(); + }) + .catch((err) => { + logger.error(err, { method: "getSteamGameAchievements" }); + return; + }); + + if (!achievementInfosHtmlText) return; + + const achievementInfos: AchievementInfo[] = []; + + const window = new JSDOM(achievementInfosHtmlText).window; + + const itens = Array.from( + window.document.getElementsByClassName("achieveRow") + ); + + for (const item of itens) { + const imageUrl = item + .getElementsByClassName("achieveImgHolder")?.[0] + .getElementsByTagName("img")?.[0]?.src; + + const achievementName = item + .getElementsByClassName("achieveTxt")?.[0] + .getElementsByTagName("h3")?.[0].innerHTML; + + const achievementDescription = item + .getElementsByClassName("achieveTxt")?.[0] + .getElementsByTagName("h5")?.[0].innerHTML; + + achievementInfos.push({ + imageUrl: imageUrl ?? "", + title: achievementName ?? "", + description: achievementDescription ?? "", + }); + } + + return achievementInfos; +}; diff --git a/src/main/events/achievements/steam/steam-achievement-merge.ts b/src/main/events/achievements/steam/steam-achievement-merge.ts new file mode 100644 index 00000000..163155b8 --- /dev/null +++ b/src/main/events/achievements/steam/steam-achievement-merge.ts @@ -0,0 +1,28 @@ +import { Achievement, AchievementInfo, AchievementPercentage } from "../types"; + +export const steamAchievementMerge = ( + achievementPercentage: AchievementPercentage[], + achievementInfo: AchievementInfo[] +): Achievement[] | undefined => { + if (achievementPercentage.length > achievementInfo.length) return; + + const size = achievementPercentage.length; + + const achievements: Achievement[] = new Array(size); + + for (let i = 0; i < size; i++) { + achievements[i] = { + id: achievementPercentage[i].name, + percent: achievementPercentage[i].percent, + imageUrl: achievementInfo[i].imageUrl, + title: achievementInfo[i].title, + description: achievementInfo[i].description, + achieved: false, + curProgress: 0, + maxProgress: 0, + unlockTime: 0, + }; + } + + return achievements; +}; diff --git a/src/main/events/achievements/steam/steam-find-game-achivement-files.ts b/src/main/events/achievements/steam/steam-find-game-achivement-files.ts new file mode 100644 index 00000000..cc73490a --- /dev/null +++ b/src/main/events/achievements/steam/steam-find-game-achivement-files.ts @@ -0,0 +1,86 @@ +import path from "node:path"; +import { existsSync, readdirSync } from "node:fs"; +import { Cracker, GameAchievementFiles } from "../types"; +import { app } from "electron"; + +const addGame = ( + achievementFiles: GameAchievementFiles, + gamePath: string, + objectId: string, + fileLocation: string[], + type: Cracker +) => { + const filePath = path.join(gamePath, objectId, ...fileLocation); + + if (existsSync(filePath)) { + const achivementFile = { + type, + filePath: filePath, + }; + + achievementFiles[objectId] + ? achievementFiles[objectId].push(achivementFile) + : (achievementFiles[objectId] = [achivementFile]); + } +}; + +export const steamFindGameAchievementFiles = ( + objectId?: string +): GameAchievementFiles => { + //TODO: change to a automatized method + const publicDir = path.join("C:", "Users", "Public", "Documents"); + const appData = app.getPath("appData"); + + const gameAchievementFiles: GameAchievementFiles = {}; + + const crackers: Cracker[] = [ + Cracker.codex, + Cracker.goldberg, + Cracker.rune, + Cracker.onlineFix, + ]; + + for (const cracker of crackers) { + let achievementPath: string; + let fileLocation: string[]; + + if (cracker === Cracker.onlineFix) { + achievementPath = path.join(publicDir, Cracker.onlineFix); + fileLocation = ["Stats", "Achievements.ini"]; + } else if (cracker === Cracker.goldberg) { + achievementPath = path.join(appData, "Goldberg SteamEmu Saves"); + fileLocation = ["achievements.json"]; + } else { + achievementPath = path.join(publicDir, "Steam", cracker); + fileLocation = ["achievements.ini"]; + } + + if (!existsSync(achievementPath)) continue; + + const objectIds = readdirSync(achievementPath); + + if (objectId) { + if (objectIds.includes(objectId)) { + addGame( + gameAchievementFiles, + achievementPath, + objectId, + fileLocation, + cracker + ); + } + } else { + for (const objectId of objectIds) { + addGame( + gameAchievementFiles, + achievementPath, + objectId, + fileLocation, + cracker + ); + } + } + } + + return gameAchievementFiles; +}; diff --git a/src/main/events/achievements/steam/steam-get-achivement.ts b/src/main/events/achievements/steam/steam-get-achivement.ts new file mode 100644 index 00000000..3d556b6f --- /dev/null +++ b/src/main/events/achievements/steam/steam-get-achivement.ts @@ -0,0 +1,48 @@ +import { gameAchievementRepository } from "@main/repository"; +import { steamGlobalAchievementPercentages } from "./steam-global-achievement-percentages"; +import { steamAchievementInfo } from "./steam-achievement-info"; +import { steamAchievementMerge } from "./steam-achievement-merge"; +import { Achievement } from "../types"; +import { Game } from "@main/entity"; + +export const steamGetAchivement = async ( + game: Game +): Promise => { + const gameAchivement = await gameAchievementRepository.findOne({ + where: { game: game }, + }); + + if (!gameAchivement) { + const achievementPercentage = await steamGlobalAchievementPercentages( + game.objectID + ); + console.log(achievementPercentage); + if (!achievementPercentage) { + await gameAchievementRepository.save({ + game, + achievements: "[]", + }); + return []; + } + + const achievementInfo = await steamAchievementInfo(game.objectID); + console.log(achievementInfo); + if (!achievementInfo) return; + + const achievements = steamAchievementMerge( + achievementPercentage, + achievementInfo + ); + + if (!achievements) return; + + await gameAchievementRepository.save({ + game, + achievements: JSON.stringify(achievements), + }); + + return achievements; + } else { + return JSON.parse(gameAchivement.achievements); + } +}; diff --git a/src/main/events/achievements/steam/steam-global-achievement-percentages.ts b/src/main/events/achievements/steam/steam-global-achievement-percentages.ts new file mode 100644 index 00000000..26061e23 --- /dev/null +++ b/src/main/events/achievements/steam/steam-global-achievement-percentages.ts @@ -0,0 +1,33 @@ +import { logger } from "@main/services"; +import { AchievementPercentage } from "../types"; + +interface GlobalAchievementPercentages { + achievementpercentages: { + achievements: Array; + }; +} + +export const steamGlobalAchievementPercentages = async ( + objectId: string +): Promise => { + const fetchUrl = `https://api.steampowered.com/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v0002/?gameid=${objectId}`; + + const achievementPercentages: Array | undefined = ( + await fetch(fetchUrl, { + method: "GET", + }) + .then((res) => { + if (res.status === 200) return res.json(); + return; + }) + .then((data: GlobalAchievementPercentages) => data) + .catch((err) => { + logger.error(err, { method: "getSteamGameAchievements" }); + return; + }) + )?.achievementpercentages.achievements; + + if (!achievementPercentages) return; + + return achievementPercentages; +}; diff --git a/src/main/events/achievements/types/index.ts b/src/main/events/achievements/types/index.ts new file mode 100644 index 00000000..9a04a3a7 --- /dev/null +++ b/src/main/events/achievements/types/index.ts @@ -0,0 +1,52 @@ +export enum Cracker { + codex = "CODEX", + rune = "RUNE", + onlineFix = "OnlineFix", + goldberg = "Goldberg", +} + +export interface CheckedAchievements { + all: Achievement[]; + new: Achievement[]; +} + +export interface Achievement { + id: string; + percent: number; + imageUrl: string; + title: string; + description: string; + achieved: boolean; + curProgress: number; + maxProgress: number; + unlockTime: number; +} + +export interface AchievementInfo { + imageUrl: string; + title: string; + description: string; +} + +export interface AchievementPercentage { + name: string; + percent: number; +} + +export interface CheckedAchievement { + all: Achievement[]; + new: Achievement[]; +} + +export interface AchievementFile { + type: Cracker; + filePath: string; +} + +export type GameAchievementFiles = { + [id: string]: AchievementFile[]; +}; + +export type GameAchievementFile = { + [id: string]: AchievementFile[]; +}; diff --git a/src/main/events/achievements/util/check-unlocked-achievements.ts b/src/main/events/achievements/util/check-unlocked-achievements.ts new file mode 100644 index 00000000..2c9e2b37 --- /dev/null +++ b/src/main/events/achievements/util/check-unlocked-achievements.ts @@ -0,0 +1,102 @@ +import { Achievement, CheckedAchievements, Cracker } from "../types"; + +export const checkUnlockedAchievements = ( + type: Cracker, + unlockedAchievements: any, + achievements: Achievement[] +): CheckedAchievements => { + if (type === Cracker.onlineFix) + return onlineFixMerge(unlockedAchievements, achievements); + if (type === Cracker.goldberg) + return goldbergUnlockedAchievements(unlockedAchievements, achievements); + return defaultMerge(unlockedAchievements, achievements); +}; + +const onlineFixMerge = ( + unlockedAchievements: any, + achievements: Achievement[] +): CheckedAchievements => { + const newUnlockedAchievements: Achievement[] = []; + + for (const achievement of achievements) { + if (achievement.achieved) continue; + + const unlockedAchievement = unlockedAchievements[achievement.id]; + + if (!unlockedAchievement) continue; + + achievement.achieved = Boolean( + unlockedAchievement?.achieved ?? achievement.achieved + ); + + achievement.unlockTime = + unlockedAchievement?.timestamp ?? achievement.unlockTime; + + if (achievement.achieved) { + newUnlockedAchievements.push(achievement); + } + } + + return { all: achievements, new: newUnlockedAchievements }; +}; + +const goldbergUnlockedAchievements = ( + unlockedAchievements: any, + achievements: Achievement[] +): CheckedAchievements => { + const newUnlockedAchievements: Achievement[] = []; + + for (const achievement of achievements) { + if (achievement.achieved) continue; + + const unlockedAchievement = unlockedAchievements[achievement.id]; + + if (!unlockedAchievement) continue; + + achievement.achieved = Boolean( + unlockedAchievement?.earned ?? achievement.achieved + ); + + achievement.unlockTime = + unlockedAchievement?.earned_time ?? achievement.unlockTime; + + if (achievement.achieved) { + newUnlockedAchievements.push(achievement); + } + } + return { all: achievements, new: newUnlockedAchievements }; +}; + +const defaultMerge = ( + unlockedAchievements: any, + achievements: Achievement[] +): CheckedAchievements => { + const newUnlockedAchievements: Achievement[] = []; + console.log("checkUnlockedAchievements"); + for (const achievement of achievements) { + if (achievement.achieved) continue; + + const unlockedAchievement = unlockedAchievements[achievement.id]; + + if (!unlockedAchievement) continue; + + achievement.achieved = Boolean( + unlockedAchievement?.Achieved ?? achievement.achieved + ); + + achievement.curProgress = + unlockedAchievement?.CurProgress ?? achievement.curProgress; + + achievement.maxProgress = + unlockedAchievement?.MaxProgress ?? achievement.maxProgress; + + achievement.unlockTime = + unlockedAchievement?.UnlockTime ?? achievement.unlockTime; + + if (achievement.achieved) { + newUnlockedAchievements.push(achievement); + } + } + console.log("newUnlocked: ", newUnlockedAchievements); + return { all: achievements, new: newUnlockedAchievements }; +}; diff --git a/src/main/events/achievements/util/parseAchievementFile.ts b/src/main/events/achievements/util/parseAchievementFile.ts new file mode 100644 index 00000000..1adf9512 --- /dev/null +++ b/src/main/events/achievements/util/parseAchievementFile.ts @@ -0,0 +1,57 @@ +import { existsSync, createReadStream, readFileSync } from "node:fs"; +import readline from "node:readline"; + +export const parseAchievementFile = async ( + filePath: string +): Promise => { + if (existsSync(filePath)) { + if (filePath.endsWith(".ini")) { + return iniParse(filePath); + } + + if (filePath.endsWith(".json")) { + return jsonParse(filePath); + } + } +}; + +const iniParse = async (filePath: string) => { + try { + const file = createReadStream(filePath); + + const lines = readline.createInterface({ + input: file, + crlfDelay: Infinity, + }); + + let objectName = ""; + const object: any = {}; + + for await (const line of lines) { + if (line.startsWith("###") || !line.length) continue; + + if (line.startsWith("[") && line.endsWith("]")) { + objectName = line.slice(1, -1); + object[objectName] = {}; + } else { + const [name, value] = line.split("="); + + const number = Number(value); + + object[objectName][name] = isNaN(number) ? value : number; + } + } + + return object; + } catch { + return null; + } +}; + +const jsonParse = (filePath: string) => { + try { + return JSON.parse(readFileSync(filePath, "utf-8")); + } catch { + return null; + } +}; diff --git a/src/main/knex-client.ts b/src/main/knex-client.ts index b6ec3e3d..6530e653 100644 --- a/src/main/knex-client.ts +++ b/src/main/knex-client.ts @@ -6,6 +6,7 @@ import { UpdateUserLanguage } from "./migrations/20240913213944_update_user_lang import { EnsureRepackUris } from "./migrations/20240915035339_ensure_repack_uris"; import { app } from "electron"; import { FixMissingColumns } from "./migrations/20240918001920_FixMissingColumns"; +import { CreateGameAchievement } from "./migrations/20240919030940_create_game_achievement"; export type HydraMigration = Knex.Migration & { name: string }; @@ -17,6 +18,7 @@ class MigrationSource implements Knex.MigrationSource { UpdateUserLanguage, EnsureRepackUris, FixMissingColumns, + CreateGameAchievement, ]); } getMigrationName(migration: HydraMigration): string { diff --git a/src/main/migrations/20240919030940_create_game_achievement.ts b/src/main/migrations/20240919030940_create_game_achievement.ts new file mode 100644 index 00000000..16d90fa7 --- /dev/null +++ b/src/main/migrations/20240919030940_create_game_achievement.ts @@ -0,0 +1,18 @@ +import type { HydraMigration } from "@main/knex-client"; +import type { Knex } from "knex"; + +export const CreateGameAchievement: HydraMigration = { + name: "CreateGameAchievement", + up: async (knex: Knex) => { + await knex.schema.createTable("game_achievement", (table) => { + table.increments("id").primary(); + table.integer("gameId").notNullable(); + table.text("achievements"); + table.foreign("gameId").references("game.id").onDelete("CASCADE"); + }); + }, + + down: async (knex: Knex) => { + await knex.schema.dropTable("game_achievement"); + }, +}; diff --git a/src/main/repository.ts b/src/main/repository.ts index 4464e775..4e5c115f 100644 --- a/src/main/repository.ts +++ b/src/main/repository.ts @@ -7,6 +7,7 @@ import { Repack, UserPreferences, UserAuth, + GameAchievement, } from "@main/entity"; export const gameRepository = dataSource.getRepository(Game); @@ -24,3 +25,6 @@ export const downloadSourceRepository = export const downloadQueueRepository = dataSource.getRepository(DownloadQueue); export const userAuthRepository = dataSource.getRepository(UserAuth); + +export const gameAchievementRepository = + dataSource.getRepository(GameAchievement); diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index 2a194bf2..50ef42a9 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -5,6 +5,10 @@ import { createGame, updateGamePlaytime } from "./library-sync"; import { GameRunning } from "@types"; import { PythonInstance } from "./download"; import { Game } from "@main/entity"; +import { + startGameAchievementObserver, + stopGameAchievementObserver, +} from "@main/events/achievements/game-achievements-observer"; export const gamesPlaytime = new Map< number, @@ -74,6 +78,8 @@ function onOpenGame(game: Game) { } else { createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {}); } + + startGameAchievementObserver(game.id); } function onTickGame(game: Game) { @@ -125,4 +131,6 @@ const onCloseGame = (game: Game) => { } else { createGame(game).catch(() => {}); } + + stopGameAchievementObserver(game.id); };