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..d008304e 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 = await 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..d2dfe85e --- /dev/null +++ b/src/main/services/achievements/find-achivement-files.ts @@ -0,0 +1,183 @@ +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 documents = app.getPath("documents"); + +const crackers = [ + Cracker.codex, + Cracker.goldberg, + Cracker.rune, + Cracker.onlineFix, + Cracker.userstats, + Cracker.rld, + Cracker.creamAPI, + Cracker.skidrow, + Cracker.smartSteamEmu, + Cracker.empress, +]; + +const getPathFromCracker = async (cracker: Cracker) => { + if (cracker === Cracker.smartSteamEmu) { + return [ + { + folderPath: path.join(appData, "SmartSteamEmu"), + fileLocation: ["User", "Achievements"], + }, + ]; + } + + 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 = async (game: Game) => { + const achievementFiles: AchievementFile[] = []; + + for (const cracker of crackers) { + for (const { folderPath, fileLocation } of await getPathFromCracker( + cracker + )) { + const filePath = path.join(folderPath, game.objectID, ...fileLocation); + + if (fs.existsSync(filePath)) { + achievementFiles.push({ + type: cracker, + filePath, + }); + } + } + } + + 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 = async () => { + const gameAchievementFiles = new Map(); + + for (const cracker of crackers) { + for (const { folderPath, fileLocation } of await getPathFromCracker( + cracker + )) { + if (!fs.existsSync(folderPath)) { + continue; + } + + 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..a2fe8f1b 100644 --- a/src/main/services/achievements/parse-achievement-file.ts +++ b/src/main/services/achievements/parse-achievement-file.ts @@ -1,60 +1,52 @@ 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.empress) { + return []; } -}; -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.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) { + 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 +69,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 +87,119 @@ 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 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[] = []; + + 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..6c0fed8f 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 = await 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 = await 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/main/services/window-manager.ts b/src/main/services/window-manager.ts index 104a2d92..9b9d09b2 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -154,10 +154,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}

+
); diff --git a/src/shared/constants.ts b/src/shared/constants.ts index a92ea029..e22d7712 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -29,6 +29,10 @@ export enum Cracker { rune = "RUNE", onlineFix = "OnlineFix", goldberg = "Goldberg", - goldberg2 = "Goldberg2", - generic = "Generic", + userstats = "user_stats", + rld = "RLD!", + empress = "EMPRESS", + skidrow = "SKIDROW", + creamAPI = "CreamAPI", + smartSteamEmu = "SmartSteamEmu", }