Skip to content

Commit 18e4bae

Browse files
authored
Merge pull request #1779 from hydralauncher/feat/steam-local-cache-achievements
feat: steam local cache achievements
2 parents 8eeacf4 + b7199f4 commit 18e4bae

File tree

14 files changed

+140
-18
lines changed

14 files changed

+140
-18
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hydralauncher",
3-
"version": "3.6.3",
3+
"version": "3.6.4",
44
"description": "Hydra",
55
"main": "./out/main/index.js",
66
"author": "Los Broxas",

src/locales/en/translation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@
379379
"installing_common_redist": "Installing…",
380380
"show_download_speed_in_megabytes": "Show download speed in megabytes per second",
381381
"extract_files_by_default": "Extract files by default after download",
382+
"enable_steam_achievements": "Enable search for Steam achievements",
382383
"achievement_custom_notification_position": "Achievement custom notification position",
383384
"top-left": "Top left",
384385
"top-center": "Top center",

src/locales/pt-BR/translation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@
364364
"installing_common_redist": "Instalando…",
365365
"show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo",
366366
"extract_files_by_default": "Extrair arquivos automaticamente após o download",
367+
"enable_steam_achievements": "Habilitar busca por conquistas da Steam",
367368
"enable_achievement_custom_notifications": "Habilitar notificações customizadas de conquistas",
368369
"top-left": "Superior esquerdo",
369370
"top-center": "Superior central",

src/main/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,4 @@ export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
4141

4242
export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets");
4343

44-
export const MAIN_LOOP_INTERVAL = 1500;
44+
export const MAIN_LOOP_INTERVAL = 2000;

src/main/services/achievements/achievement-watcher-manager.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { mergeAchievements } from "./merge-achievements";
33
import fs, { readdirSync } from "node:fs";
44
import {
55
findAchievementFileInExecutableDirectory,
6+
findAchievementFileInSteamPath,
67
findAchievementFiles,
78
findAllAchievementFiles,
89
getAlternativeObjectIds,
@@ -43,6 +44,10 @@ const watchAchievementsWindows = async () => {
4344
gameAchievementFiles.push(
4445
...findAchievementFileInExecutableDirectory(game)
4546
);
47+
48+
gameAchievementFiles.push(
49+
...(await findAchievementFileInSteamPath(game))
50+
);
4651
}
4752

4853
for (const file of gameAchievementFiles) {
@@ -61,10 +66,8 @@ const watchAchievementsWithWine = async () => {
6166

6267
for (const game of games) {
6368
const gameAchievementFiles = findAchievementFiles(game);
64-
const achievementFileInsideDirectory =
65-
findAchievementFileInExecutableDirectory(game);
6669

67-
gameAchievementFiles.push(...achievementFileInsideDirectory);
70+
gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game)));
6871

6972
for (const file of gameAchievementFiles) {
7073
await compareFile(game, file);
@@ -174,10 +177,7 @@ export class AchievementWatcherManager {
174177

175178
const gameAchievementFiles = findAchievementFiles(game);
176179

177-
const achievementFileInsideDirectory =
178-
findAchievementFileInExecutableDirectory(game);
179-
180-
gameAchievementFiles.push(...achievementFileInsideDirectory);
180+
gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game)));
181181

182182
const unlockedAchievements: UnlockedAchievement[] = [];
183183

@@ -259,7 +259,7 @@ export class AchievementWatcherManager {
259259
const gameAchievementFilesMap = findAllAchievementFiles();
260260

261261
return Promise.all(
262-
games.map((game) => {
262+
games.map(async (game) => {
263263
const achievementFiles: AchievementFile[] = [];
264264

265265
for (const objectId of getAlternativeObjectIds(game.objectId)) {
@@ -270,6 +270,10 @@ export class AchievementWatcherManager {
270270
achievementFiles.push(
271271
...findAchievementFileInExecutableDirectory(game)
272272
);
273+
274+
achievementFiles.push(
275+
...(await findAchievementFileInSteamPath(game))
276+
);
273277
}
274278

275279
return { game, achievementFiles };
@@ -284,12 +288,10 @@ export class AchievementWatcherManager {
284288
.then((games) => games.filter((game) => !game.isDeleted));
285289

286290
return Promise.all(
287-
games.map((game) => {
291+
games.map(async (game) => {
288292
const achievementFiles = findAchievementFiles(game);
289-
const achievementFileInsideDirectory =
290-
findAchievementFileInExecutableDirectory(game);
291293

292-
achievementFiles.push(...achievementFileInsideDirectory);
294+
achievementFiles.push(...(await findAchievementFileInSteamPath(game)));
293295

294296
return { game, achievementFiles };
295297
})

src/main/services/achievements/find-achivement-files.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import path from "node:path";
22
import fs from "node:fs";
3-
import type { Game, AchievementFile } from "@types";
3+
import type { Game, AchievementFile, UserPreferences } from "@types";
44
import { Cracker } from "@shared";
55
import { achievementsLogger } from "../logger";
66
import { SystemPath } from "../system-path";
7+
import { getSteamLocation, getSteamUsersIds } from "../steam";
8+
import { db, levelKeys } from "@main/level";
79

810
const getAppDataPath = () => {
911
if (process.platform === "win32") {
@@ -270,6 +272,51 @@ export const findAchievementFiles = (game: Game) => {
270272
}
271273
}
272274

275+
const achievementFileInsideDirectory =
276+
findAchievementFileInExecutableDirectory(game);
277+
278+
return achievementFiles.concat(achievementFileInsideDirectory);
279+
};
280+
281+
const steamUserIds = await getSteamUsersIds();
282+
const steamPath = await getSteamLocation();
283+
284+
export const findAchievementFileInSteamPath = async (game: Game) => {
285+
if (!steamUserIds.length) {
286+
return [];
287+
}
288+
289+
const userPreferences = await db.get<string, UserPreferences | null>(
290+
levelKeys.userPreferences,
291+
{
292+
valueEncoding: "json",
293+
}
294+
);
295+
296+
if (!userPreferences?.enableSteamAchievements) {
297+
return [];
298+
}
299+
300+
const achievementFiles: AchievementFile[] = [];
301+
302+
for (const steamUserId of steamUserIds) {
303+
const gameAchievementPath = path.join(
304+
steamPath,
305+
"userdata",
306+
steamUserId.toString(),
307+
"config",
308+
"librarycache",
309+
`${game.objectId}.json`
310+
);
311+
312+
if (fs.existsSync(gameAchievementPath)) {
313+
achievementFiles.push({
314+
type: Cracker.Steam,
315+
filePath: gameAchievementPath,
316+
});
317+
}
318+
}
319+
273320
return achievementFiles;
274321
};
275322

src/main/services/achievements/parse-achievement-file.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ export const parseAchievementFile = (
7575
return processRazor1911(filePath);
7676
}
7777

78+
if (type === Cracker.Steam) {
79+
const parsed = jsonParse(filePath);
80+
return processSteamCacheAchievement(parsed);
81+
}
82+
7883
achievementsLogger.log(
7984
`Unprocessed ${type} achievements found on ${filePath}`
8085
);
@@ -234,6 +239,35 @@ const processGoldberg = (unlockedAchievements: any): UnlockedAchievement[] => {
234239
return newUnlockedAchievements;
235240
};
236241

242+
const processSteamCacheAchievement = (
243+
unlockedAchievements: any[]
244+
): UnlockedAchievement[] => {
245+
const newUnlockedAchievements: UnlockedAchievement[] = [];
246+
247+
const achievementIndex = unlockedAchievements.findIndex(
248+
(element) => element[0] === "achievements"
249+
);
250+
251+
if (achievementIndex === -1) {
252+
achievementsLogger.info("No achievements found in Steam cache file");
253+
return [];
254+
}
255+
256+
const unlockedAchievementsData =
257+
unlockedAchievements[achievementIndex][1]["data"]["vecHighlight"];
258+
259+
for (const achievement of unlockedAchievementsData) {
260+
if (achievement.bAchieved) {
261+
newUnlockedAchievements.push({
262+
name: achievement.strID,
263+
unlockTime: achievement.rtUnlocked * 1000,
264+
});
265+
}
266+
}
267+
268+
return newUnlockedAchievements;
269+
};
270+
237271
const process3DM = (unlockedAchievements: any): UnlockedAchievement[] => {
238272
const newUnlockedAchievements: UnlockedAchievement[] = [];
239273

src/main/services/process-watcher.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { CloudSync } from "./cloud-sync";
99
import { logger } from "./logger";
1010
import path from "path";
1111
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
12+
import { MAIN_LOOP_INTERVAL } from "@main/constants";
1213

1314
export const gamesPlaytime = new Map<
1415
string,
@@ -25,7 +26,7 @@ interface GameExecutables {
2526
[key: string]: ExecutableInfo[];
2627
}
2728

28-
const TICKS_TO_UPDATE_API = 120;
29+
const TICKS_TO_UPDATE_API = (3 * 60 * 1000) / MAIN_LOOP_INTERVAL; // 3 minutes
2930
let currentTick = 1;
3031

3132
const platform = process.platform;

src/main/services/window-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ export class WindowManager {
582582
tray.popUpContextMenu(contextMenu);
583583
};
584584

585-
tray.setToolTip("Hydra");
585+
tray.setToolTip("Hydra Launcher");
586586

587587
if (process.platform !== "darwin") {
588588
await updateSystemTray();

src/renderer/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6-
<title>Hydra</title>
6+
<title>Hydra Launcher</title>
77
<meta
88
http-equiv="Content-Security-Policy"
99
content="default-src 'self' 'unsafe-inline' * data: local:;"

0 commit comments

Comments
 (0)