mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-23 02:41:02 +00:00
feat: adding drive mapping
This commit is contained in:
@@ -1,88 +1,74 @@
|
||||
import type { GameAchievement, GameShop } from "@types";
|
||||
import type { GameAchievement, GameShop, UnlockedAchievement } from "@types";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import {
|
||||
gameAchievementRepository,
|
||||
gameRepository,
|
||||
userPreferencesRepository,
|
||||
userAuthRepository,
|
||||
} from "@main/repository";
|
||||
import { UserNotLoggedInError } from "@shared";
|
||||
import { Game } from "@main/entity";
|
||||
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const getAchievementsDataFromApi = async (
|
||||
objectId: string,
|
||||
const getAchievements = async (
|
||||
shop: string,
|
||||
game: Game | null
|
||||
objectId: string,
|
||||
userId?: string
|
||||
) => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
const userAuth = await userAuthRepository.findOne({ where: { userId } });
|
||||
|
||||
const cachedAchievements = await gameAchievementRepository.findOne({
|
||||
where: { objectId, shop },
|
||||
});
|
||||
|
||||
return HydraApi.get("/games/achievements", {
|
||||
objectId,
|
||||
shop,
|
||||
language: userPreferences?.language || "en",
|
||||
})
|
||||
.then((achievements) => {
|
||||
if (game) {
|
||||
gameAchievementRepository.upsert(
|
||||
{
|
||||
objectId,
|
||||
shop,
|
||||
achievements: JSON.stringify(achievements),
|
||||
},
|
||||
["objectId", "shop"]
|
||||
);
|
||||
}
|
||||
const achievementsData = cachedAchievements?.achievements
|
||||
? JSON.parse(cachedAchievements.achievements)
|
||||
: await getGameAchievementData(objectId, shop);
|
||||
|
||||
return achievements;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err instanceof UserNotLoggedInError) throw err;
|
||||
return [];
|
||||
});
|
||||
if (!userId || userAuth) {
|
||||
const unlockedAchievements = JSON.parse(
|
||||
cachedAchievements?.unlockedAchievements || "[]"
|
||||
) as UnlockedAchievement[];
|
||||
|
||||
return { achievementsData, unlockedAchievements };
|
||||
}
|
||||
|
||||
const unlockedAchievements = await HydraApi.get<UnlockedAchievement[]>(
|
||||
`/users/${userId}/games/achievements`,
|
||||
{ shop, objectId, language: "en" }
|
||||
);
|
||||
|
||||
return { achievementsData, unlockedAchievements };
|
||||
};
|
||||
|
||||
const getGameAchievements = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
export const getGameAchievements = async (
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
shop: GameShop,
|
||||
userId?: string
|
||||
): Promise<GameAchievement[]> => {
|
||||
const [game, cachedAchievements] = await Promise.all([
|
||||
gameRepository.findOne({
|
||||
where: { objectID: objectId, shop },
|
||||
}),
|
||||
gameAchievementRepository.findOne({ where: { objectId, shop } }),
|
||||
]);
|
||||
const { achievementsData, unlockedAchievements } = await getAchievements(
|
||||
shop,
|
||||
objectId,
|
||||
userId
|
||||
);
|
||||
|
||||
const gameAchievements = cachedAchievements?.achievements
|
||||
? JSON.parse(cachedAchievements.achievements)
|
||||
: await getAchievementsDataFromApi(objectId, shop, game);
|
||||
|
||||
const unlockedAchievements = JSON.parse(
|
||||
cachedAchievements?.unlockedAchievements || "[]"
|
||||
) as { name: string; unlockTime: number }[];
|
||||
|
||||
return gameAchievements
|
||||
.map((achievement) => {
|
||||
return achievementsData
|
||||
.map((achievementData) => {
|
||||
const unlockedAchiement = unlockedAchievements.find(
|
||||
(localAchievement) => {
|
||||
return (
|
||||
localAchievement.name.toUpperCase() ==
|
||||
achievement.name.toUpperCase()
|
||||
achievementData.name.toUpperCase()
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (unlockedAchiement) {
|
||||
return {
|
||||
...achievement,
|
||||
...achievementData,
|
||||
unlocked: true,
|
||||
unlockTime: unlockedAchiement.unlockTime,
|
||||
};
|
||||
}
|
||||
|
||||
return { ...achievement, unlocked: false, unlockTime: null };
|
||||
return { ...achievementData, unlocked: false, unlockTime: null };
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.unlocked && !b.unlocked) return -1;
|
||||
@@ -91,4 +77,13 @@ const getGameAchievements = async (
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("getGameAchievements", getGameAchievements);
|
||||
const getGameAchievementsEvent = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
userId?: string
|
||||
): Promise<GameAchievement[]> => {
|
||||
return getGameAchievements(objectId, shop, userId);
|
||||
};
|
||||
|
||||
registerEvent("getGameAchievements", getGameAchievementsEvent);
|
||||
|
||||
@@ -20,11 +20,6 @@ export interface LudusaviBackup {
|
||||
};
|
||||
}
|
||||
|
||||
const getPathDrive = (key: string) => {
|
||||
const parts = key.split("/");
|
||||
return parts[0];
|
||||
};
|
||||
|
||||
const replaceLudusaviBackupWithCurrentUser = (
|
||||
backupPath: string,
|
||||
title: string
|
||||
|
||||
@@ -44,12 +44,12 @@ const addGameToLibrary = async (
|
||||
});
|
||||
}
|
||||
|
||||
updateLocalUnlockedAchivements(objectId);
|
||||
|
||||
const game = await gameRepository.findOne({
|
||||
where: { objectID: objectId },
|
||||
});
|
||||
|
||||
updateLocalUnlockedAchivements(game!);
|
||||
|
||||
createGame(game!).catch(() => {});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -30,4 +30,4 @@ export const isPortableVersion = () =>
|
||||
process.env.PORTABLE_EXECUTABLE_FILE !== null;
|
||||
|
||||
export const normalizePath = (str: string) =>
|
||||
path.normalize(str).replace(/\\/g, "/");
|
||||
path.posix.normalize(str).replace(/\\/g, "/");
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,19 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { checkAchievementFileChange as searchForAchievements } from "./achievement-file-observer";
|
||||
import { parseAchievementFile } from "./parse-achievement-file";
|
||||
import { Game } from "@main/entity";
|
||||
import { mergeAchievements } from "./merge-achievements";
|
||||
import fs, { readdirSync } from "node:fs";
|
||||
import {
|
||||
findAchievementFileInExecutableDirectory,
|
||||
findAllAchievementFiles,
|
||||
getAlternativeObjectIds,
|
||||
} from "./find-achivement-files";
|
||||
import type { AchievementFile } from "@types";
|
||||
import { achievementsLogger, logger } from "../logger";
|
||||
import { Cracker } from "@shared";
|
||||
|
||||
const fileStats: Map<string, number> = new Map();
|
||||
const fltFiles: Map<string, Set<string>> = new Map();
|
||||
|
||||
export const watchAchievements = async () => {
|
||||
const games = await gameRepository.find({
|
||||
@@ -10,5 +24,100 @@ export const watchAchievements = async () => {
|
||||
|
||||
if (games.length === 0) return;
|
||||
|
||||
await searchForAchievements(games);
|
||||
const achievementFiles = findAllAchievementFiles();
|
||||
|
||||
for (const game of games) {
|
||||
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
||||
const gameAchievementFiles = achievementFiles.get(objectId) || [];
|
||||
const achievementFileInsideDirectory =
|
||||
findAchievementFileInExecutableDirectory(game);
|
||||
|
||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
||||
|
||||
if (!gameAchievementFiles.length) continue;
|
||||
|
||||
console.log(
|
||||
"Achievements files to observe for:",
|
||||
game.title,
|
||||
gameAchievementFiles
|
||||
);
|
||||
|
||||
for (const file of gameAchievementFiles) {
|
||||
compareFile(game, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const processAchievementFileDiff = async (
|
||||
game: Game,
|
||||
file: AchievementFile
|
||||
) => {
|
||||
const unlockedAchievements = 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 compareFltFolder = async (game: Game, file: AchievementFile) => {
|
||||
try {
|
||||
const currentAchievements = new Set(readdirSync(file.filePath));
|
||||
const previousAchievements = fltFiles.get(file.filePath);
|
||||
|
||||
fltFiles.set(file.filePath, currentAchievements);
|
||||
if (
|
||||
!previousAchievements ||
|
||||
currentAchievements.difference(previousAchievements).size === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log("Detected change in FLT folder", file.filePath);
|
||||
await processAchievementFileDiff(game, file);
|
||||
} catch (err) {
|
||||
achievementsLogger.error(err);
|
||||
fltFiles.set(file.filePath, new Set());
|
||||
}
|
||||
};
|
||||
|
||||
const compareFile = async (game: Game, file: AchievementFile) => {
|
||||
if (file.type === Cracker.flt) {
|
||||
await compareFltFolder(game, file);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const currentStat = fs.statSync(file.filePath);
|
||||
const previousStat = fileStats.get(file.filePath);
|
||||
fileStats.set(file.filePath, currentStat.mtimeMs);
|
||||
|
||||
if (!previousStat) {
|
||||
if (currentStat.mtimeMs) {
|
||||
await processAchievementFileDiff(game, file);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (previousStat === currentStat.mtimeMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log(
|
||||
"Detected change in file",
|
||||
file.filePath,
|
||||
currentStat.mtimeMs,
|
||||
fileStats.get(file.filePath)
|
||||
);
|
||||
await processAchievementFileDiff(game, file);
|
||||
} catch (err) {
|
||||
fileStats.set(file.filePath, -1);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -24,9 +24,10 @@ const crackers = [
|
||||
Cracker.skidrow,
|
||||
Cracker.smartSteamEmu,
|
||||
Cracker.empress,
|
||||
Cracker.flt,
|
||||
];
|
||||
|
||||
const getPathFromCracker = async (cracker: Cracker) => {
|
||||
const getPathFromCracker = (cracker: Cracker) => {
|
||||
if (cracker === Cracker.codex) {
|
||||
return [
|
||||
{
|
||||
@@ -85,6 +86,10 @@ const getPathFromCracker = async (cracker: Cracker) => {
|
||||
folderPath: path.join(programData, "Steam", "Player"),
|
||||
fileLocation: ["stats", "achievements.ini"],
|
||||
},
|
||||
{
|
||||
folderPath: path.join(programData, "Steam", "dodi"),
|
||||
fileLocation: ["stats", "achievements.ini"],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -131,7 +136,33 @@ const getPathFromCracker = async (cracker: Cracker) => {
|
||||
return [
|
||||
{
|
||||
folderPath: path.join(appData, "SmartSteamEmu"),
|
||||
fileLocation: ["User", "Achievements"],
|
||||
fileLocation: ["User", "Achievements.ini"],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (cracker === Cracker._3dm) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (cracker === Cracker.flt) {
|
||||
return [
|
||||
{
|
||||
folderPath: path.join(appData, "FLT"),
|
||||
fileLocation: ["stats"],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (cracker == Cracker.rle) {
|
||||
return [
|
||||
{
|
||||
folderPath: path.join(appData, "RLE"),
|
||||
fileLocation: ["achievements.ini"],
|
||||
},
|
||||
{
|
||||
folderPath: path.join(appData, "RLE"),
|
||||
fileLocation: ["Achievements.ini"],
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -140,20 +171,29 @@ const getPathFromCracker = async (cracker: Cracker) => {
|
||||
throw new Error(`Cracker ${cracker} not implemented`);
|
||||
};
|
||||
|
||||
export const findAchievementFiles = async (game: Game) => {
|
||||
export const getAlternativeObjectIds = (objectId: string) => {
|
||||
// Dishonored
|
||||
if (objectId === "205100") {
|
||||
return ["205100", "217980", "31292"];
|
||||
}
|
||||
|
||||
return [objectId];
|
||||
};
|
||||
|
||||
export const findAchievementFiles = (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);
|
||||
for (const { folderPath, fileLocation } of getPathFromCracker(cracker)) {
|
||||
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
||||
const filePath = path.join(folderPath, objectId, ...fileLocation);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
achievementFiles.push({
|
||||
type: cracker,
|
||||
filePath,
|
||||
});
|
||||
if (fs.existsSync(filePath)) {
|
||||
achievementFiles.push({
|
||||
type: cracker,
|
||||
filePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,31 +203,40 @@ export const findAchievementFiles = async (game: Game) => {
|
||||
|
||||
export const findAchievementFileInExecutableDirectory = (
|
||||
game: Game
|
||||
): AchievementFile | null => {
|
||||
): AchievementFile[] => {
|
||||
if (!game.executablePath) {
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
const steamDataPath = path.join(
|
||||
game.executablePath,
|
||||
"..",
|
||||
"SteamData",
|
||||
"user_stats.ini"
|
||||
);
|
||||
|
||||
return {
|
||||
type: Cracker.userstats,
|
||||
filePath: steamDataPath,
|
||||
};
|
||||
return [
|
||||
{
|
||||
type: Cracker.userstats,
|
||||
filePath: path.join(
|
||||
game.executablePath,
|
||||
"..",
|
||||
"SteamData",
|
||||
"user_stats.ini"
|
||||
),
|
||||
},
|
||||
{
|
||||
type: Cracker._3dm,
|
||||
filePath: path.join(
|
||||
game.executablePath,
|
||||
"..",
|
||||
"3DMGAME",
|
||||
"Player",
|
||||
"stats",
|
||||
"achievements.ini"
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const findAllAchievementFiles = async () => {
|
||||
export const findAllAchievementFiles = () => {
|
||||
const gameAchievementFiles = new Map<string, AchievementFile[]>();
|
||||
|
||||
for (const cracker of crackers) {
|
||||
for (const { folderPath, fileLocation } of await getPathFromCracker(
|
||||
cracker
|
||||
)) {
|
||||
for (const { folderPath, fileLocation } of getPathFromCracker(cracker)) {
|
||||
if (!fs.existsSync(folderPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import {
|
||||
gameAchievementRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
|
||||
export const getGameAchievementData = async (
|
||||
@@ -13,5 +16,18 @@ export const getGameAchievementData = async (
|
||||
shop,
|
||||
objectId,
|
||||
language: userPreferences?.language || "en",
|
||||
});
|
||||
})
|
||||
.then(async (achievements) => {
|
||||
await gameAchievementRepository.upsert(
|
||||
{
|
||||
objectId,
|
||||
shop,
|
||||
achievements: JSON.stringify(achievements),
|
||||
},
|
||||
["objectId", "shop"]
|
||||
);
|
||||
|
||||
return achievements;
|
||||
})
|
||||
.catch(() => []);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { gameAchievementRepository, gameRepository } from "@main/repository";
|
||||
import type { GameShop, UnlockedAchievement } from "@types";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import { getGameAchievements } from "@main/events/catalogue/get-game-achievements";
|
||||
|
||||
const saveAchievementsOnLocal = async (
|
||||
objectId: string,
|
||||
@@ -17,11 +18,10 @@ const saveAchievementsOnLocal = async (
|
||||
},
|
||||
["objectId", "shop"]
|
||||
)
|
||||
.then(() => {
|
||||
.then(async () => {
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
"on-achievement-unlocked",
|
||||
objectId,
|
||||
shop
|
||||
`on-update-achievements-${objectId}-${shop}`,
|
||||
await getGameAchievements(objectId, shop as GameShop)
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -47,7 +47,7 @@ export const mergeAchievements = async (
|
||||
|
||||
const unlockedAchievements = JSON.parse(
|
||||
localGameAchievement?.unlockedAchievements || "[]"
|
||||
);
|
||||
).filter((achievement) => achievement.name);
|
||||
|
||||
const newAchievements = achievements
|
||||
.filter((achievement) => {
|
||||
@@ -60,7 +60,7 @@ export const mergeAchievements = async (
|
||||
.map((achievement) => {
|
||||
return {
|
||||
name: achievement.name.toUpperCase(),
|
||||
unlockTime: achievement.unlockTime * 1000,
|
||||
unlockTime: achievement.unlockTime,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,67 +1,84 @@
|
||||
import { Cracker } from "@shared";
|
||||
import { UnlockedAchievement } from "@types";
|
||||
import { existsSync, createReadStream, readFileSync } from "node:fs";
|
||||
import readline from "node:readline";
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { achievementsLogger } from "../logger";
|
||||
|
||||
export const parseAchievementFile = async (
|
||||
export const parseAchievementFile = (
|
||||
filePath: string,
|
||||
type: Cracker
|
||||
): Promise<UnlockedAchievement[]> => {
|
||||
): UnlockedAchievement[] => {
|
||||
if (!existsSync(filePath)) return [];
|
||||
|
||||
if (type == Cracker.codex) {
|
||||
const parsed = await iniParse(filePath);
|
||||
const parsed = iniParse(filePath);
|
||||
return processDefault(parsed);
|
||||
}
|
||||
|
||||
if (type == Cracker.rune) {
|
||||
const parsed = await iniParse(filePath);
|
||||
const parsed = iniParse(filePath);
|
||||
return processDefault(parsed);
|
||||
}
|
||||
|
||||
if (type === Cracker.onlineFix) {
|
||||
const parsed = await iniParse(filePath);
|
||||
const parsed = iniParse(filePath);
|
||||
return processOnlineFix(parsed);
|
||||
}
|
||||
|
||||
if (type === Cracker.goldberg) {
|
||||
const parsed = await jsonParse(filePath);
|
||||
const parsed = jsonParse(filePath);
|
||||
return processGoldberg(parsed);
|
||||
}
|
||||
|
||||
if (type == Cracker.userstats) {
|
||||
const parsed = await iniParse(filePath);
|
||||
const parsed = iniParse(filePath);
|
||||
return processUserStats(parsed);
|
||||
}
|
||||
|
||||
if (type == Cracker.rld) {
|
||||
const parsed = await iniParse(filePath);
|
||||
const parsed = iniParse(filePath);
|
||||
return processRld(parsed);
|
||||
}
|
||||
|
||||
if (type === Cracker.skidrow) {
|
||||
const parsed = await iniParse(filePath);
|
||||
const parsed = iniParse(filePath);
|
||||
return processSkidrow(parsed);
|
||||
}
|
||||
|
||||
achievementsLogger.log(`${type} achievements found on ${filePath}`);
|
||||
if (type === Cracker._3dm) {
|
||||
const parsed = iniParse(filePath);
|
||||
return process3DM(parsed);
|
||||
}
|
||||
|
||||
if (type === Cracker.flt) {
|
||||
const achievements = readdirSync(filePath);
|
||||
|
||||
return achievements.map((achievement) => {
|
||||
return {
|
||||
name: achievement,
|
||||
unlockTime: Date.now(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (type === Cracker.creamAPI) {
|
||||
const parsed = iniParse(filePath);
|
||||
return processCreamAPI(parsed);
|
||||
}
|
||||
|
||||
achievementsLogger.log(
|
||||
`Unprocessed ${type} achievements found on ${filePath}`
|
||||
);
|
||||
return [];
|
||||
};
|
||||
|
||||
const iniParse = async (filePath: string) => {
|
||||
const iniParse = (filePath: string) => {
|
||||
try {
|
||||
const file = createReadStream(filePath);
|
||||
|
||||
const lines = readline.createInterface({
|
||||
input: file,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
const lines = readFileSync(filePath, "utf-8").split(/[\r\n]+/);
|
||||
|
||||
let objectName = "";
|
||||
const object: Record<string, Record<string, string | number>> = {};
|
||||
|
||||
for await (const line of lines) {
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("###") || !line.length) continue;
|
||||
|
||||
if (line.startsWith("[") && line.endsWith("]")) {
|
||||
@@ -69,13 +86,13 @@ const iniParse = async (filePath: string) => {
|
||||
object[objectName] = {};
|
||||
} else {
|
||||
const [name, ...value] = line.split("=");
|
||||
object[objectName][name.trim()] = value.join("").trim();
|
||||
object[objectName][name.trim()] = value.join("=").trim();
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Parsed ini", object);
|
||||
return object;
|
||||
} catch {
|
||||
} catch (err) {
|
||||
achievementsLogger.error(`Error parsing ${filePath}`, err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -83,7 +100,8 @@ const iniParse = async (filePath: string) => {
|
||||
const jsonParse = (filePath: string) => {
|
||||
try {
|
||||
return JSON.parse(readFileSync(filePath, "utf-8"));
|
||||
} catch {
|
||||
} catch (err) {
|
||||
achievementsLogger.error(`Error parsing ${filePath}`, err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -97,7 +115,28 @@ const processOnlineFix = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
if (unlockedAchievement?.achieved) {
|
||||
parsedUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime: unlockedAchievement.timestamp,
|
||||
unlockTime: unlockedAchievement.timestamp * 1000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return parsedUnlockedAchievements;
|
||||
};
|
||||
|
||||
const processCreamAPI = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
const parsedUnlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||
const unlockedAchievement = unlockedAchievements[achievement];
|
||||
|
||||
if (unlockedAchievement?.achieved) {
|
||||
const unlockTime = unlockedAchievement.unlocktime;
|
||||
parsedUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime:
|
||||
unlockTime.length === 7
|
||||
? unlockTime * 1000 * 1000
|
||||
: unlockTime * 1000,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -115,7 +154,7 @@ const processSkidrow = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
if (unlockedAchievement[0] === "1") {
|
||||
parsedUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime: unlockedAchievement[unlockedAchievement.length - 1],
|
||||
unlockTime: unlockedAchievement[unlockedAchievement.length - 1] * 1000,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -132,13 +171,36 @@ const processGoldberg = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
if (unlockedAchievement?.earned) {
|
||||
newUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime: unlockedAchievement.earned_time,
|
||||
unlockTime: unlockedAchievement.earned_time * 1000,
|
||||
});
|
||||
}
|
||||
}
|
||||
return newUnlockedAchievements;
|
||||
};
|
||||
|
||||
const process3DM = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
const achievements = unlockedAchievements["State"];
|
||||
const times = unlockedAchievements["Time"];
|
||||
|
||||
for (const achievement of Object.keys(achievements)) {
|
||||
if (achievements[achievement] == "0101") {
|
||||
const time = times[achievement];
|
||||
|
||||
newUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime:
|
||||
new DataView(
|
||||
new Uint8Array(Buffer.from(time.toString(), "hex")).buffer
|
||||
).getUint32(0, true) * 1000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return newUnlockedAchievements;
|
||||
};
|
||||
|
||||
const processDefault = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
@@ -148,7 +210,7 @@ const processDefault = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
if (unlockedAchievement?.Achieved) {
|
||||
newUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime: unlockedAchievement.UnlockTime,
|
||||
unlockTime: unlockedAchievement.UnlockTime * 1000,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -167,11 +229,12 @@ const processRld = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
if (unlockedAchievement?.State) {
|
||||
newUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime: new DataView(
|
||||
new Uint8Array(
|
||||
Buffer.from(unlockedAchievement.Time.toString(), "hex")
|
||||
).buffer
|
||||
).getUint32(0, true),
|
||||
unlockTime:
|
||||
new DataView(
|
||||
new Uint8Array(
|
||||
Buffer.from(unlockedAchievement.Time.toString(), "hex")
|
||||
).buffer
|
||||
).getUint32(0, true) * 1000,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -195,8 +258,8 @@ const processUserStats = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
|
||||
if (!isNaN(unlockTime)) {
|
||||
newUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime: unlockTime,
|
||||
name: achievement.replace(/"/g, ``),
|
||||
unlockTime: unlockTime * 1000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,96 +2,82 @@ import { gameAchievementRepository, gameRepository } from "@main/repository";
|
||||
import {
|
||||
findAllAchievementFiles,
|
||||
findAchievementFiles,
|
||||
findAchievementFileInExecutableDirectory,
|
||||
getAlternativeObjectIds,
|
||||
} 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";
|
||||
import { achievementsLogger } from "../logger";
|
||||
import { Game } from "@main/entity";
|
||||
|
||||
export const updateAllLocalUnlockedAchievements = async () => {
|
||||
const gameAchievementFilesMap = await findAllAchievementFiles();
|
||||
const gameAchievementFilesMap = findAllAchievementFiles();
|
||||
|
||||
for (const objectId of gameAchievementFilesMap.keys()) {
|
||||
const gameAchievementFiles = gameAchievementFilesMap.get(objectId)!;
|
||||
const games = await gameRepository.find({
|
||||
where: {
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
|
||||
const [game, localAchievements] = await Promise.all([
|
||||
gameRepository.findOne({
|
||||
where: { objectID: objectId, shop: "steam", isDeleted: false },
|
||||
}),
|
||||
gameAchievementRepository.findOne({
|
||||
where: { objectId, shop: "steam" },
|
||||
}),
|
||||
]);
|
||||
for (const game of games) {
|
||||
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
||||
const gameAchievementFiles = gameAchievementFilesMap.get(objectId) || [];
|
||||
const achievementFileInsideDirectory =
|
||||
findAchievementFileInExecutableDirectory(game);
|
||||
|
||||
if (!game) continue;
|
||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
||||
|
||||
if (!localAchievements || !localAchievements.achievements) {
|
||||
await getGameAchievementData(objectId, "steam")
|
||||
.then((achievements) => {
|
||||
return gameAchievementRepository.upsert(
|
||||
{
|
||||
objectId,
|
||||
shop: "steam",
|
||||
achievements: JSON.stringify(achievements),
|
||||
},
|
||||
["objectId", "shop"]
|
||||
);
|
||||
gameAchievementRepository
|
||||
.findOne({
|
||||
where: { objectId: game.objectID, shop: "steam" },
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
.then((localAchievements) => {
|
||||
if (!localAchievements || !localAchievements.achievements) {
|
||||
getGameAchievementData(game.objectID, "steam");
|
||||
}
|
||||
});
|
||||
|
||||
const unlockedAchievements: UnlockedAchievement[] = [];
|
||||
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);
|
||||
for (const achievementFile of gameAchievementFiles) {
|
||||
const parsedAchievements = parseAchievementFile(
|
||||
achievementFile.filePath,
|
||||
achievementFile.type
|
||||
);
|
||||
|
||||
if (parsedAchievements.length) {
|
||||
unlockedAchievements.push(...parsedAchievements);
|
||||
}
|
||||
|
||||
achievementsLogger.log(
|
||||
"Achievement file for",
|
||||
game.title,
|
||||
achievementFile.filePath,
|
||||
parsedAchievements
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mergeAchievements(objectId, "steam", unlockedAchievements, false);
|
||||
mergeAchievements(game.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" },
|
||||
}),
|
||||
]);
|
||||
export const updateLocalUnlockedAchivements = async (game: Game) => {
|
||||
const gameAchievementFiles = findAchievementFiles(game);
|
||||
|
||||
if (!game) return;
|
||||
const achievementFileInsideDirectory =
|
||||
findAchievementFileInExecutableDirectory(game);
|
||||
|
||||
const gameAchievementFiles = await findAchievementFiles(game);
|
||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
||||
|
||||
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(
|
||||
const localAchievementFile = parseAchievementFile(
|
||||
achievementFile.filePath,
|
||||
achievementFile.type
|
||||
);
|
||||
@@ -101,5 +87,5 @@ export const updateLocalUnlockedAchivements = async (objectId: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
mergeAchievements(objectId, "steam", unlockedAchievements, false);
|
||||
mergeAchievements(game.objectID, "steam", unlockedAchievements, false);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,10 @@ log.transports.file.resolvePathFn = (
|
||||
return path.join(logsPath, "pythoninstance.txt");
|
||||
}
|
||||
|
||||
if (message?.scope == "achievements") {
|
||||
return path.join(logsPath, "achievements.txt");
|
||||
}
|
||||
|
||||
if (message?.level === "error") {
|
||||
return path.join(logsPath, "error.txt");
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ export const startMainLoop = async () => {
|
||||
watchAchievements(),
|
||||
]);
|
||||
|
||||
await sleep(1000);
|
||||
await sleep(1500);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user