From 63374ccd7474d9620ef26f3d1f84034d8293848c Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 9 Jun 2025 06:49:23 -0300 Subject: [PATCH 1/4] feat: parse achievements from steam local cache --- .../achievement-watcher-manager.ts | 19 +++++++++-- .../achievements/find-achivement-files.ts | 32 +++++++++++++++++ .../achievements/get-game-achievement-data.ts | 26 ++++++++++++++ .../achievements/parse-achievement-file.ts | 34 +++++++++++++++++++ src/shared/constants.ts | 1 + 5 files changed, 110 insertions(+), 2 deletions(-) diff --git a/src/main/services/achievements/achievement-watcher-manager.ts b/src/main/services/achievements/achievement-watcher-manager.ts index 495bafac..e4aa1b1c 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) { @@ -66,6 +71,8 @@ const watchAchievementsWithWine = async () => { gameAchievementFiles.push(...achievementFileInsideDirectory); + gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game))); + for (const file of gameAchievementFiles) { await compareFile(game, file); } @@ -179,6 +186,8 @@ export class AchievementWatcherManager { gameAchievementFiles.push(...achievementFileInsideDirectory); + gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game))); + const unlockedAchievements: UnlockedAchievement[] = []; for (const achievementFile of gameAchievementFiles) { @@ -259,7 +268,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 +279,10 @@ export class AchievementWatcherManager { achievementFiles.push( ...findAchievementFileInExecutableDirectory(game) ); + + achievementFiles.push( + ...(await findAchievementFileInSteamPath(game)) + ); } return { game, achievementFiles }; @@ -284,13 +297,15 @@ 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..c6b3a6fc 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -4,6 +4,7 @@ import type { Game, AchievementFile } from "@types"; import { Cracker } from "@shared"; import { achievementsLogger } from "../logger"; import { SystemPath } from "../system-path"; +import { getSteamLocation, getSteamUsersIds } from "../steam"; const getAppDataPath = () => { if (process.platform === "win32") { @@ -273,6 +274,37 @@ export const findAchievementFiles = (game: Game) => { return achievementFiles; }; +const steamUserIds = await getSteamUsersIds(); +const steamPath = await getSteamLocation(); + +export const findAchievementFileInSteamPath = async (game: Game) => { + if (!steamUserIds.length) { + 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; +}; + export const findAchievementFileInExecutableDirectory = ( game: Game ): AchievementFile[] => { diff --git a/src/main/services/achievements/get-game-achievement-data.ts b/src/main/services/achievements/get-game-achievement-data.ts index e2b663d8..214735c2 100644 --- a/src/main/services/achievements/get-game-achievement-data.ts +++ b/src/main/services/achievements/get-game-achievement-data.ts @@ -19,6 +19,32 @@ const getModifiedSinceHeader = ( : undefined; }; +const getModifiedSinceHeader = async ( + cachedAchievements: GameAchievement | undefined +): Promise => { + const hasActiveSubscription = await db + .get(levelKeys.user, { valueEncoding: "json" }) + .then((user) => { + const expiresAt = new Date(user?.subscription?.expiresAt ?? 0); + return expiresAt > new Date(); + }); + + if (!cachedAchievements) { + return undefined; + } + + const hasAchievementsPoints = + cachedAchievements.achievements[0].points != undefined; + + if (hasActiveSubscription !== hasAchievementsPoints) { + return undefined; + } + + return cachedAchievements.updatedAt + ? new Date(cachedAchievements.updatedAt) + : undefined; +}; + export const getGameAchievementData = async ( objectId: string, shop: GameShop, 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/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", From e0c5f80b68ed0ade4120fca813a2e9d6c78cc611 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 9 Jun 2025 08:40:31 -0300 Subject: [PATCH 2/4] feat: refactor --- .../achievement-watcher-manager.ts | 13 ---------- .../achievements/find-achivement-files.ts | 5 +++- .../achievements/get-game-achievement-data.ts | 26 ------------------- 3 files changed, 4 insertions(+), 40 deletions(-) diff --git a/src/main/services/achievements/achievement-watcher-manager.ts b/src/main/services/achievements/achievement-watcher-manager.ts index e4aa1b1c..b862abbe 100644 --- a/src/main/services/achievements/achievement-watcher-manager.ts +++ b/src/main/services/achievements/achievement-watcher-manager.ts @@ -66,10 +66,6 @@ const watchAchievementsWithWine = async () => { for (const game of games) { const gameAchievementFiles = findAchievementFiles(game); - const achievementFileInsideDirectory = - findAchievementFileInExecutableDirectory(game); - - gameAchievementFiles.push(...achievementFileInsideDirectory); gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game))); @@ -181,11 +177,6 @@ export class AchievementWatcherManager { const gameAchievementFiles = findAchievementFiles(game); - const achievementFileInsideDirectory = - findAchievementFileInExecutableDirectory(game); - - gameAchievementFiles.push(...achievementFileInsideDirectory); - gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game))); const unlockedAchievements: UnlockedAchievement[] = []; @@ -299,10 +290,6 @@ export class AchievementWatcherManager { return Promise.all( games.map(async (game) => { const achievementFiles = findAchievementFiles(game); - const achievementFileInsideDirectory = - findAchievementFileInExecutableDirectory(game); - - achievementFiles.push(...achievementFileInsideDirectory); achievementFiles.push(...(await findAchievementFileInSteamPath(game))); diff --git a/src/main/services/achievements/find-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts index c6b3a6fc..183f6ba9 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -271,7 +271,10 @@ export const findAchievementFiles = (game: Game) => { } } - return achievementFiles; + const achievementFileInsideDirectory = + findAchievementFileInExecutableDirectory(game); + + return achievementFiles.concat(achievementFileInsideDirectory); }; const steamUserIds = await getSteamUsersIds(); diff --git a/src/main/services/achievements/get-game-achievement-data.ts b/src/main/services/achievements/get-game-achievement-data.ts index 214735c2..e2b663d8 100644 --- a/src/main/services/achievements/get-game-achievement-data.ts +++ b/src/main/services/achievements/get-game-achievement-data.ts @@ -19,32 +19,6 @@ const getModifiedSinceHeader = ( : undefined; }; -const getModifiedSinceHeader = async ( - cachedAchievements: GameAchievement | undefined -): Promise => { - const hasActiveSubscription = await db - .get(levelKeys.user, { valueEncoding: "json" }) - .then((user) => { - const expiresAt = new Date(user?.subscription?.expiresAt ?? 0); - return expiresAt > new Date(); - }); - - if (!cachedAchievements) { - return undefined; - } - - const hasAchievementsPoints = - cachedAchievements.achievements[0].points != undefined; - - if (hasActiveSubscription !== hasAchievementsPoints) { - return undefined; - } - - return cachedAchievements.updatedAt - ? new Date(cachedAchievements.updatedAt) - : undefined; -}; - export const getGameAchievementData = async ( objectId: string, shop: GameShop, From 2b8cc506dff9dee8ebcdaaefb6cbce007859e432 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:51:49 -0300 Subject: [PATCH 3/4] feat: settings to enable steam achievements search --- src/locales/en/translation.json | 1 + src/locales/pt-BR/translation.json | 1 + .../achievements/find-achivement-files.ts | 14 ++++++++++- src/main/services/window-manager.ts | 2 +- src/renderer/index.html | 2 +- .../src/pages/settings/settings-behavior.scss | 11 +++++++++ .../src/pages/settings/settings-behavior.tsx | 23 +++++++++++++++++++ src/types/level.types.ts | 1 + 8 files changed, 52 insertions(+), 3 deletions(-) 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/services/achievements/find-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts index 183f6ba9..26d44170 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -1,10 +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") { @@ -285,6 +286,17 @@ export const findAchievementFileInSteamPath = async (game: Game) => { return []; } + const userPreferences = await db.get( + levelKeys.userPreferences, + { + valueEncoding: "json", + } + ); + + if (!userPreferences?.enableSteamAchievements) { + return []; + } + const achievementFiles: AchievementFile[] = []; for (const steamUserId of steamUserIds) { 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/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 { From b7199f4d9501f06d4348e230f7b38913dbb76267 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:54:21 -0300 Subject: [PATCH 4/4] chore: bump version --- package.json | 2 +- src/main/constants.ts | 2 +- src/main/services/process-watcher.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) 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/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/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;