feat: real time achievement track

This commit is contained in:
Zamitto 2024-09-26 13:37:33 -03:00
parent 24d21b9839
commit 5c790edb2c
14 changed files with 181 additions and 407 deletions

View file

@ -2,80 +2,77 @@ import { watch } from "node:fs/promises";
import { getGameAchievementsToWatch } from "./get-game-achievements-to-watch";
import { checkUnlockedAchievements } from "./util/check-unlocked-achievements";
import { parseAchievementFile } from "./util/parseAchievementFile";
import { gameAchievementRepository } from "@main/repository";
import { Game } from "@main/entity";
import { mergeAchievements } from "./merge-achievements";
import fs from "node:fs";
import { AchievementFile } from "./types";
type GameAchievementObserver = {
[id: number]: AbortController | null;
[id: number]: AbortController;
};
const gameAchievementObserver: GameAchievementObserver = {};
export const startGameAchievementObserver = async (gameId: number) => {
if (
gameAchievementObserver[gameId] === null ||
gameAchievementObserver[gameId]
) {
return;
const processAchievementFile = async (game: Game, file: AchievementFile) => {
const localAchievementFile = await parseAchievementFile(file.filePath);
console.log(localAchievementFile);
if (localAchievementFile) {
const unlockedAchievements = checkUnlockedAchievements(
file.type,
localAchievementFile
);
console.log(unlockedAchievements);
if (unlockedAchievements.length) {
mergeAchievements(game.objectID, game.shop, unlockedAchievements);
}
}
};
console.log(`Starting: ${gameId}`);
export const startGameAchievementObserver = async (game: Game) => {
if (gameAchievementObserver[game.id]) return;
const achievementsToWatch = await getGameAchievementsToWatch(gameId);
console.log(`Starting: ${game.title}`);
if (!achievementsToWatch) {
console.log("No achievements to observe");
gameAchievementObserver[gameId] = null;
return;
}
const achievementFiles = await getGameAchievementsToWatch(game.id);
const { steamId, checkedAchievements, achievementFiles } =
achievementsToWatch;
gameAchievementObserver[gameId] = new AbortController();
const achievements = checkedAchievements.all;
console.log(
"Achievements files to observe for:",
game.title,
achievementFiles
);
for (const file of achievementFiles) {
const signal = gameAchievementObserver[gameId]?.signal;
if (!signal) return;
console.log(`cracker: ${file.type}, steamId: ${steamId}`);
if (!fs.existsSync(file.filePath)) {
continue;
}
console.log(`cracker: ${file.type}, objectId: ${game.objectID}`);
if (!gameAchievementObserver[game.id]) {
const abortController = new AbortController();
gameAchievementObserver[game.id] = abortController;
}
const signal = gameAchievementObserver[game.id]?.signal;
(async () => {
try {
processAchievementFile(game, file);
const watcher = watch(file.filePath, {
signal,
});
for await (const event of watcher) {
if (event.eventType === "change") {
console.log("file modified");
const localAchievementFile = await parseAchievementFile(
file.filePath
);
if (!localAchievementFile) continue;
const checked = checkUnlockedAchievements(
file.type,
localAchievementFile,
achievements
);
if (checked.new) {
console.log(checked.new);
gameAchievementRepository.update(
{
game: { id: steamId },
},
{
achievements: JSON.stringify(checked.all),
}
);
}
processAchievementFile(game, file);
}
}
} catch (err: any) {
console.log(`cracker: ${file.type}, steamId ${steamId}`);
if (err?.name === "AbortError") return;
console.log(`cracker: ${file.type}, steamId ${game.objectID}`);
throw err;
}
})();

View file

@ -1,61 +1,25 @@
import { gameRepository, gameAchievementRepository } from "@main/repository";
import { steamGetAchivement } from "./steam/steam-get-achivement";
import { gameRepository } from "@main/repository";
import { steamFindGameAchievementFiles } from "./steam/steam-find-game-achivement-files";
import { AchievementFile, CheckedAchievements } from "./types";
import { parseAchievementFile } from "./util/parseAchievementFile";
import { checkUnlockedAchievements } from "./util/check-unlocked-achievements";
import { AchievementFile } from "./types";
export const getGameAchievementsToWatch = async (
gameId: number
): Promise<
| {
steamId: number;
checkedAchievements: CheckedAchievements;
achievementFiles: AchievementFile[];
}
| undefined
> => {
): Promise<AchievementFile[]> => {
const game = await gameRepository.findOne({ where: { id: gameId } });
if (!game || game.shop !== "steam") return;
if (!game || game.shop !== "steam") return [];
const steamId = Number(game.objectID);
const achievements = await steamGetAchivement(game);
console.log(achievements);
if (!achievements || !achievements.length) return;
const achievementFiles = steamFindGameAchievementFiles(game.objectID)[
steamId
];
console.log(achievementFiles);
if (!achievementFiles || !achievementFiles.length) return;
console.log(
"achivements files:",
achievementFiles,
game.title,
game.objectID
);
const checkedAchievements: CheckedAchievements = {
all: achievements,
new: [],
};
for (const achievementFile of achievementFiles) {
const file = await parseAchievementFile(achievementFile.filePath);
checkedAchievements.new.push(
...checkUnlockedAchievements(achievementFile.type, file, achievements).new
);
}
if (checkedAchievements.new.length) {
await gameAchievementRepository.update(
{
game: { id: gameId },
},
{
unlockedAchievements: JSON.stringify(checkedAchievements.all),
}
);
}
return { steamId, checkedAchievements, achievementFiles };
return achievementFiles || [];
};

View file

@ -0,0 +1,51 @@
import { gameAchievementRepository } from "@main/repository";
import { UnlockedAchievement } from "./types";
export const mergeAchievements = async (
objectId: string,
shop: string,
achievements: UnlockedAchievement[]
) => {
const localGameAchievement = await gameAchievementRepository.findOne({
where: {
objectId,
shop,
},
});
const unlockedAchievements = JSON.parse(
localGameAchievement?.unlockedAchievements || "[]"
);
console.log("file achievemets:", achievements);
const newAchievements = achievements.filter((achievement) => {
return !unlockedAchievements.some((localAchievement) => {
return localAchievement.name === achievement.name;
});
});
const mergedAchievements = unlockedAchievements.concat(newAchievements);
console.log("merged achievemetns", mergedAchievements);
gameAchievementRepository.upsert(
{
objectId,
shop,
unlockedAchievements: JSON.stringify(mergedAchievements),
},
["objectId", "shop"]
);
// return HydraApi.get("/profile/games/achievements").then(async (response) => {
// console.log(response);
// });
// if (game.remoteId) {
// HydraApi.put("/profile/games/achievements", {
// id: game.remoteId,
// achievements: unlockedAchievements,
// }).catch(() => {
// console.log("erro");
// });
// }
};

View file

@ -1,7 +0,0 @@
import { HydraApi } from "../hydra-api";
export const mergeWithRemoteAchievements = async () => {
return HydraApi.get("/profile/games/achievements").then(async (response) => {
console.log(response);
});
};

View file

@ -2,13 +2,14 @@ import { gameAchievementRepository, gameRepository } from "@main/repository";
import { steamFindGameAchievementFiles } from "./steam/steam-find-game-achivement-files";
import { parseAchievementFile } from "./util/parseAchievementFile";
import { HydraApi } from "@main/services";
import { checkUnlockedAchievements } from "./util/check-unlocked-achievements";
import { mergeAchievements } from "./merge-achievements";
import { UnlockedAchievement } from "./types";
export const saveAllLocalSteamAchivements = async () => {
const gameAchievementFiles = steamFindGameAchievementFiles();
for (const key of Object.keys(gameAchievementFiles)) {
const objectId = key;
for (const objectId of Object.keys(gameAchievementFiles)) {
const [game, localAchievements] = await Promise.all([
gameRepository.findOne({
where: { objectID: objectId, shop: "steam", isDeleted: false },
@ -42,40 +43,20 @@ export const saveAllLocalSteamAchivements = async () => {
.catch(console.log);
}
const unlockedAchievements: { name: string; unlockTime: number }[] = [];
const unlockedAchievements: UnlockedAchievement[] = [];
for (const achievementFile of gameAchievementFiles[key]) {
for (const achievementFile of gameAchievementFiles[objectId]) {
const localAchievementFile = await parseAchievementFile(
achievementFile.filePath
);
console.log(achievementFile.filePath);
for (const a of Object.keys(localAchievementFile)) {
// TODO: use checkUnlockedAchievements after refactoring it to be generic
unlockedAchievements.push({
name: a,
unlockTime: localAchievementFile[a].UnlockTime,
});
}
unlockedAchievements.push(
...checkUnlockedAchievements(achievementFile.type, localAchievementFile)
);
}
gameAchievementRepository.upsert(
{
objectId,
shop: "steam",
unlockedAchievements: JSON.stringify(unlockedAchievements),
},
["objectId", "shop"]
);
if (game.remoteId) {
HydraApi.put("/profile/games/achievements", {
id: game.remoteId,
achievements: unlockedAchievements,
}).catch(() => {
console.log("erro");
});
}
mergeAchievements(objectId, "steam", unlockedAchievements);
}
};

View file

@ -1,54 +0,0 @@
import { logger } from "@main/services";
import { AchievementInfo } from "../types";
import { JSDOM } from "jsdom";
export const steamAchievementInfo = async (
objectId: string
): Promise<AchievementInfo[] | undefined> => {
const fetchUrl = `https://steamcommunity.com/stats/${objectId}/achievements`;
const achievementInfosHtmlText = await fetch(fetchUrl, {
method: "GET",
//headers: { "Accept-Language": "" },
})
.then((res) => {
if (res.status === 200) return res.text();
throw new Error();
})
.catch((err) => {
logger.error(err, { method: "getSteamGameAchievements" });
return;
});
if (!achievementInfosHtmlText) return;
const achievementInfos: AchievementInfo[] = [];
const window = new JSDOM(achievementInfosHtmlText).window;
const itens = Array.from(
window.document.getElementsByClassName("achieveRow")
);
for (const item of itens) {
const imageUrl = item
.getElementsByClassName("achieveImgHolder")?.[0]
.getElementsByTagName("img")?.[0]?.src;
const achievementName = item
.getElementsByClassName("achieveTxt")?.[0]
.getElementsByTagName("h3")?.[0].innerHTML;
const achievementDescription = item
.getElementsByClassName("achieveTxt")?.[0]
.getElementsByTagName("h5")?.[0].innerHTML;
achievementInfos.push({
imageUrl: imageUrl ?? "",
title: achievementName ?? "",
description: achievementDescription ?? "",
});
}
return achievementInfos;
};

View file

@ -1,28 +0,0 @@
import { Achievement, AchievementInfo, AchievementPercentage } from "../types";
export const steamAchievementMerge = (
achievementPercentage: AchievementPercentage[],
achievementInfo: AchievementInfo[]
): Achievement[] | undefined => {
if (achievementPercentage.length > achievementInfo.length) return;
const size = achievementPercentage.length;
const achievements: Achievement[] = new Array(size);
for (let i = 0; i < size; i++) {
achievements[i] = {
id: achievementPercentage[i].name,
percent: achievementPercentage[i].percent,
imageUrl: achievementInfo[i].imageUrl,
title: achievementInfo[i].title,
description: achievementInfo[i].description,
achieved: false,
curProgress: 0,
maxProgress: 0,
unlockTime: 0,
};
}
return achievements;
};

View file

@ -12,16 +12,14 @@ const addGame = (
) => {
const filePath = path.join(achievementPath, objectId, ...fileLocation);
if (fs.existsSync(filePath)) {
const achivementFile = {
type,
filePath,
};
const achivementFile = {
type,
filePath,
};
achievementFiles[objectId]
? achievementFiles[objectId].push(achivementFile)
: (achievementFiles[objectId] = [achivementFile]);
}
achievementFiles[objectId]
? achievementFiles[objectId].push(achivementFile)
: (achievementFiles[objectId] = [achivementFile]);
};
export const steamFindGameAchievementFiles = (
@ -55,30 +53,16 @@ export const steamFindGameAchievementFiles = (
fileLocation = ["achievements.ini"];
}
if (!fs.existsSync(achievementPath)) continue;
const objectIds = objectId ? [objectId] : fs.readdirSync(achievementPath);
const objectIds = fs.readdirSync(achievementPath);
if (objectId) {
if (objectIds.includes(objectId)) {
addGame(
gameAchievementFiles,
achievementPath,
objectId,
fileLocation,
cracker
);
}
} else {
for (const objectId of objectIds) {
addGame(
gameAchievementFiles,
achievementPath,
objectId,
fileLocation,
cracker
);
}
for (const objectId of objectIds) {
addGame(
gameAchievementFiles,
achievementPath,
objectId,
fileLocation,
cracker
);
}
}

View file

@ -1,48 +0,0 @@
import { gameAchievementRepository } from "@main/repository";
import { steamGlobalAchievementPercentages } from "./steam-global-achievement-percentages";
import { steamAchievementInfo } from "./steam-achievement-info";
import { steamAchievementMerge } from "./steam-achievement-merge";
import { Achievement } from "../types";
import { Game } from "@main/entity";
export const steamGetAchivement = async (
game: Game
): Promise<Achievement[] | undefined> => {
const gameAchivement = await gameAchievementRepository.findOne({
where: { game: game },
});
if (!gameAchivement) {
const achievementPercentage = await steamGlobalAchievementPercentages(
game.objectID
);
console.log(achievementPercentage);
if (!achievementPercentage) {
await gameAchievementRepository.save({
game,
achievements: "[]",
});
return [];
}
const achievementInfo = await steamAchievementInfo(game.objectID);
console.log(achievementInfo);
if (!achievementInfo) return;
const achievements = steamAchievementMerge(
achievementPercentage,
achievementInfo
);
if (!achievements) return;
await gameAchievementRepository.save({
game,
achievements: JSON.stringify(achievements),
});
return achievements;
} else {
return JSON.parse(gameAchivement.achievements);
}
};

View file

@ -1,33 +0,0 @@
import { logger } from "@main/services";
import { AchievementPercentage } from "../types";
interface GlobalAchievementPercentages {
achievementpercentages: {
achievements: Array<AchievementPercentage>;
};
}
export const steamGlobalAchievementPercentages = async (
objectId: string
): Promise<AchievementPercentage[] | undefined> => {
const fetchUrl = `https://api.steampowered.com/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v0002/?gameid=${objectId}`;
const achievementPercentages: Array<AchievementPercentage> | undefined = (
await fetch(fetchUrl, {
method: "GET",
})
.then((res) => {
if (res.status === 200) return res.json();
return;
})
.then((data: GlobalAchievementPercentages) => data)
.catch((err) => {
logger.error(err, { method: "getSteamGameAchievements" });
return;
})
)?.achievementpercentages.achievements;
if (!achievementPercentages) return;
return achievementPercentages;
};

View file

@ -10,6 +10,11 @@ export interface CheckedAchievements {
new: Achievement[];
}
export interface UnlockedAchievement {
name: string;
unlockTime: number;
}
export interface Achievement {
id: string;
percent: number;

View file

@ -1,102 +1,63 @@
import { Achievement, CheckedAchievements, Cracker } from "../types";
import { Cracker, UnlockedAchievement } from "../types";
export const checkUnlockedAchievements = (
type: Cracker,
unlockedAchievements: any,
achievements: Achievement[]
): CheckedAchievements => {
if (type === Cracker.onlineFix)
return onlineFixMerge(unlockedAchievements, achievements);
unlockedAchievements: any
): UnlockedAchievement[] => {
if (type === Cracker.onlineFix) return onlineFixMerge(unlockedAchievements);
if (type === Cracker.goldberg)
return goldbergUnlockedAchievements(unlockedAchievements, achievements);
return defaultMerge(unlockedAchievements, achievements);
return goldbergUnlockedAchievements(unlockedAchievements);
return defaultMerge(unlockedAchievements);
};
const onlineFixMerge = (
unlockedAchievements: any,
achievements: Achievement[]
): CheckedAchievements => {
const newUnlockedAchievements: Achievement[] = [];
const onlineFixMerge = (unlockedAchievements: any): UnlockedAchievement[] => {
const parsedUnlockedAchievements: UnlockedAchievement[] = [];
for (const achievement of achievements) {
if (achievement.achieved) continue;
for (const achievement of Object.keys(unlockedAchievements)) {
const unlockedAchievement = unlockedAchievements[achievement];
const unlockedAchievement = unlockedAchievements[achievement.id];
if (!unlockedAchievement) continue;
achievement.achieved = Boolean(
unlockedAchievement?.achieved ?? achievement.achieved
);
achievement.unlockTime =
unlockedAchievement?.timestamp ?? achievement.unlockTime;
if (achievement.achieved) {
newUnlockedAchievements.push(achievement);
if (unlockedAchievement?.achieved) {
parsedUnlockedAchievements.push({
name: achievement,
unlockTime: unlockedAchievement.timestamp,
});
}
}
return { all: achievements, new: newUnlockedAchievements };
return parsedUnlockedAchievements;
};
const goldbergUnlockedAchievements = (
unlockedAchievements: any,
achievements: Achievement[]
): CheckedAchievements => {
const newUnlockedAchievements: Achievement[] = [];
unlockedAchievements: any
): UnlockedAchievement[] => {
const newUnlockedAchievements: UnlockedAchievement[] = [];
for (const achievement of achievements) {
if (achievement.achieved) continue;
for (const achievement of Object.keys(unlockedAchievements)) {
const unlockedAchievement = unlockedAchievements[achievement];
const unlockedAchievement = unlockedAchievements[achievement.id];
if (!unlockedAchievement) continue;
achievement.achieved = Boolean(
unlockedAchievement?.earned ?? achievement.achieved
);
achievement.unlockTime =
unlockedAchievement?.earned_time ?? achievement.unlockTime;
if (achievement.achieved) {
newUnlockedAchievements.push(achievement);
if (unlockedAchievement?.earned) {
newUnlockedAchievements.push({
name: achievement,
unlockTime: unlockedAchievement.earned_time,
});
}
}
return { all: achievements, new: newUnlockedAchievements };
return newUnlockedAchievements;
};
const defaultMerge = (
unlockedAchievements: any,
achievements: Achievement[]
): CheckedAchievements => {
const newUnlockedAchievements: Achievement[] = [];
console.log("checkUnlockedAchievements");
for (const achievement of achievements) {
if (achievement.achieved) continue;
const defaultMerge = (unlockedAchievements: any): UnlockedAchievement[] => {
const newUnlockedAchievements: UnlockedAchievement[] = [];
const unlockedAchievement = unlockedAchievements[achievement.id];
for (const achievement of Object.keys(unlockedAchievements)) {
const unlockedAchievement = unlockedAchievements[achievement];
if (!unlockedAchievement) continue;
achievement.achieved = Boolean(
unlockedAchievement?.Achieved ?? achievement.achieved
);
achievement.curProgress =
unlockedAchievement?.CurProgress ?? achievement.curProgress;
achievement.maxProgress =
unlockedAchievement?.MaxProgress ?? achievement.maxProgress;
achievement.unlockTime =
unlockedAchievement?.UnlockTime ?? achievement.unlockTime;
if (achievement.achieved) {
newUnlockedAchievements.push(achievement);
if (unlockedAchievement?.Achieved) {
newUnlockedAchievements.push({
name: achievement,
unlockTime: unlockedAchievement.UnlockTime,
});
}
}
console.log("newUnlocked: ", newUnlockedAchievements);
return { all: achievements, new: newUnlockedAchievements };
return newUnlockedAchievements;
};

View file

@ -79,7 +79,7 @@ function onOpenGame(game: Game) {
createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {});
}
startGameAchievementObserver(game.id);
startGameAchievementObserver(game);
}
function onTickGame(game: Game) {
@ -116,6 +116,8 @@ function onTickGame(game: Game) {
})
.catch(() => {});
}
startGameAchievementObserver(game);
}
const onCloseGame = (game: Game) => {

View file

@ -123,7 +123,6 @@ export function GameDetailsContextProvider({
if (statsResult.status === "fulfilled") setStats(statsResult.value);
if (achievements.status === "fulfilled") {
console.log(achievements.value);
setAchievements(achievements.value);
}
})