diff --git a/package.json b/package.json index b4ecc5bf..ed7e31e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydralauncher", - "version": "3.6.3", + "version": "3.6.4", "description": "Hydra", "main": "./out/main/index.js", "author": "Los Broxas", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 24de88cf..14fba932 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -379,6 +379,7 @@ "installing_common_redist": "Installing…", "show_download_speed_in_megabytes": "Show download speed in megabytes per second", "extract_files_by_default": "Extract files by default after download", + "enable_steam_achievements": "Enable search for Steam achievements", "achievement_custom_notification_position": "Achievement custom notification position", "top-left": "Top left", "top-center": "Top center", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 8373b415..1f6adcd0 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -364,6 +364,7 @@ "installing_common_redist": "Instalando…", "show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo", "extract_files_by_default": "Extrair arquivos automaticamente após o download", + "enable_steam_achievements": "Habilitar busca por conquistas da Steam", "enable_achievement_custom_notifications": "Habilitar notificações customizadas de conquistas", "top-left": "Superior esquerdo", "top-center": "Superior central", diff --git a/src/main/constants.ts b/src/main/constants.ts index 16642d50..b067be80 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -41,4 +41,4 @@ export const appVersion = app.getVersion() + (isStaging ? "-staging" : ""); export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets"); -export const MAIN_LOOP_INTERVAL = 1500; +export const MAIN_LOOP_INTERVAL = 2000; diff --git a/src/main/services/achievements/achievement-watcher-manager.ts b/src/main/services/achievements/achievement-watcher-manager.ts index 495bafac..b862abbe 100644 --- a/src/main/services/achievements/achievement-watcher-manager.ts +++ b/src/main/services/achievements/achievement-watcher-manager.ts @@ -3,6 +3,7 @@ import { mergeAchievements } from "./merge-achievements"; import fs, { readdirSync } from "node:fs"; import { findAchievementFileInExecutableDirectory, + findAchievementFileInSteamPath, findAchievementFiles, findAllAchievementFiles, getAlternativeObjectIds, @@ -43,6 +44,10 @@ const watchAchievementsWindows = async () => { gameAchievementFiles.push( ...findAchievementFileInExecutableDirectory(game) ); + + gameAchievementFiles.push( + ...(await findAchievementFileInSteamPath(game)) + ); } for (const file of gameAchievementFiles) { @@ -61,10 +66,8 @@ const watchAchievementsWithWine = async () => { for (const game of games) { const gameAchievementFiles = findAchievementFiles(game); - const achievementFileInsideDirectory = - findAchievementFileInExecutableDirectory(game); - gameAchievementFiles.push(...achievementFileInsideDirectory); + gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game))); for (const file of gameAchievementFiles) { await compareFile(game, file); @@ -174,10 +177,7 @@ export class AchievementWatcherManager { const gameAchievementFiles = findAchievementFiles(game); - const achievementFileInsideDirectory = - findAchievementFileInExecutableDirectory(game); - - gameAchievementFiles.push(...achievementFileInsideDirectory); + gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game))); const unlockedAchievements: UnlockedAchievement[] = []; @@ -259,7 +259,7 @@ export class AchievementWatcherManager { const gameAchievementFilesMap = findAllAchievementFiles(); return Promise.all( - games.map((game) => { + games.map(async (game) => { const achievementFiles: AchievementFile[] = []; for (const objectId of getAlternativeObjectIds(game.objectId)) { @@ -270,6 +270,10 @@ export class AchievementWatcherManager { achievementFiles.push( ...findAchievementFileInExecutableDirectory(game) ); + + achievementFiles.push( + ...(await findAchievementFileInSteamPath(game)) + ); } return { game, achievementFiles }; @@ -284,12 +288,10 @@ export class AchievementWatcherManager { .then((games) => games.filter((game) => !game.isDeleted)); return Promise.all( - games.map((game) => { + games.map(async (game) => { const achievementFiles = findAchievementFiles(game); - const achievementFileInsideDirectory = - findAchievementFileInExecutableDirectory(game); - achievementFiles.push(...achievementFileInsideDirectory); + achievementFiles.push(...(await findAchievementFileInSteamPath(game))); return { game, achievementFiles }; }) diff --git a/src/main/services/achievements/find-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts index 7a531388..26d44170 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -1,9 +1,11 @@ import path from "node:path"; import fs from "node:fs"; -import type { Game, AchievementFile } from "@types"; +import type { Game, AchievementFile, UserPreferences } from "@types"; import { Cracker } from "@shared"; import { achievementsLogger } from "../logger"; import { SystemPath } from "../system-path"; +import { getSteamLocation, getSteamUsersIds } from "../steam"; +import { db, levelKeys } from "@main/level"; const getAppDataPath = () => { if (process.platform === "win32") { @@ -270,6 +272,51 @@ export const findAchievementFiles = (game: Game) => { } } + const achievementFileInsideDirectory = + findAchievementFileInExecutableDirectory(game); + + return achievementFiles.concat(achievementFileInsideDirectory); +}; + +const steamUserIds = await getSteamUsersIds(); +const steamPath = await getSteamLocation(); + +export const findAchievementFileInSteamPath = async (game: Game) => { + if (!steamUserIds.length) { + return []; + } + + const userPreferences = await db.get( + levelKeys.userPreferences, + { + valueEncoding: "json", + } + ); + + if (!userPreferences?.enableSteamAchievements) { + return []; + } + + const achievementFiles: AchievementFile[] = []; + + for (const steamUserId of steamUserIds) { + const gameAchievementPath = path.join( + steamPath, + "userdata", + steamUserId.toString(), + "config", + "librarycache", + `${game.objectId}.json` + ); + + if (fs.existsSync(gameAchievementPath)) { + achievementFiles.push({ + type: Cracker.Steam, + filePath: gameAchievementPath, + }); + } + } + return achievementFiles; }; diff --git a/src/main/services/achievements/parse-achievement-file.ts b/src/main/services/achievements/parse-achievement-file.ts index 726d8d0f..44827782 100644 --- a/src/main/services/achievements/parse-achievement-file.ts +++ b/src/main/services/achievements/parse-achievement-file.ts @@ -75,6 +75,11 @@ export const parseAchievementFile = ( return processRazor1911(filePath); } + if (type === Cracker.Steam) { + const parsed = jsonParse(filePath); + return processSteamCacheAchievement(parsed); + } + achievementsLogger.log( `Unprocessed ${type} achievements found on ${filePath}` ); @@ -234,6 +239,35 @@ const processGoldberg = (unlockedAchievements: any): UnlockedAchievement[] => { return newUnlockedAchievements; }; +const processSteamCacheAchievement = ( + unlockedAchievements: any[] +): UnlockedAchievement[] => { + const newUnlockedAchievements: UnlockedAchievement[] = []; + + const achievementIndex = unlockedAchievements.findIndex( + (element) => element[0] === "achievements" + ); + + if (achievementIndex === -1) { + achievementsLogger.info("No achievements found in Steam cache file"); + return []; + } + + const unlockedAchievementsData = + unlockedAchievements[achievementIndex][1]["data"]["vecHighlight"]; + + for (const achievement of unlockedAchievementsData) { + if (achievement.bAchieved) { + newUnlockedAchievements.push({ + name: achievement.strID, + unlockTime: achievement.rtUnlocked * 1000, + }); + } + } + + return newUnlockedAchievements; +}; + const process3DM = (unlockedAchievements: any): UnlockedAchievement[] => { const newUnlockedAchievements: UnlockedAchievement[] = []; diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index eb9febe8..8c407ad5 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -9,6 +9,7 @@ import { CloudSync } from "./cloud-sync"; import { logger } from "./logger"; import path from "path"; import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager"; +import { MAIN_LOOP_INTERVAL } from "@main/constants"; export const gamesPlaytime = new Map< string, @@ -25,7 +26,7 @@ interface GameExecutables { [key: string]: ExecutableInfo[]; } -const TICKS_TO_UPDATE_API = 120; +const TICKS_TO_UPDATE_API = (3 * 60 * 1000) / MAIN_LOOP_INTERVAL; // 3 minutes let currentTick = 1; const platform = process.platform; diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 35a52397..dfcfc3ba 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -582,7 +582,7 @@ export class WindowManager { tray.popUpContextMenu(contextMenu); }; - tray.setToolTip("Hydra"); + tray.setToolTip("Hydra Launcher"); if (process.platform !== "darwin") { await updateSystemTray(); diff --git a/src/renderer/index.html b/src/renderer/index.html index 5d62f4c5..42166e56 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -3,7 +3,7 @@ - Hydra + Hydra Launcher + +
+ + handleChange({ + enableSteamAchievements: !form.enableSteamAchievements, + }) + } + /> + + + + +
); } diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 448b7eec..851aec49 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -35,6 +35,7 @@ export enum Cracker { onlineFix = "OnlineFix", goldberg = "Goldberg", userstats = "user_stats", + Steam = "Steam", rld = "RLD!", empress = "EMPRESS", skidrow = "SKIDROW", diff --git a/src/types/level.types.ts b/src/types/level.types.ts index 13f59c7f..e99641fe 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -101,6 +101,7 @@ export interface UserPreferences { friendStartGameNotificationsEnabled?: boolean; showDownloadSpeedInMegabytes?: boolean; extractFilesByDefault?: boolean; + enableSteamAchievements?: boolean; } export interface ScreenState {