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 { getGameAchievementsToWatch } from "./get-game-achievements-to-watch";
import { checkUnlockedAchievements } from "./util/check-unlocked-achievements"; import { checkUnlockedAchievements } from "./util/check-unlocked-achievements";
import { parseAchievementFile } from "./util/parseAchievementFile"; 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 = { type GameAchievementObserver = {
[id: number]: AbortController | null; [id: number]: AbortController;
}; };
const gameAchievementObserver: GameAchievementObserver = {}; const gameAchievementObserver: GameAchievementObserver = {};
export const startGameAchievementObserver = async (gameId: number) => { const processAchievementFile = async (game: Game, file: AchievementFile) => {
if ( const localAchievementFile = await parseAchievementFile(file.filePath);
gameAchievementObserver[gameId] === null || console.log(localAchievementFile);
gameAchievementObserver[gameId]
) { if (localAchievementFile) {
return; 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) { const achievementFiles = await getGameAchievementsToWatch(game.id);
console.log("No achievements to observe");
gameAchievementObserver[gameId] = null;
return;
}
const { steamId, checkedAchievements, achievementFiles } = console.log(
achievementsToWatch; "Achievements files to observe for:",
game.title,
gameAchievementObserver[gameId] = new AbortController(); achievementFiles
);
const achievements = checkedAchievements.all;
for (const file of achievementFiles) { for (const file of achievementFiles) {
const signal = gameAchievementObserver[gameId]?.signal; if (!fs.existsSync(file.filePath)) {
if (!signal) return; continue;
console.log(`cracker: ${file.type}, steamId: ${steamId}`); }
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 () => { (async () => {
try { try {
processAchievementFile(game, file);
const watcher = watch(file.filePath, { const watcher = watch(file.filePath, {
signal, signal,
}); });
for await (const event of watcher) { for await (const event of watcher) {
if (event.eventType === "change") { if (event.eventType === "change") {
console.log("file modified"); processAchievementFile(game, file);
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),
}
);
}
} }
} }
} catch (err: any) { } catch (err: any) {
console.log(`cracker: ${file.type}, steamId ${steamId}`);
if (err?.name === "AbortError") return; if (err?.name === "AbortError") return;
console.log(`cracker: ${file.type}, steamId ${game.objectID}`);
throw err; throw err;
} }
})(); })();

View file

@ -1,61 +1,25 @@
import { gameRepository, gameAchievementRepository } from "@main/repository"; import { gameRepository } from "@main/repository";
import { steamGetAchivement } from "./steam/steam-get-achivement";
import { steamFindGameAchievementFiles } from "./steam/steam-find-game-achivement-files"; import { steamFindGameAchievementFiles } from "./steam/steam-find-game-achivement-files";
import { AchievementFile, CheckedAchievements } from "./types"; import { AchievementFile } from "./types";
import { parseAchievementFile } from "./util/parseAchievementFile";
import { checkUnlockedAchievements } from "./util/check-unlocked-achievements";
export const getGameAchievementsToWatch = async ( export const getGameAchievementsToWatch = async (
gameId: number gameId: number
): Promise< ): Promise<AchievementFile[]> => {
| {
steamId: number;
checkedAchievements: CheckedAchievements;
achievementFiles: AchievementFile[];
}
| undefined
> => {
const game = await gameRepository.findOne({ where: { id: gameId } }); 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 steamId = Number(game.objectID);
const achievements = await steamGetAchivement(game);
console.log(achievements);
if (!achievements || !achievements.length) return;
const achievementFiles = steamFindGameAchievementFiles(game.objectID)[ const achievementFiles = steamFindGameAchievementFiles(game.objectID)[
steamId steamId
]; ];
console.log(achievementFiles); console.log(
if (!achievementFiles || !achievementFiles.length) return; "achivements files:",
achievementFiles,
game.title,
game.objectID
);
const checkedAchievements: CheckedAchievements = { return achievementFiles || [];
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 };
}; };

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 { steamFindGameAchievementFiles } from "./steam/steam-find-game-achivement-files";
import { parseAchievementFile } from "./util/parseAchievementFile"; import { parseAchievementFile } from "./util/parseAchievementFile";
import { HydraApi } from "@main/services"; 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 () => { export const saveAllLocalSteamAchivements = async () => {
const gameAchievementFiles = steamFindGameAchievementFiles(); const gameAchievementFiles = steamFindGameAchievementFiles();
for (const key of Object.keys(gameAchievementFiles)) { for (const objectId of Object.keys(gameAchievementFiles)) {
const objectId = key;
const [game, localAchievements] = await Promise.all([ const [game, localAchievements] = await Promise.all([
gameRepository.findOne({ gameRepository.findOne({
where: { objectID: objectId, shop: "steam", isDeleted: false }, where: { objectID: objectId, shop: "steam", isDeleted: false },
@ -42,40 +43,20 @@ export const saveAllLocalSteamAchivements = async () => {
.catch(console.log); .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( const localAchievementFile = await parseAchievementFile(
achievementFile.filePath achievementFile.filePath
); );
console.log(achievementFile.filePath); console.log(achievementFile.filePath);
for (const a of Object.keys(localAchievementFile)) { unlockedAchievements.push(
// TODO: use checkUnlockedAchievements after refactoring it to be generic ...checkUnlockedAchievements(achievementFile.type, localAchievementFile)
unlockedAchievements.push({ );
name: a,
unlockTime: localAchievementFile[a].UnlockTime,
});
}
} }
gameAchievementRepository.upsert( mergeAchievements(objectId, "steam", unlockedAchievements);
{
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");
});
}
} }
}; };

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); const filePath = path.join(achievementPath, objectId, ...fileLocation);
if (fs.existsSync(filePath)) { const achivementFile = {
const achivementFile = { type,
type, filePath,
filePath, };
};
achievementFiles[objectId] achievementFiles[objectId]
? achievementFiles[objectId].push(achivementFile) ? achievementFiles[objectId].push(achivementFile)
: (achievementFiles[objectId] = [achivementFile]); : (achievementFiles[objectId] = [achivementFile]);
}
}; };
export const steamFindGameAchievementFiles = ( export const steamFindGameAchievementFiles = (
@ -55,30 +53,16 @@ export const steamFindGameAchievementFiles = (
fileLocation = ["achievements.ini"]; fileLocation = ["achievements.ini"];
} }
if (!fs.existsSync(achievementPath)) continue; const objectIds = objectId ? [objectId] : fs.readdirSync(achievementPath);
const objectIds = fs.readdirSync(achievementPath); for (const objectId of objectIds) {
addGame(
if (objectId) { gameAchievementFiles,
if (objectIds.includes(objectId)) { achievementPath,
addGame( objectId,
gameAchievementFiles, fileLocation,
achievementPath, cracker
objectId, );
fileLocation,
cracker
);
}
} else {
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[]; new: Achievement[];
} }
export interface UnlockedAchievement {
name: string;
unlockTime: number;
}
export interface Achievement { export interface Achievement {
id: string; id: string;
percent: number; percent: number;

View file

@ -1,102 +1,63 @@
import { Achievement, CheckedAchievements, Cracker } from "../types"; import { Cracker, UnlockedAchievement } from "../types";
export const checkUnlockedAchievements = ( export const checkUnlockedAchievements = (
type: Cracker, type: Cracker,
unlockedAchievements: any, unlockedAchievements: any
achievements: Achievement[] ): UnlockedAchievement[] => {
): CheckedAchievements => { if (type === Cracker.onlineFix) return onlineFixMerge(unlockedAchievements);
if (type === Cracker.onlineFix)
return onlineFixMerge(unlockedAchievements, achievements);
if (type === Cracker.goldberg) if (type === Cracker.goldberg)
return goldbergUnlockedAchievements(unlockedAchievements, achievements); return goldbergUnlockedAchievements(unlockedAchievements);
return defaultMerge(unlockedAchievements, achievements); return defaultMerge(unlockedAchievements);
}; };
const onlineFixMerge = ( const onlineFixMerge = (unlockedAchievements: any): UnlockedAchievement[] => {
unlockedAchievements: any, const parsedUnlockedAchievements: UnlockedAchievement[] = [];
achievements: Achievement[]
): CheckedAchievements => {
const newUnlockedAchievements: Achievement[] = [];
for (const achievement of achievements) { for (const achievement of Object.keys(unlockedAchievements)) {
if (achievement.achieved) continue; const unlockedAchievement = unlockedAchievements[achievement];
const unlockedAchievement = unlockedAchievements[achievement.id]; if (unlockedAchievement?.achieved) {
parsedUnlockedAchievements.push({
if (!unlockedAchievement) continue; name: achievement,
unlockTime: unlockedAchievement.timestamp,
achievement.achieved = Boolean( });
unlockedAchievement?.achieved ?? achievement.achieved
);
achievement.unlockTime =
unlockedAchievement?.timestamp ?? achievement.unlockTime;
if (achievement.achieved) {
newUnlockedAchievements.push(achievement);
} }
} }
return { all: achievements, new: newUnlockedAchievements }; return parsedUnlockedAchievements;
}; };
const goldbergUnlockedAchievements = ( const goldbergUnlockedAchievements = (
unlockedAchievements: any, unlockedAchievements: any
achievements: Achievement[] ): UnlockedAchievement[] => {
): CheckedAchievements => { const newUnlockedAchievements: UnlockedAchievement[] = [];
const newUnlockedAchievements: Achievement[] = [];
for (const achievement of achievements) { for (const achievement of Object.keys(unlockedAchievements)) {
if (achievement.achieved) continue; const unlockedAchievement = unlockedAchievements[achievement];
const unlockedAchievement = unlockedAchievements[achievement.id]; if (unlockedAchievement?.earned) {
newUnlockedAchievements.push({
if (!unlockedAchievement) continue; name: achievement,
unlockTime: unlockedAchievement.earned_time,
achievement.achieved = Boolean( });
unlockedAchievement?.earned ?? achievement.achieved
);
achievement.unlockTime =
unlockedAchievement?.earned_time ?? achievement.unlockTime;
if (achievement.achieved) {
newUnlockedAchievements.push(achievement);
} }
} }
return { all: achievements, new: newUnlockedAchievements }; return newUnlockedAchievements;
}; };
const defaultMerge = ( const defaultMerge = (unlockedAchievements: any): UnlockedAchievement[] => {
unlockedAchievements: any, const newUnlockedAchievements: UnlockedAchievement[] = [];
achievements: Achievement[]
): CheckedAchievements => {
const newUnlockedAchievements: Achievement[] = [];
console.log("checkUnlockedAchievements");
for (const achievement of achievements) {
if (achievement.achieved) continue;
const unlockedAchievement = unlockedAchievements[achievement.id]; for (const achievement of Object.keys(unlockedAchievements)) {
const unlockedAchievement = unlockedAchievements[achievement];
if (!unlockedAchievement) continue; if (unlockedAchievement?.Achieved) {
newUnlockedAchievements.push({
achievement.achieved = Boolean( name: achievement,
unlockedAchievement?.Achieved ?? achievement.achieved unlockTime: unlockedAchievement.UnlockTime,
); });
achievement.curProgress =
unlockedAchievement?.CurProgress ?? achievement.curProgress;
achievement.maxProgress =
unlockedAchievement?.MaxProgress ?? achievement.maxProgress;
achievement.unlockTime =
unlockedAchievement?.UnlockTime ?? achievement.unlockTime;
if (achievement.achieved) {
newUnlockedAchievements.push(achievement);
} }
} }
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(() => {}); createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {});
} }
startGameAchievementObserver(game.id); startGameAchievementObserver(game);
} }
function onTickGame(game: Game) { function onTickGame(game: Game) {
@ -116,6 +116,8 @@ function onTickGame(game: Game) {
}) })
.catch(() => {}); .catch(() => {});
} }
startGameAchievementObserver(game);
} }
const onCloseGame = (game: Game) => { const onCloseGame = (game: Game) => {

View file

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