diff --git a/src/main/services/achievements/achievement-file-observer.ts b/src/main/services/achievements/achievement-file-observer.ts deleted file mode 100644 index 281ba58f..00000000 --- a/src/main/services/achievements/achievement-file-observer.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { parseAchievementFile } from "./parse-achievement-file"; -import { Game } from "@main/entity"; -import { mergeAchievements } from "./merge-achievements"; -import fs from "node:fs"; -import { - findAchievementFileInExecutableDirectory, - findAllAchievementFiles, -} from "./find-achivement-files"; -import type { AchievementFile } from "@types"; -import { logger } from "../logger"; - -const fileStats: Map = new Map(); - -const processAchievementFileDiff = async ( - game: Game, - file: AchievementFile -) => { - const unlockedAchievements = await parseAchievementFile( - file.filePath, - file.type - ); - - logger.log("Achievements from file", file.filePath, unlockedAchievements); - - if (unlockedAchievements.length) { - return mergeAchievements( - game.objectID, - game.shop, - unlockedAchievements, - true - ); - } -}; - -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 processAchievementFileDiff(game, file); - } catch (err) { - fileStats.set(file.filePath, -1); - } -}; - -export const checkAchievementFileChange = async (games: Game[]) => { - const achievementFiles = await findAllAchievementFiles(); - - for (const game of games) { - const gameAchievementFiles = achievementFiles.get(game.objectID) || []; - const achievementFileInsideDirectory = - findAchievementFileInExecutableDirectory(game); - - gameAchievementFiles.push(...achievementFileInsideDirectory); - - if (!gameAchievementFiles.length) continue; - - 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/achievement-watcher.ts b/src/main/services/achievements/achievement-watcher.ts index d88771a8..bc00ddd3 100644 --- a/src/main/services/achievements/achievement-watcher.ts +++ b/src/main/services/achievements/achievement-watcher.ts @@ -1,5 +1,18 @@ import { gameRepository } from "@main/repository"; -import { checkAchievementFileChange as searchForAchievements } from "./achievement-file-observer"; +import { parseAchievementFile } from "./parse-achievement-file"; +import { Game } from "@main/entity"; +import { mergeAchievements } from "./merge-achievements"; +import fs, { readdirSync } from "node:fs"; +import { + findAchievementFileInExecutableDirectory, + findAllAchievementFiles, +} from "./find-achivement-files"; +import type { AchievementFile } from "@types"; +import { achievementsLogger, logger } from "../logger"; +import { Cracker } from "@shared"; + +const fileStats: Map = new Map(); +const fltFiles: Map> = new Map(); export const watchAchievements = async () => { const games = await gameRepository.find({ @@ -12,3 +25,96 @@ export const watchAchievements = async () => { await searchForAchievements(games); }; + +const processAchievementFileDiff = async ( + game: Game, + file: AchievementFile +) => { + const unlockedAchievements = await parseAchievementFile( + file.filePath, + file.type + ); + + logger.log("Achievements from file", file.filePath, unlockedAchievements); + + if (unlockedAchievements.length) { + return mergeAchievements( + game.objectID, + game.shop, + unlockedAchievements, + true + ); + } +}; + +const compareFltFolder = async (game: Game, file: AchievementFile) => { + try { + const currentAchievements = new Set(readdirSync(file.filePath)); + const previousAchievements = fltFiles.get(file.filePath); + + fltFiles.set(file.filePath, currentAchievements); + if ( + !previousAchievements || + currentAchievements.difference(previousAchievements).size === 0 + ) { + return; + } + + logger.log("Detected change in FLT folder", file.filePath); + await processAchievementFileDiff(game, file); + } catch (err) { + achievementsLogger.error(err); + fltFiles.set(file.filePath, new Set()); + } +}; + +const compareFile = async (game: Game, file: AchievementFile) => { + if (file.type === Cracker.flt) { + await compareFltFolder(game, file); + return; + } + + try { + const currentStat = fs.statSync(file.filePath); + const previousStat = fileStats.get(file.filePath); + fileStats.set(file.filePath, currentStat.mtimeMs); + + if (!previousStat || previousStat === currentStat.mtimeMs) { + return; + } + + logger.log( + "Detected change in file", + file.filePath, + currentStat.mtimeMs, + fileStats.get(file.filePath) + ); + await processAchievementFileDiff(game, file); + } catch (err) { + fileStats.set(file.filePath, -1); + } +}; + +const searchForAchievements = async (games: Game[]) => { + const achievementFiles = await findAllAchievementFiles(); + + for (const game of games) { + const gameAchievementFiles = achievementFiles.get(game.objectID) || []; + const achievementFileInsideDirectory = + findAchievementFileInExecutableDirectory(game); + + gameAchievementFiles.push(...achievementFileInsideDirectory); + + if (!gameAchievementFiles.length) continue; + + 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/find-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts index 09823ca3..247888e6 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -24,6 +24,7 @@ const crackers = [ Cracker.skidrow, Cracker.smartSteamEmu, Cracker.empress, + Cracker.flt, ]; const getPathFromCracker = async (cracker: Cracker) => { @@ -131,7 +132,7 @@ const getPathFromCracker = async (cracker: Cracker) => { return [ { folderPath: path.join(appData, "SmartSteamEmu"), - fileLocation: ["User", "Achievements"], + fileLocation: ["User", "Achievements.ini"], }, ]; } @@ -140,6 +141,15 @@ const getPathFromCracker = async (cracker: Cracker) => { return []; } + if (cracker === Cracker.flt) { + return [ + { + folderPath: path.join(appData, "FLT"), + fileLocation: ["stats"], + }, + ]; + } + achievementsLogger.error(`Cracker ${cracker} not implemented`); throw new Error(`Cracker ${cracker} not implemented`); }; diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index dd4bd465..b611f2a2 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -47,7 +47,7 @@ export const mergeAchievements = async ( const unlockedAchievements = JSON.parse( localGameAchievement?.unlockedAchievements || "[]" - ); + ).filter((achievement) => achievement.name); const newAchievements = achievements .filter((achievement) => { @@ -60,7 +60,7 @@ export const mergeAchievements = async ( .map((achievement) => { return { name: achievement.name.toUpperCase(), - unlockTime: achievement.unlockTime * 1000, + unlockTime: achievement.unlockTime, }; }); diff --git a/src/main/services/achievements/parse-achievement-file.ts b/src/main/services/achievements/parse-achievement-file.ts index f16c0e2c..251fd1f9 100644 --- a/src/main/services/achievements/parse-achievement-file.ts +++ b/src/main/services/achievements/parse-achievement-file.ts @@ -1,6 +1,11 @@ import { Cracker } from "@shared"; import { UnlockedAchievement } from "@types"; -import { existsSync, createReadStream, readFileSync } from "node:fs"; +import { + existsSync, + createReadStream, + readFileSync, + readdirSync, +} from "node:fs"; import readline from "node:readline"; import { achievementsLogger } from "../logger"; @@ -50,6 +55,17 @@ export const parseAchievementFile = async ( return process3DM(parsed); } + if (type === Cracker.flt) { + const achievements = readdirSync(filePath); + + return achievements.map((achievement) => { + return { + name: achievement, + unlockTime: Date.now(), + }; + }); + } + achievementsLogger.log(`${type} achievements found on ${filePath}`); return []; }; @@ -101,7 +117,7 @@ const processOnlineFix = (unlockedAchievements: any): UnlockedAchievement[] => { if (unlockedAchievement?.achieved) { parsedUnlockedAchievements.push({ name: achievement, - unlockTime: unlockedAchievement.timestamp, + unlockTime: unlockedAchievement.timestamp * 1000, }); } } @@ -119,7 +135,7 @@ const processSkidrow = (unlockedAchievements: any): UnlockedAchievement[] => { if (unlockedAchievement[0] === "1") { parsedUnlockedAchievements.push({ name: achievement, - unlockTime: unlockedAchievement[unlockedAchievement.length - 1], + unlockTime: unlockedAchievement[unlockedAchievement.length - 1] * 1000, }); } } @@ -136,7 +152,7 @@ const processGoldberg = (unlockedAchievements: any): UnlockedAchievement[] => { if (unlockedAchievement?.earned) { newUnlockedAchievements.push({ name: achievement, - unlockTime: unlockedAchievement.earned_time, + unlockTime: unlockedAchievement.earned_time * 1000, }); } } @@ -155,9 +171,10 @@ const process3DM = (unlockedAchievements: any): UnlockedAchievement[] => { newUnlockedAchievements.push({ name: achievement, - unlockTime: new DataView( - new Uint8Array(Buffer.from(time.toString(), "hex")).buffer - ).getUint32(0, true), + unlockTime: + new DataView( + new Uint8Array(Buffer.from(time.toString(), "hex")).buffer + ).getUint32(0, true) * 1000, }); } } @@ -174,7 +191,7 @@ const processDefault = (unlockedAchievements: any): UnlockedAchievement[] => { if (unlockedAchievement?.Achieved) { newUnlockedAchievements.push({ name: achievement, - unlockTime: unlockedAchievement.UnlockTime, + unlockTime: unlockedAchievement.UnlockTime * 1000, }); } } @@ -193,11 +210,12 @@ const processRld = (unlockedAchievements: any): UnlockedAchievement[] => { if (unlockedAchievement?.State) { newUnlockedAchievements.push({ name: achievement, - unlockTime: new DataView( - new Uint8Array( - Buffer.from(unlockedAchievement.Time.toString(), "hex") - ).buffer - ).getUint32(0, true), + unlockTime: + new DataView( + new Uint8Array( + Buffer.from(unlockedAchievement.Time.toString(), "hex") + ).buffer + ).getUint32(0, true) * 1000, }); } } @@ -222,7 +240,7 @@ const processUserStats = (unlockedAchievements: any): UnlockedAchievement[] => { if (!isNaN(unlockTime)) { newUnlockedAchievements.push({ name: achievement.replace(/"/g, ``), - unlockTime: unlockTime, + unlockTime: unlockTime * 1000, }); } } diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 4a826dcd..6a94c103 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -36,4 +36,5 @@ export enum Cracker { creamAPI = "CreamAPI", smartSteamEmu = "SmartSteamEmu", _3dm = "3dm", + flt = "FLT", }