mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-26 20:31:03 +00:00
feat: adding change hero
This commit is contained in:
81
src/main/services/achievements/achievement-file-observer.ts
Normal file
81
src/main/services/achievements/achievement-file-observer.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { parseAchievementFile } from "./parse-achievement-file";
|
||||
import { Game } from "@main/entity";
|
||||
import { mergeAchievements } from "./merge-achievements";
|
||||
import fs from "node:fs";
|
||||
import {
|
||||
findAchievementFileInExecutableDirectory,
|
||||
findAllAchievementFiles,
|
||||
} from "./find-achivement-files";
|
||||
import type { AchievementFile } from "@types";
|
||||
import { logger } from "../logger";
|
||||
|
||||
const fileStats: Map<string, number> = new Map();
|
||||
|
||||
const processAchievementFileDiff = async (
|
||||
game: Game,
|
||||
file: AchievementFile
|
||||
) => {
|
||||
const unlockedAchievements = await parseAchievementFile(
|
||||
file.filePath,
|
||||
file.type
|
||||
);
|
||||
|
||||
logger.log("Achievements from file", file.filePath, unlockedAchievements);
|
||||
|
||||
if (unlockedAchievements.length) {
|
||||
return mergeAchievements(
|
||||
game.objectID,
|
||||
game.shop,
|
||||
unlockedAchievements,
|
||||
true
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const compareFile = async (game: Game, file: AchievementFile) => {
|
||||
try {
|
||||
const stat = fs.statSync(file.filePath);
|
||||
const currentFileStat = fileStats.get(file.filePath);
|
||||
fileStats.set(file.filePath, stat.mtimeMs);
|
||||
|
||||
if (!currentFileStat || currentFileStat === stat.mtimeMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log(
|
||||
"Detected change in file",
|
||||
file.filePath,
|
||||
stat.mtimeMs,
|
||||
fileStats.get(file.filePath)
|
||||
);
|
||||
await processAchievementFileDiff(game, file);
|
||||
} catch (err) {
|
||||
fileStats.set(file.filePath, -1);
|
||||
}
|
||||
};
|
||||
|
||||
export const checkAchievementFileChange = async (games: Game[]) => {
|
||||
const achievementFiles = await findAllAchievementFiles();
|
||||
|
||||
for (const game of games) {
|
||||
const gameAchievementFiles = achievementFiles.get(game.objectID) || [];
|
||||
const achievementFileInsideDirectory =
|
||||
findAchievementFileInExecutableDirectory(game);
|
||||
|
||||
if (achievementFileInsideDirectory) {
|
||||
gameAchievementFiles.push(achievementFileInsideDirectory);
|
||||
}
|
||||
|
||||
if (!gameAchievementFiles.length) continue;
|
||||
|
||||
logger.log(
|
||||
"Achievements files to observe for:",
|
||||
game.title,
|
||||
gameAchievementFiles
|
||||
);
|
||||
|
||||
for (const file of gameAchievementFiles) {
|
||||
compareFile(game, file);
|
||||
}
|
||||
}
|
||||
};
|
||||
14
src/main/services/achievements/achievement-watcher.ts
Normal file
14
src/main/services/achievements/achievement-watcher.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { checkAchievementFileChange as searchForAchievements } from "./achievement-file-observer";
|
||||
|
||||
export const watchAchievements = async () => {
|
||||
const games = await gameRepository.find({
|
||||
where: {
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (games.length === 0) return;
|
||||
|
||||
await searchForAchievements(games);
|
||||
};
|
||||
215
src/main/services/achievements/find-achivement-files.ts
Normal file
215
src/main/services/achievements/find-achivement-files.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import { app } from "electron";
|
||||
import type { AchievementFile } from "@types";
|
||||
import { Cracker } from "@shared";
|
||||
import { Game } from "@main/entity";
|
||||
import { achievementsLogger } from "../logger";
|
||||
|
||||
//TODO: change to a automatized method
|
||||
const publicDocuments = path.join("C:", "Users", "Public", "Documents");
|
||||
const programData = path.join("C:", "ProgramData");
|
||||
const appData = app.getPath("appData");
|
||||
const documents = app.getPath("documents");
|
||||
const localAppData = path.join(appData, "..", "Local");
|
||||
|
||||
const crackers = [
|
||||
Cracker.codex,
|
||||
Cracker.goldberg,
|
||||
Cracker.rune,
|
||||
Cracker.onlineFix,
|
||||
Cracker.userstats,
|
||||
Cracker.rld,
|
||||
Cracker.creamAPI,
|
||||
Cracker.skidrow,
|
||||
Cracker.smartSteamEmu,
|
||||
Cracker.empress,
|
||||
];
|
||||
|
||||
const getPathFromCracker = async (cracker: Cracker) => {
|
||||
if (cracker === Cracker.codex) {
|
||||
return [
|
||||
{
|
||||
folderPath: path.join(publicDocuments, "Steam", "CODEX"),
|
||||
fileLocation: ["achievements.ini"],
|
||||
},
|
||||
{
|
||||
folderPath: path.join(appData, "Steam", "CODEX"),
|
||||
fileLocation: ["achievements.ini"],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (cracker === Cracker.rune) {
|
||||
return [
|
||||
{
|
||||
folderPath: path.join(publicDocuments, "Steam", "RUNE"),
|
||||
fileLocation: ["achievements.ini"],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (cracker === Cracker.onlineFix) {
|
||||
return [
|
||||
{
|
||||
folderPath: path.join(publicDocuments, Cracker.onlineFix),
|
||||
fileLocation: ["Stats", "Achievements.ini"],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (cracker === Cracker.goldberg) {
|
||||
return [
|
||||
{
|
||||
folderPath: path.join(appData, "Goldberg SteamEmu Saves"),
|
||||
fileLocation: ["achievements.json"],
|
||||
},
|
||||
{
|
||||
folderPath: path.join(appData, "GSE Saves"),
|
||||
fileLocation: ["achievements.json"],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (cracker === Cracker.userstats) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (cracker === Cracker.rld) {
|
||||
return [
|
||||
{
|
||||
folderPath: path.join(programData, "RLD!"),
|
||||
fileLocation: ["achievements.ini"],
|
||||
},
|
||||
{
|
||||
folderPath: path.join(programData, "Steam", "Player"),
|
||||
fileLocation: ["stats", "achievements.ini"],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (cracker === Cracker.empress) {
|
||||
return [
|
||||
{
|
||||
folderPath: path.join(appData, "EMPRESS", "remote"),
|
||||
fileLocation: ["achievements.json"],
|
||||
},
|
||||
{
|
||||
folderPath: path.join(publicDocuments, "EMPRESS", "remote"),
|
||||
fileLocation: ["achievements.json"],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (cracker === Cracker.skidrow) {
|
||||
return [
|
||||
{
|
||||
folderPath: path.join(documents, "SKIDROW"),
|
||||
fileLocation: ["SteamEmu", "UserStats", "achiev.ini"],
|
||||
},
|
||||
{
|
||||
folderPath: path.join(documents, "Player"),
|
||||
fileLocation: ["SteamEmu", "UserStats", "achiev.ini"],
|
||||
},
|
||||
{
|
||||
folderPath: path.join(localAppData, "SKIDROW"),
|
||||
fileLocation: ["SteamEmu", "UserStats", "achiev.ini"],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (cracker === Cracker.creamAPI) {
|
||||
return [
|
||||
{
|
||||
folderPath: path.join(appData, "CreamAPI"),
|
||||
fileLocation: ["stats", "CreamAPI.Achievements.cfg"],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (cracker === Cracker.smartSteamEmu) {
|
||||
return [
|
||||
{
|
||||
folderPath: path.join(appData, "SmartSteamEmu"),
|
||||
fileLocation: ["User", "Achievements"],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
achievementsLogger.error(`Cracker ${cracker} not implemented`);
|
||||
throw new Error(`Cracker ${cracker} not implemented`);
|
||||
};
|
||||
|
||||
export const findAchievementFiles = async (game: Game) => {
|
||||
const achievementFiles: AchievementFile[] = [];
|
||||
|
||||
for (const cracker of crackers) {
|
||||
for (const { folderPath, fileLocation } of await getPathFromCracker(
|
||||
cracker
|
||||
)) {
|
||||
const filePath = path.join(folderPath, game.objectID, ...fileLocation);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
achievementFiles.push({
|
||||
type: cracker,
|
||||
filePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return achievementFiles;
|
||||
};
|
||||
|
||||
export const findAchievementFileInExecutableDirectory = (
|
||||
game: Game
|
||||
): AchievementFile | null => {
|
||||
if (!game.executablePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const steamDataPath = path.join(
|
||||
game.executablePath,
|
||||
"..",
|
||||
"SteamData",
|
||||
"user_stats.ini"
|
||||
);
|
||||
|
||||
return {
|
||||
type: Cracker.userstats,
|
||||
filePath: steamDataPath,
|
||||
};
|
||||
};
|
||||
|
||||
export const findAllAchievementFiles = async () => {
|
||||
const gameAchievementFiles = new Map<string, AchievementFile[]>();
|
||||
|
||||
for (const cracker of crackers) {
|
||||
for (const { folderPath, fileLocation } of await getPathFromCracker(
|
||||
cracker
|
||||
)) {
|
||||
if (!fs.existsSync(folderPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectIds = fs.readdirSync(folderPath);
|
||||
|
||||
for (const objectId of objectIds) {
|
||||
const filePath = path.join(folderPath, objectId, ...fileLocation);
|
||||
|
||||
if (!fs.existsSync(filePath)) continue;
|
||||
|
||||
const achivementFile = {
|
||||
type: cracker,
|
||||
filePath,
|
||||
};
|
||||
|
||||
gameAchievementFiles.get(objectId)
|
||||
? gameAchievementFiles.get(objectId)!.push(achivementFile)
|
||||
: gameAchievementFiles.set(objectId, [achivementFile]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gameAchievementFiles;
|
||||
};
|
||||
17
src/main/services/achievements/get-game-achievement-data.ts
Normal file
17
src/main/services/achievements/get-game-achievement-data.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
|
||||
export const getGameAchievementData = async (
|
||||
objectId: string,
|
||||
shop: string
|
||||
) => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
return HydraApi.get("/games/achievements", {
|
||||
shop,
|
||||
objectId,
|
||||
language: userPreferences?.language || "en",
|
||||
});
|
||||
};
|
||||
118
src/main/services/achievements/merge-achievements.ts
Normal file
118
src/main/services/achievements/merge-achievements.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { gameAchievementRepository, gameRepository } from "@main/repository";
|
||||
import type { GameShop, UnlockedAchievement } from "@types";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
|
||||
const saveAchievementsOnLocal = async (
|
||||
objectId: string,
|
||||
shop: string,
|
||||
achievements: any[]
|
||||
) => {
|
||||
return gameAchievementRepository
|
||||
.upsert(
|
||||
{
|
||||
objectId,
|
||||
shop,
|
||||
unlockedAchievements: JSON.stringify(achievements),
|
||||
},
|
||||
["objectId", "shop"]
|
||||
)
|
||||
.then(() => {
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
"on-achievement-unlocked",
|
||||
objectId,
|
||||
shop
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const mergeAchievements = async (
|
||||
objectId: string,
|
||||
shop: string,
|
||||
achievements: UnlockedAchievement[],
|
||||
publishNotification: boolean
|
||||
) => {
|
||||
const game = await gameRepository.findOne({
|
||||
where: { objectID: objectId, shop: shop as GameShop },
|
||||
});
|
||||
|
||||
if (!game) return;
|
||||
|
||||
const localGameAchievement = await gameAchievementRepository.findOne({
|
||||
where: {
|
||||
objectId,
|
||||
shop,
|
||||
},
|
||||
});
|
||||
|
||||
const unlockedAchievements = JSON.parse(
|
||||
localGameAchievement?.unlockedAchievements || "[]"
|
||||
);
|
||||
|
||||
const newAchievements = achievements
|
||||
.filter((achievement) => {
|
||||
return !unlockedAchievements.some((localAchievement) => {
|
||||
return (
|
||||
localAchievement.name.toUpperCase() === achievement.name.toUpperCase()
|
||||
);
|
||||
});
|
||||
})
|
||||
.map((achievement) => {
|
||||
return {
|
||||
name: achievement.name.toUpperCase(),
|
||||
unlockTime: achievement.unlockTime * 1000,
|
||||
};
|
||||
});
|
||||
|
||||
if (newAchievements.length && publishNotification) {
|
||||
const achievementsInfo = newAchievements
|
||||
.sort((a, b) => {
|
||||
return a.unlockTime - b.unlockTime;
|
||||
})
|
||||
.map((achievement) => {
|
||||
return JSON.parse(localGameAchievement?.achievements || "[]").find(
|
||||
(steamAchievement) => {
|
||||
return (
|
||||
achievement.name.toUpperCase() ===
|
||||
steamAchievement.name.toUpperCase()
|
||||
);
|
||||
}
|
||||
);
|
||||
})
|
||||
.filter((achievement) => achievement)
|
||||
.map((achievement) => {
|
||||
return {
|
||||
displayName: achievement.displayName,
|
||||
iconUrl: achievement.icon,
|
||||
};
|
||||
});
|
||||
|
||||
WindowManager.notificationWindow?.webContents.send(
|
||||
"on-achievement-unlocked",
|
||||
objectId,
|
||||
shop,
|
||||
achievementsInfo
|
||||
);
|
||||
}
|
||||
|
||||
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
|
||||
|
||||
if (game?.remoteId) {
|
||||
return HydraApi.put("/profile/games/achievements", {
|
||||
id: game.remoteId,
|
||||
achievements: mergedLocalAchievements,
|
||||
})
|
||||
.then((response) => {
|
||||
return saveAchievementsOnLocal(
|
||||
response.objectId,
|
||||
response.shop,
|
||||
response.achievements
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
return saveAchievementsOnLocal(objectId, shop, mergedLocalAchievements);
|
||||
});
|
||||
}
|
||||
|
||||
return saveAchievementsOnLocal(objectId, shop, mergedLocalAchievements);
|
||||
};
|
||||
205
src/main/services/achievements/parse-achievement-file.ts
Normal file
205
src/main/services/achievements/parse-achievement-file.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { Cracker } from "@shared";
|
||||
import { UnlockedAchievement } from "@types";
|
||||
import { existsSync, createReadStream, readFileSync } from "node:fs";
|
||||
import readline from "node:readline";
|
||||
import { achievementsLogger } from "../logger";
|
||||
|
||||
export const parseAchievementFile = async (
|
||||
filePath: string,
|
||||
type: Cracker
|
||||
): Promise<UnlockedAchievement[]> => {
|
||||
if (!existsSync(filePath)) return [];
|
||||
|
||||
if (type == Cracker.codex) {
|
||||
const parsed = await iniParse(filePath);
|
||||
return processDefault(parsed);
|
||||
}
|
||||
|
||||
if (type == Cracker.rune) {
|
||||
const parsed = await iniParse(filePath);
|
||||
return processDefault(parsed);
|
||||
}
|
||||
|
||||
if (type === Cracker.onlineFix) {
|
||||
const parsed = await iniParse(filePath);
|
||||
return processOnlineFix(parsed);
|
||||
}
|
||||
|
||||
if (type === Cracker.goldberg) {
|
||||
const parsed = await jsonParse(filePath);
|
||||
return processGoldberg(parsed);
|
||||
}
|
||||
|
||||
if (type == Cracker.userstats) {
|
||||
const parsed = await iniParse(filePath);
|
||||
return processUserStats(parsed);
|
||||
}
|
||||
|
||||
if (type == Cracker.rld) {
|
||||
const parsed = await iniParse(filePath);
|
||||
return processRld(parsed);
|
||||
}
|
||||
|
||||
if (type === Cracker.skidrow) {
|
||||
const parsed = await iniParse(filePath);
|
||||
return processSkidrow(parsed);
|
||||
}
|
||||
|
||||
achievementsLogger.log(`${type} achievements found on ${filePath}`);
|
||||
return [];
|
||||
};
|
||||
|
||||
const iniParse = async (filePath: string) => {
|
||||
try {
|
||||
const file = createReadStream(filePath);
|
||||
|
||||
const lines = readline.createInterface({
|
||||
input: file,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
let objectName = "";
|
||||
const object: Record<string, Record<string, string | number>> = {};
|
||||
|
||||
for await (const line of lines) {
|
||||
if (line.startsWith("###") || !line.length) continue;
|
||||
|
||||
if (line.startsWith("[") && line.endsWith("]")) {
|
||||
objectName = line.slice(1, -1);
|
||||
object[objectName] = {};
|
||||
} else {
|
||||
const [name, ...value] = line.split("=");
|
||||
object[objectName][name.trim()] = value.join("").trim();
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Parsed ini", object);
|
||||
return object;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const jsonParse = (filePath: string) => {
|
||||
try {
|
||||
return JSON.parse(readFileSync(filePath, "utf-8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const processOnlineFix = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
const parsedUnlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||
const unlockedAchievement = unlockedAchievements[achievement];
|
||||
|
||||
if (unlockedAchievement?.achieved) {
|
||||
parsedUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime: unlockedAchievement.timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return parsedUnlockedAchievements;
|
||||
};
|
||||
|
||||
const processSkidrow = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
const parsedUnlockedAchievements: UnlockedAchievement[] = [];
|
||||
const achievements = unlockedAchievements["Achievements"];
|
||||
|
||||
for (const achievement of Object.keys(achievements)) {
|
||||
const unlockedAchievement = achievements[achievement].split("@");
|
||||
|
||||
if (unlockedAchievement[0] === "1") {
|
||||
parsedUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime: unlockedAchievement[unlockedAchievement.length - 1],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return parsedUnlockedAchievements;
|
||||
};
|
||||
|
||||
const processGoldberg = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||
const unlockedAchievement = unlockedAchievements[achievement];
|
||||
|
||||
if (unlockedAchievement?.earned) {
|
||||
newUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime: unlockedAchievement.earned_time,
|
||||
});
|
||||
}
|
||||
}
|
||||
return newUnlockedAchievements;
|
||||
};
|
||||
|
||||
const processDefault = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||
const unlockedAchievement = unlockedAchievements[achievement];
|
||||
|
||||
if (unlockedAchievement?.Achieved) {
|
||||
newUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime: unlockedAchievement.UnlockTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return newUnlockedAchievements;
|
||||
};
|
||||
|
||||
const processRld = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||
if (achievement === "Steam") continue;
|
||||
|
||||
const unlockedAchievement = unlockedAchievements[achievement];
|
||||
|
||||
if (unlockedAchievement?.State) {
|
||||
newUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime: new DataView(
|
||||
new Uint8Array(
|
||||
Buffer.from(unlockedAchievement.Time.toString(), "hex")
|
||||
).buffer
|
||||
).getUint32(0, true),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return newUnlockedAchievements;
|
||||
};
|
||||
|
||||
const processUserStats = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
const achievements = unlockedAchievements["ACHIEVEMENTS"];
|
||||
|
||||
if (!achievements) return [];
|
||||
|
||||
for (const achievement of Object.keys(achievements)) {
|
||||
const unlockedAchievement = achievements[achievement];
|
||||
|
||||
const unlockTime = Number(
|
||||
unlockedAchievement.slice(1, -1).replace("unlocked = true, time = ", "")
|
||||
);
|
||||
|
||||
if (!isNaN(unlockTime)) {
|
||||
newUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime: unlockTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return newUnlockedAchievements;
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
import { gameAchievementRepository, gameRepository } from "@main/repository";
|
||||
import {
|
||||
findAllAchievementFiles,
|
||||
findAchievementFiles,
|
||||
} from "./find-achivement-files";
|
||||
import { parseAchievementFile } from "./parse-achievement-file";
|
||||
import { mergeAchievements } from "./merge-achievements";
|
||||
import type { UnlockedAchievement } from "@types";
|
||||
import { getGameAchievementData } from "./get-game-achievement-data";
|
||||
|
||||
export const updateAllLocalUnlockedAchievements = async () => {
|
||||
const gameAchievementFilesMap = await findAllAchievementFiles();
|
||||
|
||||
for (const objectId of gameAchievementFilesMap.keys()) {
|
||||
const gameAchievementFiles = gameAchievementFilesMap.get(objectId)!;
|
||||
|
||||
const [game, localAchievements] = await Promise.all([
|
||||
gameRepository.findOne({
|
||||
where: { objectID: objectId, shop: "steam", isDeleted: false },
|
||||
}),
|
||||
gameAchievementRepository.findOne({
|
||||
where: { objectId, shop: "steam" },
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!game) continue;
|
||||
|
||||
if (!localAchievements || !localAchievements.achievements) {
|
||||
await getGameAchievementData(objectId, "steam")
|
||||
.then((achievements) => {
|
||||
return gameAchievementRepository.upsert(
|
||||
{
|
||||
objectId,
|
||||
shop: "steam",
|
||||
achievements: JSON.stringify(achievements),
|
||||
},
|
||||
["objectId", "shop"]
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
const unlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
for (const achievementFile of gameAchievementFiles) {
|
||||
const parsedAchievements = await parseAchievementFile(
|
||||
achievementFile.filePath,
|
||||
achievementFile.type
|
||||
);
|
||||
console.log("Parsed for", game.title, parsedAchievements);
|
||||
if (parsedAchievements.length) {
|
||||
unlockedAchievements.push(...parsedAchievements);
|
||||
}
|
||||
}
|
||||
|
||||
mergeAchievements(objectId, "steam", unlockedAchievements, false);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateLocalUnlockedAchivements = async (objectId: string) => {
|
||||
const [game, localAchievements] = await Promise.all([
|
||||
gameRepository.findOne({
|
||||
where: { objectID: objectId, shop: "steam", isDeleted: false },
|
||||
}),
|
||||
gameAchievementRepository.findOne({
|
||||
where: { objectId, shop: "steam" },
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!game) return;
|
||||
|
||||
const gameAchievementFiles = await findAchievementFiles(game);
|
||||
|
||||
console.log("Achievements files for", game.title, gameAchievementFiles);
|
||||
|
||||
if (!localAchievements || !localAchievements.achievements) {
|
||||
await getGameAchievementData(objectId, "steam")
|
||||
.then((achievements) => {
|
||||
return gameAchievementRepository.upsert(
|
||||
{
|
||||
objectId,
|
||||
shop: "steam",
|
||||
achievements: JSON.stringify(achievements),
|
||||
},
|
||||
["objectId", "shop"]
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
const unlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
for (const achievementFile of gameAchievementFiles) {
|
||||
const localAchievementFile = await parseAchievementFile(
|
||||
achievementFile.filePath,
|
||||
achievementFile.type
|
||||
);
|
||||
|
||||
if (localAchievementFile.length) {
|
||||
unlockedAchievements.push(...localAchievementFile);
|
||||
}
|
||||
}
|
||||
|
||||
mergeAchievements(objectId, "steam", unlockedAchievements, false);
|
||||
};
|
||||
@@ -17,6 +17,7 @@ export class HydraApi {
|
||||
private static instance: AxiosInstance;
|
||||
|
||||
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
||||
private static readonly ADD_LOG_INTERCEPTOR = true;
|
||||
|
||||
private static secondsToMilliseconds = (seconds: number) => seconds * 1000;
|
||||
|
||||
@@ -87,60 +88,66 @@ export class HydraApi {
|
||||
headers: { "User-Agent": `Hydra Launcher v${appVersion}` },
|
||||
});
|
||||
|
||||
this.instance.interceptors.request.use(
|
||||
(request) => {
|
||||
logger.log(" ---- REQUEST -----");
|
||||
const data = Array.isArray(request.data)
|
||||
? request.data
|
||||
: omit(request.data, ["refreshToken"]);
|
||||
logger.log(request.method, request.url, request.params, data);
|
||||
return request;
|
||||
},
|
||||
(error) => {
|
||||
logger.error("request error", error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
this.instance.interceptors.response.use(
|
||||
(response) => {
|
||||
logger.log(" ---- RESPONSE -----");
|
||||
const data = Array.isArray(response.data)
|
||||
? response.data
|
||||
: omit(response.data, ["username", "accessToken", "refreshToken"]);
|
||||
logger.log(
|
||||
response.status,
|
||||
response.config.method,
|
||||
response.config.url,
|
||||
data
|
||||
);
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
logger.error(" ---- RESPONSE ERROR -----");
|
||||
|
||||
const { config } = error;
|
||||
|
||||
logger.error(
|
||||
config.method,
|
||||
config.baseURL,
|
||||
config.url,
|
||||
config.headers,
|
||||
config.data
|
||||
);
|
||||
|
||||
if (error.response) {
|
||||
logger.error("Response", error.response.status, error.response.data);
|
||||
} else if (error.request) {
|
||||
logger.error("Request", error.request);
|
||||
} else {
|
||||
logger.error("Error", error.message);
|
||||
if (this.ADD_LOG_INTERCEPTOR) {
|
||||
this.instance.interceptors.request.use(
|
||||
(request) => {
|
||||
logger.log(" ---- REQUEST -----");
|
||||
const data = Array.isArray(request.data)
|
||||
? request.data
|
||||
: omit(request.data, ["refreshToken"]);
|
||||
logger.log(request.method, request.url, request.params, data);
|
||||
return request;
|
||||
},
|
||||
(error) => {
|
||||
logger.error("request error", error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
logger.error(" ----- END RESPONSE ERROR -------");
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
this.instance.interceptors.response.use(
|
||||
(response) => {
|
||||
logger.log(" ---- RESPONSE -----");
|
||||
const data = Array.isArray(response.data)
|
||||
? response.data
|
||||
: omit(response.data, ["username", "accessToken", "refreshToken"]);
|
||||
logger.log(
|
||||
response.status,
|
||||
response.config.method,
|
||||
response.config.url,
|
||||
data
|
||||
);
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
logger.error(" ---- RESPONSE ERROR -----");
|
||||
|
||||
const { config } = error;
|
||||
|
||||
logger.error(
|
||||
config.method,
|
||||
config.baseURL,
|
||||
config.url,
|
||||
config.headers,
|
||||
config.data
|
||||
);
|
||||
|
||||
if (error.response) {
|
||||
logger.error(
|
||||
"Response",
|
||||
error.response.status,
|
||||
error.response.data
|
||||
);
|
||||
} else if (error.request) {
|
||||
logger.error("Request", error.request);
|
||||
} else {
|
||||
logger.error("Error", error.message);
|
||||
}
|
||||
|
||||
logger.error(" ----- END RESPONSE ERROR -------");
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const userAuth = await userAuthRepository.findOne({
|
||||
where: { id: 1 },
|
||||
|
||||
@@ -4,6 +4,7 @@ import { IsNull } from "typeorm";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import { mergeWithRemoteGames } from "./merge-with-remote-games";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { updateAllLocalUnlockedAchievements } from "../achievements/update-local-unlocked-achivements";
|
||||
|
||||
export const uploadGamesBatch = async () => {
|
||||
const games = await gameRepository.find({
|
||||
@@ -28,6 +29,8 @@ export const uploadGamesBatch = async () => {
|
||||
|
||||
await mergeWithRemoteGames();
|
||||
|
||||
await updateAllLocalUnlockedAchievements();
|
||||
|
||||
if (WindowManager.mainWindow)
|
||||
WindowManager.mainWindow.webContents.send("on-library-batch-complete");
|
||||
};
|
||||
|
||||
@@ -29,3 +29,4 @@ log.initialize();
|
||||
|
||||
export const pythonInstanceLogger = log.scope("python-instance");
|
||||
export const logger = log.scope("main");
|
||||
export const achievementsLogger = log.scope("achievements");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { sleep } from "@main/helpers";
|
||||
import { DownloadManager } from "./download";
|
||||
import { watchProcesses } from "./process-watcher";
|
||||
import { watchAchievements } from "./achievements/achievement-watcher";
|
||||
|
||||
export const startMainLoop = async () => {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
@@ -8,6 +9,7 @@ export const startMainLoop = async () => {
|
||||
await Promise.allSettled([
|
||||
watchProcesses(),
|
||||
DownloadManager.watchDownloads(),
|
||||
watchAchievements(),
|
||||
]);
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
@@ -25,12 +25,12 @@ export const watchProcesses = async () => {
|
||||
if (games.length === 0) return;
|
||||
const processes = await PythonInstance.getProcessList();
|
||||
|
||||
const processSet = new Set(processes.map((process) => process.exe));
|
||||
|
||||
for (const game of games) {
|
||||
const executablePath = game.executablePath!;
|
||||
|
||||
const gameProcess = processes.find((runningProcess) => {
|
||||
return executablePath == runningProcess.exe;
|
||||
});
|
||||
const gameProcess = processSet.has(executablePath);
|
||||
|
||||
if (gameProcess) {
|
||||
if (gamesPlaytime.has(game.id)) {
|
||||
|
||||
@@ -19,8 +19,9 @@ import { HydraApi } from "./hydra-api";
|
||||
|
||||
export class WindowManager {
|
||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||
public static notificationWindow: Electron.BrowserWindow | null = null;
|
||||
|
||||
private static loadURL(hash = "") {
|
||||
private static loadMainWindowURL(hash = "") {
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||
@@ -37,6 +38,21 @@ export class WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static loadNotificationWindowURL() {
|
||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||
this.notificationWindow?.loadURL(
|
||||
`${process.env["ELECTRON_RENDERER_URL"]}#/achievement-notification`
|
||||
);
|
||||
} else {
|
||||
this.notificationWindow?.loadFile(
|
||||
path.join(__dirname, "../renderer/index.html"),
|
||||
{
|
||||
hash: "achievement-notification",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static createMainWindow() {
|
||||
if (this.mainWindow) return;
|
||||
|
||||
@@ -108,7 +124,7 @@ export class WindowManager {
|
||||
}
|
||||
);
|
||||
|
||||
this.loadURL();
|
||||
this.loadMainWindowURL();
|
||||
this.mainWindow.removeMenu();
|
||||
|
||||
this.mainWindow.on("ready-to-show", () => {
|
||||
@@ -125,9 +141,37 @@ export class WindowManager {
|
||||
app.quit();
|
||||
}
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
WindowManager.mainWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
public static createNotificationWindow() {
|
||||
this.notificationWindow = new BrowserWindow({
|
||||
transparent: true,
|
||||
maximizable: false,
|
||||
autoHideMenuBar: true,
|
||||
minimizable: false,
|
||||
focusable: false,
|
||||
skipTaskbar: true,
|
||||
frame: false,
|
||||
width: 350,
|
||||
height: 104,
|
||||
x: 0,
|
||||
y: 0,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
},
|
||||
});
|
||||
|
||||
this.notificationWindow.setIgnoreMouseEvents(true);
|
||||
this.notificationWindow.setVisibleOnAllWorkspaces(true, {
|
||||
visibleOnFullScreen: true,
|
||||
});
|
||||
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
|
||||
this.loadNotificationWindowURL();
|
||||
}
|
||||
|
||||
public static openAuthWindow() {
|
||||
if (this.mainWindow) {
|
||||
const authWindow = new BrowserWindow({
|
||||
@@ -174,7 +218,7 @@ export class WindowManager {
|
||||
|
||||
public static redirect(hash: string) {
|
||||
if (!this.mainWindow) this.createMainWindow();
|
||||
this.loadURL(hash);
|
||||
this.loadMainWindowURL(hash);
|
||||
|
||||
if (this.mainWindow?.isMinimized()) this.mainWindow.restore();
|
||||
this.mainWindow?.focus();
|
||||
|
||||
Reference in New Issue
Block a user