feat: parse achievements from steam local cache

This commit is contained in:
Zamitto
2025-06-09 06:49:23 -03:00
parent 8eeacf478d
commit 63374ccd74
5 changed files with 110 additions and 2 deletions

View File

@@ -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 };
})
);

View File

@@ -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[] => {

View File

@@ -19,6 +19,32 @@ const getModifiedSinceHeader = (
: undefined;
};
const getModifiedSinceHeader = async (
cachedAchievements: GameAchievement | undefined
): Promise<Date | undefined> => {
const hasActiveSubscription = await db
.get<string, User>(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,

View File

@@ -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[] = [];

View File

@@ -35,6 +35,7 @@ export enum Cracker {
onlineFix = "OnlineFix",
goldberg = "Goldberg",
userstats = "user_stats",
Steam = "Steam",
rld = "RLD!",
empress = "EMPRESS",
skidrow = "SKIDROW",