From 05652d9c1b33641bd9c85bae3cb16c7a40de1417 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:30:07 -0300 Subject: [PATCH] feat: refactor watcher --- .../achievements/achievement-watcher.ts | 6 +- .../check-unlocked-achievements.ts | 18 +++ .../find-steam-game-achivement-files.ts | 45 ++++++- .../game-achievements-observer.ts | 83 ++++++------- .../achievements/merge-achievements.ts | 11 +- .../achievements/parse-achievement-file.ts | 54 ++++++++- .../update-local-unlocked-achivements.ts | 24 ++-- src/main/services/hydra-api.ts | 111 ++++++++++-------- src/main/services/window-manager.ts | 1 + src/shared/constants.ts | 1 + 10 files changed, 229 insertions(+), 125 deletions(-) diff --git a/src/main/services/achievements/achievement-watcher.ts b/src/main/services/achievements/achievement-watcher.ts index 9d1564c0..a1d1a47a 100644 --- a/src/main/services/achievements/achievement-watcher.ts +++ b/src/main/services/achievements/achievement-watcher.ts @@ -1,5 +1,5 @@ import { gameRepository } from "@main/repository"; -import { startGameAchievementObserver } from "./game-achievements-observer"; +import { startGameAchievementObserver as searchForAchievements } from "./game-achievements-observer"; export const watchAchievements = async () => { const games = await gameRepository.find({ @@ -10,7 +10,5 @@ export const watchAchievements = async () => { if (games.length === 0) return; - for (const game of games) { - startGameAchievementObserver(game); - } + await searchForAchievements(games); }; diff --git a/src/main/services/achievements/check-unlocked-achievements.ts b/src/main/services/achievements/check-unlocked-achievements.ts index 71a80b2b..1fa2c3f5 100644 --- a/src/main/services/achievements/check-unlocked-achievements.ts +++ b/src/main/services/achievements/check-unlocked-achievements.ts @@ -8,6 +8,7 @@ export const checkUnlockedAchievements = ( if (type === Cracker.onlineFix) return onlineFixMerge(unlockedAchievements); if (type === Cracker.goldberg) return goldbergUnlockedAchievements(unlockedAchievements); + if (type == Cracker.generic) return genericMerge(unlockedAchievements); return defaultMerge(unlockedAchievements); }; @@ -62,3 +63,20 @@ const defaultMerge = (unlockedAchievements: any): UnlockedAchievement[] => { return newUnlockedAchievements; }; + +const genericMerge = (unlockedAchievements: any): UnlockedAchievement[] => { + const newUnlockedAchievements: UnlockedAchievement[] = []; + + for (const achievement of Object.keys(unlockedAchievements)) { + const unlockedAchievement = unlockedAchievements[achievement]; + + if (unlockedAchievement?.unlocked) { + newUnlockedAchievements.push({ + name: achievement, + unlockTime: unlockedAchievement.time, + }); + } + } + + return newUnlockedAchievements; +}; diff --git a/src/main/services/achievements/find-steam-game-achivement-files.ts b/src/main/services/achievements/find-steam-game-achivement-files.ts index e6ea05f5..7cc5bc6a 100644 --- a/src/main/services/achievements/find-steam-game-achivement-files.ts +++ b/src/main/services/achievements/find-steam-game-achivement-files.ts @@ -3,6 +3,7 @@ import fs from "node:fs"; import { app } from "electron"; import type { AchievementFile } from "@types"; import { Cracker } from "@shared"; +import { Game } from "@main/entity"; //TODO: change to a automatized method const publicDir = path.join("C:", "Users", "Public", "Documents"); @@ -17,6 +18,8 @@ const addGame = ( ) => { const filePath = path.join(achievementPath, objectId, ...fileLocation); + if (!fs.existsSync(filePath)) return; + const achivementFile = { type, filePath, @@ -35,12 +38,13 @@ const getObjectIdsInFolder = (path: string) => { return []; }; -export const findSteamGameAchievementFiles = (objectId: string) => { +export const findSteamGameAchievementFiles = (game: Game) => { const crackers = [ Cracker.codex, Cracker.goldberg, Cracker.rune, Cracker.onlineFix, + Cracker.generic, ]; const achievementFiles: AchievementFile[] = []; @@ -54,20 +58,51 @@ export const findSteamGameAchievementFiles = (objectId: string) => { } else if (cracker === Cracker.goldberg) { achievementPath = path.join(appData, "Goldberg SteamEmu Saves"); fileLocation = ["achievements.json"]; + } else if (cracker === Cracker.generic) { + achievementPath = path.join(publicDir, Cracker.generic); + fileLocation = ["user_stats.ini"]; } else { achievementPath = path.join(publicDir, "Steam", cracker); fileLocation = ["achievements.ini"]; } - achievementFiles.push({ - type: cracker, - filePath: path.join(achievementPath, objectId, ...fileLocation), - }); + const filePath = path.join(achievementPath, game.objectID, ...fileLocation); + + if (fs.existsSync(filePath)) { + achievementFiles.push({ + type: cracker, + filePath: path.join(achievementPath, game.objectID, ...fileLocation), + }); + } } return achievementFiles; }; +export const findAchievementFileInExecutableDirectory = ( + game: Game +): AchievementFile | null => { + if (!game.executablePath) { + return null; + } + + const steamDataPath = path.join( + game.executablePath, + "..", + "SteamData", + "user_stats.ini" + ); + + if (fs.existsSync(steamDataPath)) { + return { + type: Cracker.generic, + filePath: steamDataPath, + }; + } + + return null; +}; + export const findAllSteamGameAchievementFiles = () => { const gameAchievementFiles = new Map(); diff --git a/src/main/services/achievements/game-achievements-observer.ts b/src/main/services/achievements/game-achievements-observer.ts index 3a0125b5..cdc2178b 100644 --- a/src/main/services/achievements/game-achievements-observer.ts +++ b/src/main/services/achievements/game-achievements-observer.ts @@ -1,21 +1,22 @@ -import { watch } from "node:fs/promises"; import { checkUnlockedAchievements } from "./check-unlocked-achievements"; import { parseAchievementFile } from "./parse-achievement-file"; import { Game } from "@main/entity"; import { mergeAchievements } from "./merge-achievements"; import fs from "node:fs"; -import { findSteamGameAchievementFiles } from "./find-steam-game-achivement-files"; +import { + findAchievementFileInExecutableDirectory, + findAllSteamGameAchievementFiles, +} from "./find-steam-game-achivement-files"; import type { AchievementFile } from "@types"; import { logger } from "../logger"; -type GameAchievementObserver = { - [id: number]: AbortController; -}; - -const gameAchievementObserver: GameAchievementObserver = {}; +const fileStats: Map = new Map(); const processAchievementFile = async (game: Game, file: AchievementFile) => { - const localAchievementFile = await parseAchievementFile(file.filePath); + const localAchievementFile = await parseAchievementFile( + file.filePath, + file.type + ); logger.log("Parsed achievements file", file.filePath, localAchievementFile); if (localAchievementFile) { @@ -36,46 +37,48 @@ const processAchievementFile = async (game: Game, file: AchievementFile) => { } }; -const startFileWatch = async (game: Game, file: AchievementFile) => { +const compareFile = async (game: Game, file: AchievementFile) => { try { + const stat = fs.statSync(file.filePath); + const currentFileStat = fileStats.get(file.filePath); + fileStats.set(file.filePath, stat.mtimeMs); + + if (!currentFileStat || currentFileStat === stat.mtimeMs) { + return; + } + + logger.log( + "Detected change in file", + file.filePath, + stat.mtimeMs, + fileStats.get(file.filePath) + ); await processAchievementFile(game, file); - - const watcher = watch(file.filePath); - - for await (const event of watcher) { - if (event.eventType === "change") { - logger.log("Detected change in file", file.filePath); - await processAchievementFile(game, file); - } - } - } catch (err: any) { - if (err?.name === "AbortError") return; - logger.error("Failed to watch file", file.filePath, err); - } + } catch (err) {} }; -export const startGameAchievementObserver = async (game: Game) => { - if (game.shop !== "steam") return; - if (gameAchievementObserver[game.id]) return; +export const startGameAchievementObserver = async (games: Game[]) => { + const achievementFiles = findAllSteamGameAchievementFiles(); - const achievementFiles = findSteamGameAchievementFiles(game.objectID); + for (const game of games) { + const gameAchievementFiles = achievementFiles.get(game.objectID) || []; + const achievementFileInsideDirectory = + findAchievementFileInExecutableDirectory(game); - logger.log( - "Achievements files to observe for:", - game.title, - achievementFiles - ); - - for (const file of achievementFiles) { - if (!fs.existsSync(file.filePath)) { - continue; + if (achievementFileInsideDirectory) { + gameAchievementFiles.push(achievementFileInsideDirectory); } - if (!gameAchievementObserver[game.id]) { - const abortController = new AbortController(); - gameAchievementObserver[game.id] = abortController; - } + if (!gameAchievementFiles.length) continue; - startFileWatch(game, file); + logger.log( + "Achievements files to observe for:", + game.title, + gameAchievementFiles + ); + + for (const file of gameAchievementFiles) { + compareFile(game, file); + } } }; diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index daad6615..31ffc767 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -52,14 +52,12 @@ export const mergeAchievements = async ( const newAchievements = achievements .filter((achievement) => { return !unlockedAchievements.some((localAchievement) => { - return ( - localAchievement.name.toUpperCase() === achievement.name.toUpperCase() - ); + return localAchievement.name === achievement.name.toUpperCase(); }); }) .map((achievement) => { return { - ...achievement, + name: achievement.name.toUpperCase(), unlockTime: achievement.unlockTime * 1000, }; }); @@ -69,10 +67,7 @@ export const mergeAchievements = async ( .map((achievement) => { return JSON.parse(localGameAchievement?.achievements || "[]").find( (steamAchievement) => { - return ( - achievement.name.toUpperCase() === - steamAchievement.name.toUpperCase() - ); + return achievement.name === steamAchievement.name; } ); }) diff --git a/src/main/services/achievements/parse-achievement-file.ts b/src/main/services/achievements/parse-achievement-file.ts index 64aa120a..232726e2 100644 --- a/src/main/services/achievements/parse-achievement-file.ts +++ b/src/main/services/achievements/parse-achievement-file.ts @@ -1,10 +1,16 @@ +import { Cracker } from "@shared"; import { existsSync, createReadStream, readFileSync } from "node:fs"; import readline from "node:readline"; export const parseAchievementFile = async ( - filePath: string + filePath: string, + type: Cracker ): Promise => { if (existsSync(filePath)) { + if (type === Cracker.generic) { + return genericParse(filePath); + } + if (filePath.endsWith(".ini")) { return iniParse(filePath); } @@ -15,6 +21,42 @@ export const parseAchievementFile = async ( } }; +const genericParse = async (filePath: string) => { + try { + const file = createReadStream(filePath); + + const lines = readline.createInterface({ + input: file, + crlfDelay: Infinity, + }); + + const object: Record> = {}; + + for await (const line of lines) { + if (line.startsWith("###") || !line.length) continue; + + if (line.startsWith("[") && line.endsWith("]")) { + continue; + } + + const [name, ...value] = line.split(" = "); + const objectName = name.slice(1, -1); + object[objectName] = {}; + + const joinedValue = value.join("=").slice(1, -1); + + for (const teste of joinedValue.split(",")) { + const [name, value] = teste.split("="); + object[objectName][name.trim()] = value; + } + } + console.log(object); + return object; + } catch { + return null; + } +}; + const iniParse = async (filePath: string) => { try { const file = createReadStream(filePath); @@ -34,11 +76,15 @@ const iniParse = async (filePath: string) => { objectName = line.slice(1, -1); object[objectName] = {}; } else { - const [name, value] = line.split("="); + const [name, ...value] = line.split("="); + console.log(line); + console.log(name, value); - const number = Number(value); + const joinedValue = value.join("").trim(); - object[objectName][name] = isNaN(number) ? value : number; + const number = Number(joinedValue); + + object[objectName][name.trim()] = isNaN(number) ? joinedValue : number; } } diff --git a/src/main/services/achievements/update-local-unlocked-achivements.ts b/src/main/services/achievements/update-local-unlocked-achivements.ts index 35e72a4e..6751eab3 100644 --- a/src/main/services/achievements/update-local-unlocked-achivements.ts +++ b/src/main/services/achievements/update-local-unlocked-achivements.ts @@ -10,9 +10,11 @@ import type { UnlockedAchievement } from "@types"; import { getGameAchievementData } from "./get-game-achievement-data"; export const updateAllLocalUnlockedAchievements = async () => { - const gameAchievementFiles = findAllSteamGameAchievementFiles(); + const gameAchievementFilesMap = findAllSteamGameAchievementFiles(); + + for (const objectId of gameAchievementFilesMap.keys()) { + const gameAchievementFiles = gameAchievementFilesMap.get(objectId)!; - for (const objectId of gameAchievementFiles.keys()) { const [game, localAchievements] = await Promise.all([ gameRepository.findOne({ where: { objectID: objectId, shop: "steam", isDeleted: false }, @@ -24,11 +26,7 @@ export const updateAllLocalUnlockedAchievements = async () => { if (!game) continue; - console.log( - "Achievements files for", - game.title, - gameAchievementFiles.get(objectId) - ); + console.log("Achievements files for", game.title, gameAchievementFiles); if (!localAchievements || !localAchievements.achievements) { await getGameAchievementData(objectId, "steam") @@ -47,9 +45,10 @@ export const updateAllLocalUnlockedAchievements = async () => { const unlockedAchievements: UnlockedAchievement[] = []; - for (const achievementFile of gameAchievementFiles.get(objectId)!) { + for (const achievementFile of gameAchievementFiles) { const localAchievementFile = await parseAchievementFile( - achievementFile.filePath + achievementFile.filePath, + achievementFile.type ); if (localAchievementFile) { @@ -70,8 +69,6 @@ export const updateLocalUnlockedAchivements = async ( publishNotification: boolean, objectId: string ) => { - const gameAchievementFiles = findSteamGameAchievementFiles(objectId); - const [game, localAchievements] = await Promise.all([ gameRepository.findOne({ where: { objectID: objectId, shop: "steam", isDeleted: false }, @@ -83,6 +80,8 @@ export const updateLocalUnlockedAchivements = async ( if (!game) return; + const gameAchievementFiles = findSteamGameAchievementFiles(game); + console.log("Achievements files for", game.title, gameAchievementFiles); if (!localAchievements || !localAchievements.achievements) { @@ -104,7 +103,8 @@ export const updateLocalUnlockedAchivements = async ( for (const achievementFile of gameAchievementFiles) { const localAchievementFile = await parseAchievementFile( - achievementFile.filePath + achievementFile.filePath, + achievementFile.type ); if (localAchievementFile) { diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index ac3d153a..d62bafcb 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -17,6 +17,7 @@ export class HydraApi { private static instance: AxiosInstance; private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes + private static readonly ADD_LOG_INTERCEPTOR = true; private static secondsToMilliseconds = (seconds: number) => seconds * 1000; @@ -87,60 +88,66 @@ export class HydraApi { headers: { "User-Agent": `Hydra Launcher v${appVersion}` }, }); - this.instance.interceptors.request.use( - (request) => { - logger.log(" ---- REQUEST -----"); - const data = Array.isArray(request.data) - ? request.data - : omit(request.data, ["refreshToken"]); - logger.log(request.method, request.url, request.params, data); - return request; - }, - (error) => { - logger.error("request error", error); - return Promise.reject(error); - } - ); - - this.instance.interceptors.response.use( - (response) => { - logger.log(" ---- RESPONSE -----"); - const data = Array.isArray(response.data) - ? response.data - : omit(response.data, ["username", "accessToken", "refreshToken"]); - logger.log( - response.status, - response.config.method, - response.config.url, - data - ); - return response; - }, - (error) => { - logger.error(" ---- RESPONSE ERROR -----"); - - const { config } = error; - - logger.error( - config.method, - config.baseURL, - config.url, - config.headers, - config.data - ); - - if (error.response) { - logger.error("Response", error.response.status, error.response.data); - } else if (error.request) { - logger.error("Request", error.request); - } else { - logger.error("Error", error.message); + if (this.ADD_LOG_INTERCEPTOR) { + this.instance.interceptors.request.use( + (request) => { + logger.log(" ---- REQUEST -----"); + const data = Array.isArray(request.data) + ? request.data + : omit(request.data, ["refreshToken"]); + logger.log(request.method, request.url, request.params, data); + return request; + }, + (error) => { + logger.error("request error", error); + return Promise.reject(error); } + ); - logger.error(" ----- END RESPONSE ERROR -------"); - return Promise.reject(error); - } - ); + this.instance.interceptors.response.use( + (response) => { + logger.log(" ---- RESPONSE -----"); + const data = Array.isArray(response.data) + ? response.data + : omit(response.data, ["username", "accessToken", "refreshToken"]); + logger.log( + response.status, + response.config.method, + response.config.url, + data + ); + return response; + }, + (error) => { + logger.error(" ---- RESPONSE ERROR -----"); + + const { config } = error; + + logger.error( + config.method, + config.baseURL, + config.url, + config.headers, + config.data + ); + + if (error.response) { + logger.error( + "Response", + error.response.status, + error.response.data + ); + } else if (error.request) { + logger.error("Request", error.request); + } else { + logger.error("Error", error.message); + } + + logger.error(" ----- END RESPONSE ERROR -------"); + return Promise.reject(error); + } + ); + } const userAuth = await userAuthRepository.findOne({ where: { id: 1 }, diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 1f66a64d..70ad783d 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -102,6 +102,7 @@ export class WindowManager { this.notificationWindow = new BrowserWindow({ transparent: true, maximizable: false, + autoHideMenuBar: true, minimizable: false, focusable: true, skipTaskbar: true, diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 15661959..79f0e6bd 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -29,4 +29,5 @@ export enum Cracker { rune = "RUNE", onlineFix = "OnlineFix", goldberg = "Goldberg", + generic = "Generic", }