From 7cddcd8147731e38381b62c5bde8acc01e419492 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:16:30 -0300 Subject: [PATCH 1/3] feat: refactoring code --- src/main/events/catalogue/get-game-stats.ts | 3 +- .../events/library/add-game-to-library.ts | 2 +- ...server.ts => achievement-file-observer.ts} | 41 ++-- .../achievements/achievement-watcher.ts | 2 +- .../check-unlocked-achievements.ts | 82 -------- .../achievements/find-achivement-files.ts | 115 ++++++++++++ .../find-steam-game-achivement-files.ts | 138 -------------- .../achievements/parse-achievement-file.ts | 177 ++++++++++++------ .../update-local-unlocked-achivements.ts | 44 ++--- src/shared/constants.ts | 3 +- 10 files changed, 273 insertions(+), 334 deletions(-) rename src/main/services/achievements/{game-achievements-observer.ts => achievement-file-observer.ts} (62%) delete mode 100644 src/main/services/achievements/check-unlocked-achievements.ts create mode 100644 src/main/services/achievements/find-achivement-files.ts delete mode 100644 src/main/services/achievements/find-steam-game-achivement-files.ts diff --git a/src/main/events/catalogue/get-game-stats.ts b/src/main/events/catalogue/get-game-stats.ts index 0d03b42d..b64a42ce 100644 --- a/src/main/events/catalogue/get-game-stats.ts +++ b/src/main/events/catalogue/get-game-stats.ts @@ -9,12 +9,11 @@ const getGameStats = async ( objectId: string, shop: GameShop ) => { - const response = await HydraApi.get( + return HydraApi.get( `/games/stats`, { objectId, shop }, { needsAuth: false } ); - return response; }; registerEvent("getGameStats", getGameStats); diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 69045d46..c6ec63c0 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -44,7 +44,7 @@ const addGameToLibrary = async ( }); } - updateLocalUnlockedAchivements(true, objectID); + updateLocalUnlockedAchivements(objectID); const game = await gameRepository.findOne({ where: { objectID } }); diff --git a/src/main/services/achievements/game-achievements-observer.ts b/src/main/services/achievements/achievement-file-observer.ts similarity index 62% rename from src/main/services/achievements/game-achievements-observer.ts rename to src/main/services/achievements/achievement-file-observer.ts index fd582e91..d8610ec8 100644 --- a/src/main/services/achievements/game-achievements-observer.ts +++ b/src/main/services/achievements/achievement-file-observer.ts @@ -1,39 +1,34 @@ -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 { findAchievementFileInExecutableDirectory, - findAllSteamGameAchievementFiles, -} from "./find-steam-game-achivement-files"; + findAllAchievementFiles, +} from "./find-achivement-files"; import type { AchievementFile } from "@types"; import { logger } from "../logger"; const fileStats: Map = new Map(); -const processAchievementFile = async (game: Game, file: AchievementFile) => { - const localAchievementFile = await parseAchievementFile( +const processAchievementFileDiff = async ( + game: Game, + file: AchievementFile +) => { + const unlockedAchievements = await parseAchievementFile( file.filePath, file.type ); - logger.log("Parsed achievements file", file.filePath, localAchievementFile); - if (localAchievementFile) { - const unlockedAchievements = checkUnlockedAchievements( - file.type, - localAchievementFile - ); - logger.log("Achievements from file", file.filePath, unlockedAchievements); + logger.log("Achievements from file", file.filePath, unlockedAchievements); - if (unlockedAchievements.length) { - return mergeAchievements( - game.objectID, - game.shop, - unlockedAchievements, - true - ); - } + if (unlockedAchievements.length) { + return mergeAchievements( + game.objectID, + game.shop, + unlockedAchievements, + true + ); } }; @@ -53,14 +48,14 @@ const compareFile = async (game: Game, file: AchievementFile) => { stat.mtimeMs, fileStats.get(file.filePath) ); - await processAchievementFile(game, file); + await processAchievementFileDiff(game, file); } catch (err) { fileStats.set(file.filePath, -1); } }; -export const startGameAchievementObserver = async (games: Game[]) => { - const achievementFiles = findAllSteamGameAchievementFiles(); +export const checkAchievementFileChange = async (games: Game[]) => { + const achievementFiles = findAllAchievementFiles(); for (const game of games) { const gameAchievementFiles = achievementFiles.get(game.objectID) || []; diff --git a/src/main/services/achievements/achievement-watcher.ts b/src/main/services/achievements/achievement-watcher.ts index a1d1a47a..d88771a8 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 as searchForAchievements } from "./game-achievements-observer"; +import { checkAchievementFileChange as searchForAchievements } from "./achievement-file-observer"; export const watchAchievements = async () => { const games = await gameRepository.find({ diff --git a/src/main/services/achievements/check-unlocked-achievements.ts b/src/main/services/achievements/check-unlocked-achievements.ts deleted file mode 100644 index 608ff5bd..00000000 --- a/src/main/services/achievements/check-unlocked-achievements.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Cracker } from "@shared"; -import type { UnlockedAchievement } from "@types"; - -export const checkUnlockedAchievements = ( - type: Cracker, - unlockedAchievements: any -): UnlockedAchievement[] => { - if (type === Cracker.onlineFix) return onlineFixMerge(unlockedAchievements); - if (type === Cracker.goldberg || type === Cracker.goldberg2) - return goldbergUnlockedAchievements(unlockedAchievements); - if (type == Cracker.generic) return genericMerge(unlockedAchievements); - return defaultMerge(unlockedAchievements); -}; - -const onlineFixMerge = (unlockedAchievements: any): UnlockedAchievement[] => { - const parsedUnlockedAchievements: UnlockedAchievement[] = []; - - for (const achievement of Object.keys(unlockedAchievements)) { - const unlockedAchievement = unlockedAchievements[achievement]; - - if (unlockedAchievement?.achieved) { - parsedUnlockedAchievements.push({ - name: achievement, - unlockTime: unlockedAchievement.timestamp, - }); - } - } - - return parsedUnlockedAchievements; -}; - -const goldbergUnlockedAchievements = ( - unlockedAchievements: any -): UnlockedAchievement[] => { - const newUnlockedAchievements: UnlockedAchievement[] = []; - - for (const achievement of Object.keys(unlockedAchievements)) { - const unlockedAchievement = unlockedAchievements[achievement]; - - if (unlockedAchievement?.earned) { - newUnlockedAchievements.push({ - name: achievement, - unlockTime: unlockedAchievement.earned_time, - }); - } - } - return newUnlockedAchievements; -}; - -const defaultMerge = (unlockedAchievements: any): UnlockedAchievement[] => { - const newUnlockedAchievements: UnlockedAchievement[] = []; - - for (const achievement of Object.keys(unlockedAchievements)) { - const unlockedAchievement = unlockedAchievements[achievement]; - - if (unlockedAchievement?.Achieved) { - newUnlockedAchievements.push({ - name: achievement, - unlockTime: unlockedAchievement.UnlockTime, - }); - } - } - - 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-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts new file mode 100644 index 00000000..73122b9e --- /dev/null +++ b/src/main/services/achievements/find-achivement-files.ts @@ -0,0 +1,115 @@ +import path from "node:path"; +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"); +const programData = path.join("C:", "ProgramData"); +const appData = app.getPath("appData"); + +const crackers = [ + Cracker.codex, + Cracker.goldberg, + Cracker.goldberg2, + Cracker.rune, + Cracker.onlineFix, + Cracker.userstats, + Cracker.rld, +]; + +const getPathFromCracker = (cracker: Cracker) => { + let folderPath: string; + let fileLocation: string[]; + + if (cracker === Cracker.onlineFix) { + folderPath = path.join(publicDir, Cracker.onlineFix); + fileLocation = ["Stats", "Achievements.ini"]; + } else if (cracker === Cracker.goldberg) { + folderPath = path.join(appData, "Goldberg SteamEmu Saves"); + fileLocation = ["achievements.json"]; + } else if (cracker === Cracker.goldberg2) { + folderPath = path.join(appData, "GSE Saves"); + fileLocation = ["achievements.json"]; + } else if (cracker === Cracker.rld) { + folderPath = path.join(programData, Cracker.rld); + fileLocation = ["achievements.ini"]; + } else { + folderPath = path.join(publicDir, "Steam", cracker); + fileLocation = ["achievements.ini"]; + } + + return { folderPath, fileLocation }; +}; + +export const findAchievementFiles = (game: Game) => { + const achievementFiles: AchievementFile[] = []; + + for (const cracker of crackers) { + const { folderPath, fileLocation } = getPathFromCracker(cracker); + + const filePath = path.join(folderPath, game.objectID, ...fileLocation); + + if (fs.existsSync(filePath)) { + achievementFiles.push({ + type: cracker, + filePath: path.join(folderPath, 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" + ); + + return { + type: Cracker.userstats, + filePath: steamDataPath, + }; +}; + +export const findAllAchievementFiles = () => { + const gameAchievementFiles = new Map(); + + for (const cracker of crackers) { + const { folderPath, fileLocation } = getPathFromCracker(cracker); + + if (!fs.existsSync(folderPath)) { + return gameAchievementFiles; + } + + const objectIds = fs.readdirSync(folderPath); + + for (const objectId of objectIds) { + const filePath = path.join(folderPath, objectId, ...fileLocation); + + if (!fs.existsSync(filePath)) continue; + + const achivementFile = { + type: cracker, + filePath, + }; + + gameAchievementFiles.get(objectId) + ? gameAchievementFiles.get(objectId)!.push(achivementFile) + : gameAchievementFiles.set(objectId, [achivementFile]); + } + } + + return gameAchievementFiles; +}; diff --git a/src/main/services/achievements/find-steam-game-achivement-files.ts b/src/main/services/achievements/find-steam-game-achivement-files.ts deleted file mode 100644 index 980f0ae8..00000000 --- a/src/main/services/achievements/find-steam-game-achivement-files.ts +++ /dev/null @@ -1,138 +0,0 @@ -import path from "node:path"; -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"); -const appData = app.getPath("appData"); - -const crackers = [ - Cracker.codex, - Cracker.goldberg, - Cracker.goldberg2, - Cracker.rune, - Cracker.onlineFix, - Cracker.generic, -]; - -const addGame = ( - achievementFiles: Map, - achievementPath: string, - objectId: string, - fileLocation: string[], - type: Cracker -) => { - const filePath = path.join(achievementPath, objectId, ...fileLocation); - - if (!fs.existsSync(filePath)) return; - - const achivementFile = { - type, - filePath, - }; - - achievementFiles.get(objectId) - ? achievementFiles.get(objectId)!.push(achivementFile) - : achievementFiles.set(objectId, [achivementFile]); -}; - -const getObjectIdsInFolder = (path: string) => { - if (fs.existsSync(path)) { - return fs.readdirSync(path); - } - - return []; -}; - -export const findSteamGameAchievementFiles = (game: Game) => { - const achievementFiles: AchievementFile[] = []; - 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 if (cracker === Cracker.goldberg2) { - achievementPath = path.join(appData, "GSE Saves"); - fileLocation = ["achievements.json"]; - } else { - achievementPath = path.join(publicDir, "Steam", cracker); - fileLocation = ["achievements.ini"]; - } - - 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" - ); - - return { - type: Cracker.generic, - filePath: steamDataPath, - }; -}; - -export const findAllSteamGameAchievementFiles = () => { - const gameAchievementFiles = new Map(); - - 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.goldberg2) { - achievementPath = path.join(appData, "GSE Saves"); - fileLocation = ["achievements.json"]; - } 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"]; - } - - const objectIds = getObjectIdsInFolder(achievementPath); - - for (const objectId of objectIds) { - addGame( - gameAchievementFiles, - achievementPath, - objectId, - fileLocation, - cracker - ); - } - } - - return gameAchievementFiles; -}; diff --git a/src/main/services/achievements/parse-achievement-file.ts b/src/main/services/achievements/parse-achievement-file.ts index 232726e2..c00c5d87 100644 --- a/src/main/services/achievements/parse-achievement-file.ts +++ b/src/main/services/achievements/parse-achievement-file.ts @@ -1,60 +1,35 @@ import { Cracker } from "@shared"; +import { UnlockedAchievement } from "@types"; import { existsSync, createReadStream, readFileSync } from "node:fs"; import readline from "node:readline"; export const parseAchievementFile = async ( filePath: string, type: Cracker -): Promise => { - if (existsSync(filePath)) { - if (type === Cracker.generic) { - return genericParse(filePath); - } +): Promise => { + if (!existsSync(filePath)) return []; - if (filePath.endsWith(".ini")) { - return iniParse(filePath); - } - - if (filePath.endsWith(".json")) { - return jsonParse(filePath); - } + if (type === Cracker.onlineFix) { + const parsed = await iniParse(filePath); + return processOnlineFix(parsed); } -}; - -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; + if (type === Cracker.goldberg || type === Cracker.goldberg2) { + const parsed = await jsonParse(filePath); + return processGoldberg(parsed); } + + if (type == Cracker.userstats) { + const parsed = await iniParse(filePath); + return processUserStats(parsed); + } + + if (type == Cracker.rld) { + const parsed = await iniParse(filePath); + return processRld(parsed); + } + + const parsed = await iniParse(filePath); + return processDefault(parsed); }; const iniParse = async (filePath: string) => { @@ -77,17 +52,11 @@ const iniParse = async (filePath: string) => { object[objectName] = {}; } else { const [name, ...value] = line.split("="); - console.log(line); - console.log(name, value); - - const joinedValue = value.join("").trim(); - - const number = Number(joinedValue); - - object[objectName][name.trim()] = isNaN(number) ? joinedValue : number; + object[objectName][name.trim()] = value.join("").trim(); } } + console.log("Parsed ini", object); return object; } catch { return null; @@ -101,3 +70,101 @@ const jsonParse = (filePath: string) => { return null; } }; + +const processOnlineFix = (unlockedAchievements: any): UnlockedAchievement[] => { + const parsedUnlockedAchievements: UnlockedAchievement[] = []; + + for (const achievement of Object.keys(unlockedAchievements)) { + const unlockedAchievement = unlockedAchievements[achievement]; + + if (unlockedAchievement?.achieved) { + parsedUnlockedAchievements.push({ + name: achievement, + unlockTime: unlockedAchievement.timestamp, + }); + } + } + + return parsedUnlockedAchievements; +}; + +const processGoldberg = (unlockedAchievements: any): UnlockedAchievement[] => { + const newUnlockedAchievements: UnlockedAchievement[] = []; + + for (const achievement of Object.keys(unlockedAchievements)) { + const unlockedAchievement = unlockedAchievements[achievement]; + + if (unlockedAchievement?.earned) { + newUnlockedAchievements.push({ + name: achievement, + unlockTime: unlockedAchievement.earned_time, + }); + } + } + return newUnlockedAchievements; +}; + +const processDefault = (unlockedAchievements: any): UnlockedAchievement[] => { + const newUnlockedAchievements: UnlockedAchievement[] = []; + + for (const achievement of Object.keys(unlockedAchievements)) { + const unlockedAchievement = unlockedAchievements[achievement]; + + if (unlockedAchievement?.Achieved) { + newUnlockedAchievements.push({ + name: achievement, + unlockTime: unlockedAchievement.UnlockTime, + }); + } + } + + return newUnlockedAchievements; +}; + +const processRld = (unlockedAchievements: any): UnlockedAchievement[] => { + const newUnlockedAchievements: UnlockedAchievement[] = []; + + for (const achievement of Object.keys(unlockedAchievements)) { + if (achievement === "Steam") continue; + + const unlockedAchievement = unlockedAchievements[achievement]; + + if (unlockedAchievement?.State) { + newUnlockedAchievements.push({ + name: achievement, + unlockTime: new DataView( + new Uint8Array( + Buffer.from(unlockedAchievement.Time.toString(), "hex") + ).buffer + ).getUint32(0, true), + }); + } + } + + return newUnlockedAchievements; +}; + +const processUserStats = (unlockedAchievements: any): UnlockedAchievement[] => { + const newUnlockedAchievements: UnlockedAchievement[] = []; + + const achievements = unlockedAchievements["ACHIEVEMENTS"]; + + if (!achievements) return []; + + for (const achievement of Object.keys(achievements)) { + const unlockedAchievement = achievements[achievement]; + + const unlockTime = Number( + unlockedAchievement.slice(1, -1).replace("unlocked = true, time = ", "") + ); + + if (!isNaN(unlockTime)) { + newUnlockedAchievements.push({ + name: achievement, + unlockTime: unlockTime, + }); + } + } + + return newUnlockedAchievements; +}; diff --git a/src/main/services/achievements/update-local-unlocked-achivements.ts b/src/main/services/achievements/update-local-unlocked-achivements.ts index 6751eab3..16ed0ae2 100644 --- a/src/main/services/achievements/update-local-unlocked-achivements.ts +++ b/src/main/services/achievements/update-local-unlocked-achivements.ts @@ -1,16 +1,15 @@ import { gameAchievementRepository, gameRepository } from "@main/repository"; import { - findAllSteamGameAchievementFiles, - findSteamGameAchievementFiles, -} from "./find-steam-game-achivement-files"; + findAllAchievementFiles, + findAchievementFiles, +} from "./find-achivement-files"; import { parseAchievementFile } from "./parse-achievement-file"; -import { checkUnlockedAchievements } from "./check-unlocked-achievements"; import { mergeAchievements } from "./merge-achievements"; import type { UnlockedAchievement } from "@types"; import { getGameAchievementData } from "./get-game-achievement-data"; export const updateAllLocalUnlockedAchievements = async () => { - const gameAchievementFilesMap = findAllSteamGameAchievementFiles(); + const gameAchievementFilesMap = findAllAchievementFiles(); for (const objectId of gameAchievementFilesMap.keys()) { const gameAchievementFiles = gameAchievementFilesMap.get(objectId)!; @@ -26,8 +25,6 @@ export const updateAllLocalUnlockedAchievements = async () => { if (!game) continue; - console.log("Achievements files for", game.title, gameAchievementFiles); - if (!localAchievements || !localAchievements.achievements) { await getGameAchievementData(objectId, "steam") .then((achievements) => { @@ -46,18 +43,13 @@ export const updateAllLocalUnlockedAchievements = async () => { const unlockedAchievements: UnlockedAchievement[] = []; for (const achievementFile of gameAchievementFiles) { - const localAchievementFile = await parseAchievementFile( + const parsedAchievements = await parseAchievementFile( achievementFile.filePath, achievementFile.type ); - - if (localAchievementFile) { - unlockedAchievements.push( - ...checkUnlockedAchievements( - achievementFile.type, - localAchievementFile - ) - ); + console.log("Parsed for", game.title, parsedAchievements); + if (parsedAchievements.length) { + unlockedAchievements.push(...parsedAchievements); } } @@ -65,10 +57,7 @@ export const updateAllLocalUnlockedAchievements = async () => { } }; -export const updateLocalUnlockedAchivements = async ( - publishNotification: boolean, - objectId: string -) => { +export const updateLocalUnlockedAchivements = async (objectId: string) => { const [game, localAchievements] = await Promise.all([ gameRepository.findOne({ where: { objectID: objectId, shop: "steam", isDeleted: false }, @@ -80,7 +69,7 @@ export const updateLocalUnlockedAchivements = async ( if (!game) return; - const gameAchievementFiles = findSteamGameAchievementFiles(game); + const gameAchievementFiles = findAchievementFiles(game); console.log("Achievements files for", game.title, gameAchievementFiles); @@ -107,17 +96,10 @@ export const updateLocalUnlockedAchivements = async ( achievementFile.type ); - if (localAchievementFile) { - unlockedAchievements.push( - ...checkUnlockedAchievements(achievementFile.type, localAchievementFile) - ); + if (localAchievementFile.length) { + unlockedAchievements.push(...localAchievementFile); } } - mergeAchievements( - objectId, - "steam", - unlockedAchievements, - publishNotification - ); + mergeAchievements(objectId, "steam", unlockedAchievements, false); }; diff --git a/src/shared/constants.ts b/src/shared/constants.ts index a92ea029..62c51418 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -30,5 +30,6 @@ export enum Cracker { onlineFix = "OnlineFix", goldberg = "Goldberg", goldberg2 = "Goldberg2", - generic = "Generic", + userstats = "user_stats", + rld = "RLD!", } From 7e2d9316f30d2d274fe02f674c8acf3d223d1d98 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Thu, 3 Oct 2024 19:13:00 -0300 Subject: [PATCH 2/3] feat: achievement animation --- src/main/services/window-manager.ts | 8 +- .../src/pages/achievement/achievement.css.ts | 44 ++++++++++ .../src/pages/achievement/achievement.tsx | 83 +++++++++++++------ 3 files changed, 105 insertions(+), 30 deletions(-) create mode 100644 src/renderer/src/pages/achievement/achievement.css.ts diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 4ee747da..76a5d830 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -107,10 +107,10 @@ export class WindowManager { focusable: false, skipTaskbar: true, frame: false, - width: 240, - height: 60, - x: 25, - y: 25, + width: 350, + height: 104, + x: 0, + y: 0, webPreferences: { preload: path.join(__dirname, "../preload/index.mjs"), sandbox: false, diff --git a/src/renderer/src/pages/achievement/achievement.css.ts b/src/renderer/src/pages/achievement/achievement.css.ts new file mode 100644 index 00000000..d5cb180b --- /dev/null +++ b/src/renderer/src/pages/achievement/achievement.css.ts @@ -0,0 +1,44 @@ +import { recipe } from "@vanilla-extract/recipes"; +import { vars } from "../../theme.css"; +import { keyframes, style } from "@vanilla-extract/css"; + +const animationIn = keyframes({ + "0%": { transform: `translateY(-240px)` }, + "100%": { transform: "translateY(0)" }, +}); + +const animationOut = keyframes({ + "0%": { transform: `translateY(0)` }, + "100%": { transform: "translateY(-240px)" }, +}); + +export const container = recipe({ + base: { + marginTop: "24px", + marginLeft: "24px", + animationDuration: "1.0s", + height: "60px", + display: "flex", + }, + variants: { + closing: { + true: { + animationName: animationOut, + transform: "translateY(-240px)", + }, + false: { + animationName: animationIn, + transform: "translateY(0)", + }, + }, + }, +}); + +export const content = style({ + display: "flex", + flexDirection: "row", + gap: "8px", + alignItems: "center", + background: vars.color.background, + paddingRight: "8px", +}); diff --git a/src/renderer/src/pages/achievement/achievement.tsx b/src/renderer/src/pages/achievement/achievement.tsx index 27559e38..0ab5ac6a 100644 --- a/src/renderer/src/pages/achievement/achievement.tsx +++ b/src/renderer/src/pages/achievement/achievement.tsx @@ -1,7 +1,7 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import achievementSound from "@renderer/assets/audio/achievement.wav"; import { useTranslation } from "react-i18next"; -import { vars } from "@renderer/theme.css"; +import * as styles from "./achievement.css"; interface AchievementInfo { displayName: string; @@ -11,8 +11,16 @@ interface AchievementInfo { export function Achievement() { const { t } = useTranslation("achievement"); + const [isClosing, setIsClosing] = useState(false); + const [isVisible, setIsVisible] = useState(false); + const [achievements, setAchievements] = useState([]); + const [currentAchievement, setCurrentAchievement] = + useState(null); + const achievementAnimation = useRef(-1); + const closingAnimation = useRef(-1); + const visibleAnimation = useRef(-1); const audio = useMemo(() => { const audio = new Audio(achievementSound); @@ -24,11 +32,9 @@ export function Achievement() { useEffect(() => { const unsubscribe = window.electron.onAchievementUnlocked( (_object, _shop, achievements) => { - if (!achievements) return; + if (!achievements || !achievements.length) return; - if (achievements.length) { - setAchievements((ach) => ach.concat(achievements)); - } + setAchievements((ach) => ach.concat(achievements)); audio.play(); } @@ -41,12 +47,37 @@ export function Achievement() { const hasAchievementsPending = achievements.length > 0; + const startAnimateClosing = useCallback(() => { + cancelAnimationFrame(closingAnimation.current); + cancelAnimationFrame(visibleAnimation.current); + cancelAnimationFrame(achievementAnimation.current); + + setIsClosing(true); + + const zero = performance.now(); + closingAnimation.current = requestAnimationFrame( + function animateClosing(time) { + if (time - zero <= 1000) { + closingAnimation.current = requestAnimationFrame(animateClosing); + } else { + setIsVisible(false); + } + } + ); + }, []); + useEffect(() => { if (hasAchievementsPending) { + setIsClosing(false); + setIsVisible(true); + let zero = performance.now(); + cancelAnimationFrame(closingAnimation.current); + cancelAnimationFrame(visibleAnimation.current); + cancelAnimationFrame(achievementAnimation.current); achievementAnimation.current = requestAnimationFrame( function animateLock(time) { - if (time - zero > 3000) { + if (time - zero > 2500) { zero = performance.now(); setAchievements((ach) => ach.slice(1)); } @@ -54,30 +85,30 @@ export function Achievement() { } ); } else { - cancelAnimationFrame(achievementAnimation.current); + startAnimateClosing(); } }, [hasAchievementsPending]); - if (!hasAchievementsPending) return null; + useEffect(() => { + if (achievements.length) { + setCurrentAchievement(achievements[0]); + } + }, [achievements]); + + if (!isVisible || !currentAchievement) return null; return ( -
- {achievements[0].displayName} -
-

{t("achievement_unlocked")}

-

{achievements[0].displayName}

+
+
+ {currentAchievement.displayName} +
+

{t("achievement_unlocked")}

+

{currentAchievement.displayName}

+
); From 9731035820a0800e159a1794b33955fb133926c8 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Thu, 3 Oct 2024 20:26:45 -0300 Subject: [PATCH 3/3] feat: searching new folders --- .../achievements/achievement-file-observer.ts | 2 +- .../achievements/find-achivement-files.ts | 162 +++++++++++++----- .../achievements/parse-achievement-file.ts | 37 +++- .../update-local-unlocked-achivements.ts | 4 +- src/shared/constants.ts | 5 +- 5 files changed, 158 insertions(+), 52 deletions(-) diff --git a/src/main/services/achievements/achievement-file-observer.ts b/src/main/services/achievements/achievement-file-observer.ts index d8610ec8..d008304e 100644 --- a/src/main/services/achievements/achievement-file-observer.ts +++ b/src/main/services/achievements/achievement-file-observer.ts @@ -55,7 +55,7 @@ const compareFile = async (game: Game, file: AchievementFile) => { }; export const checkAchievementFileChange = async (games: Game[]) => { - const achievementFiles = findAllAchievementFiles(); + const achievementFiles = await findAllAchievementFiles(); for (const game of games) { const gameAchievementFiles = achievementFiles.get(game.objectID) || []; diff --git a/src/main/services/achievements/find-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts index 73122b9e..d2dfe85e 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -9,54 +9,120 @@ import { Game } from "@main/entity"; const publicDir = path.join("C:", "Users", "Public", "Documents"); const programData = path.join("C:", "ProgramData"); const appData = app.getPath("appData"); +const documents = app.getPath("documents"); const crackers = [ Cracker.codex, Cracker.goldberg, - Cracker.goldberg2, Cracker.rune, Cracker.onlineFix, Cracker.userstats, Cracker.rld, + Cracker.creamAPI, + Cracker.skidrow, + Cracker.smartSteamEmu, + Cracker.empress, ]; -const getPathFromCracker = (cracker: Cracker) => { - let folderPath: string; - let fileLocation: string[]; - - if (cracker === Cracker.onlineFix) { - folderPath = path.join(publicDir, Cracker.onlineFix); - fileLocation = ["Stats", "Achievements.ini"]; - } else if (cracker === Cracker.goldberg) { - folderPath = path.join(appData, "Goldberg SteamEmu Saves"); - fileLocation = ["achievements.json"]; - } else if (cracker === Cracker.goldberg2) { - folderPath = path.join(appData, "GSE Saves"); - fileLocation = ["achievements.json"]; - } else if (cracker === Cracker.rld) { - folderPath = path.join(programData, Cracker.rld); - fileLocation = ["achievements.ini"]; - } else { - folderPath = path.join(publicDir, "Steam", cracker); - fileLocation = ["achievements.ini"]; +const getPathFromCracker = async (cracker: Cracker) => { + if (cracker === Cracker.smartSteamEmu) { + return [ + { + folderPath: path.join(appData, "SmartSteamEmu"), + fileLocation: ["User", "Achievements"], + }, + ]; } - return { folderPath, fileLocation }; + if (cracker === Cracker.onlineFix) { + return [ + { + folderPath: path.join(publicDir, Cracker.onlineFix), + fileLocation: ["Stats", "Achievements.ini"], + }, + ]; + } + + if (cracker === Cracker.goldberg) { + return [ + { + folderPath: path.join(appData, "Goldberg SteamEmu Saves"), + fileLocation: ["achievements.json"], + }, + { + folderPath: path.join(appData, "GSE Saves"), + fileLocation: ["achievements.json"], + }, + ]; + } + + if (cracker === Cracker.rld) { + return [ + { + folderPath: path.join(programData, "RLD!"), + fileLocation: ["achievements.ini"], + }, + ]; + } + + if (cracker === Cracker.creamAPI) { + return [ + { + folderPath: path.join(appData, "CreamAPI"), + fileLocation: ["achievements.ini"], + }, + ]; + } + + if (cracker === Cracker.skidrow) { + return [ + { + folderPath: path.join(documents, "SKIDROW"), + fileLocation: ["SteamEmu", "UserStats", "achiev.ini"], + }, + { + folderPath: path.join(documents, "Player"), + fileLocation: ["SteamEmu", "UserStats", "achiev.ini"], + }, + ]; + } + + if (cracker === Cracker.codex) { + return [ + { + folderPath: path.join(publicDir, "Steam", "CODEX"), + fileLocation: ["achievements.ini"], + }, + { + folderPath: path.join(appData, "Steam", "CODEX"), + fileLocation: ["achievements.ini"], + }, + ]; + } + + return [ + { + folderPath: path.join(publicDir, "Steam", cracker), + fileLocation: ["achievements.ini"], + }, + ]; }; -export const findAchievementFiles = (game: Game) => { +export const findAchievementFiles = async (game: Game) => { const achievementFiles: AchievementFile[] = []; for (const cracker of crackers) { - const { folderPath, fileLocation } = getPathFromCracker(cracker); + for (const { folderPath, fileLocation } of await getPathFromCracker( + cracker + )) { + const filePath = path.join(folderPath, game.objectID, ...fileLocation); - const filePath = path.join(folderPath, game.objectID, ...fileLocation); - - if (fs.existsSync(filePath)) { - achievementFiles.push({ - type: cracker, - filePath: path.join(folderPath, game.objectID, ...fileLocation), - }); + if (fs.existsSync(filePath)) { + achievementFiles.push({ + type: cracker, + filePath, + }); + } } } @@ -83,31 +149,33 @@ export const findAchievementFileInExecutableDirectory = ( }; }; -export const findAllAchievementFiles = () => { +export const findAllAchievementFiles = async () => { const gameAchievementFiles = new Map(); for (const cracker of crackers) { - const { folderPath, fileLocation } = getPathFromCracker(cracker); + for (const { folderPath, fileLocation } of await getPathFromCracker( + cracker + )) { + if (!fs.existsSync(folderPath)) { + continue; + } - if (!fs.existsSync(folderPath)) { - return gameAchievementFiles; - } + const objectIds = fs.readdirSync(folderPath); - const objectIds = fs.readdirSync(folderPath); + for (const objectId of objectIds) { + const filePath = path.join(folderPath, objectId, ...fileLocation); - for (const objectId of objectIds) { - const filePath = path.join(folderPath, objectId, ...fileLocation); + if (!fs.existsSync(filePath)) continue; - if (!fs.existsSync(filePath)) continue; + const achivementFile = { + type: cracker, + filePath, + }; - const achivementFile = { - type: cracker, - filePath, - }; - - gameAchievementFiles.get(objectId) - ? gameAchievementFiles.get(objectId)!.push(achivementFile) - : gameAchievementFiles.set(objectId, [achivementFile]); + gameAchievementFiles.get(objectId) + ? gameAchievementFiles.get(objectId)!.push(achivementFile) + : gameAchievementFiles.set(objectId, [achivementFile]); + } } } diff --git a/src/main/services/achievements/parse-achievement-file.ts b/src/main/services/achievements/parse-achievement-file.ts index c00c5d87..a2fe8f1b 100644 --- a/src/main/services/achievements/parse-achievement-file.ts +++ b/src/main/services/achievements/parse-achievement-file.ts @@ -9,11 +9,28 @@ export const parseAchievementFile = async ( ): Promise => { if (!existsSync(filePath)) return []; + if (type === Cracker.empress) { + return []; + } + + if (type === Cracker.skidrow) { + const parsed = await iniParse(filePath); + return processSkidrow(parsed); + } + + if (type === Cracker.smartSteamEmu) { + return []; + } + + if (type === Cracker.creamAPI) { + return []; + } + if (type === Cracker.onlineFix) { const parsed = await iniParse(filePath); return processOnlineFix(parsed); } - if (type === Cracker.goldberg || type === Cracker.goldberg2) { + if (type === Cracker.goldberg) { const parsed = await jsonParse(filePath); return processGoldberg(parsed); } @@ -88,6 +105,24 @@ const processOnlineFix = (unlockedAchievements: any): UnlockedAchievement[] => { return parsedUnlockedAchievements; }; +const processSkidrow = (unlockedAchievements: any): UnlockedAchievement[] => { + const parsedUnlockedAchievements: UnlockedAchievement[] = []; + const achievements = unlockedAchievements["Achievements"]; + + for (const achievement of Object.keys(achievements)) { + const unlockedAchievement = achievements[achievement].split("@"); + + if (unlockedAchievement[0] === "1") { + parsedUnlockedAchievements.push({ + name: achievement, + unlockTime: unlockedAchievement[unlockedAchievement.length - 1], + }); + } + } + + return parsedUnlockedAchievements; +}; + const processGoldberg = (unlockedAchievements: any): UnlockedAchievement[] => { const newUnlockedAchievements: UnlockedAchievement[] = []; diff --git a/src/main/services/achievements/update-local-unlocked-achivements.ts b/src/main/services/achievements/update-local-unlocked-achivements.ts index 16ed0ae2..6c0fed8f 100644 --- a/src/main/services/achievements/update-local-unlocked-achivements.ts +++ b/src/main/services/achievements/update-local-unlocked-achivements.ts @@ -9,7 +9,7 @@ import type { UnlockedAchievement } from "@types"; import { getGameAchievementData } from "./get-game-achievement-data"; export const updateAllLocalUnlockedAchievements = async () => { - const gameAchievementFilesMap = findAllAchievementFiles(); + const gameAchievementFilesMap = await findAllAchievementFiles(); for (const objectId of gameAchievementFilesMap.keys()) { const gameAchievementFiles = gameAchievementFilesMap.get(objectId)!; @@ -69,7 +69,7 @@ export const updateLocalUnlockedAchivements = async (objectId: string) => { if (!game) return; - const gameAchievementFiles = findAchievementFiles(game); + const gameAchievementFiles = await findAchievementFiles(game); console.log("Achievements files for", game.title, gameAchievementFiles); diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 62c51418..e22d7712 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -29,7 +29,10 @@ export enum Cracker { rune = "RUNE", onlineFix = "OnlineFix", goldberg = "Goldberg", - goldberg2 = "Goldberg2", userstats = "user_stats", rld = "RLD!", + empress = "EMPRESS", + skidrow = "SKIDROW", + creamAPI = "CreamAPI", + smartSteamEmu = "SmartSteamEmu", }