mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 08:43:48 +03:00
feat: adding drive mapping
This commit is contained in:
commit
16cd5b43d8
@ -131,6 +131,7 @@
|
|||||||
"executable_path_in_use": "Executable already in use by \"{{game}}\"",
|
"executable_path_in_use": "Executable already in use by \"{{game}}\"",
|
||||||
"warning": "Warning:",
|
"warning": "Warning:",
|
||||||
"hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util its conclusion. In case Hydra closes before the conclusion, you will lose your progress.",
|
"hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util its conclusion. In case Hydra closes before the conclusion, you will lose your progress.",
|
||||||
|
"achievements": "Achievements {{unlockedCount}}/{{achievementsCount}}",
|
||||||
"cloud_save": "Cloud save",
|
"cloud_save": "Cloud save",
|
||||||
"cloud_save_description": "Save your progress in the cloud and continue playing on any device",
|
"cloud_save_description": "Save your progress in the cloud and continue playing on any device",
|
||||||
"backups": "Backups",
|
"backups": "Backups",
|
||||||
|
@ -127,6 +127,7 @@
|
|||||||
"executable_path_in_use": "Executável em uso por \"{{game}}\"",
|
"executable_path_in_use": "Executável em uso por \"{{game}}\"",
|
||||||
"warning": "Aviso:",
|
"warning": "Aviso:",
|
||||||
"hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.",
|
"hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.",
|
||||||
|
"achievements": "Conquistas {{unlockedCount}}/{{achievementsCount}}",
|
||||||
"cloud_save": "Salvamento em nuvem",
|
"cloud_save": "Salvamento em nuvem",
|
||||||
"cloud_save_description": "Matenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo",
|
"cloud_save_description": "Matenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo",
|
||||||
"backups": "Backups",
|
"backups": "Backups",
|
||||||
|
@ -115,7 +115,8 @@
|
|||||||
"download": "Transferir",
|
"download": "Transferir",
|
||||||
"executable_path_in_use": "Executável em uso por \"{{game}}\"",
|
"executable_path_in_use": "Executável em uso por \"{{game}}\"",
|
||||||
"warning": "Aviso:",
|
"warning": "Aviso:",
|
||||||
"hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso."
|
"hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.",
|
||||||
|
"achievements": "Conquistas {{unlockedCount}}/{{achievementsCount}}"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
|
@ -1,88 +1,74 @@
|
|||||||
import type { GameAchievement, GameShop } from "@types";
|
import type { GameAchievement, GameShop, UnlockedAchievement } from "@types";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
import {
|
import {
|
||||||
gameAchievementRepository,
|
gameAchievementRepository,
|
||||||
gameRepository,
|
userAuthRepository,
|
||||||
userPreferencesRepository,
|
|
||||||
} from "@main/repository";
|
} from "@main/repository";
|
||||||
import { UserNotLoggedInError } from "@shared";
|
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
||||||
import { Game } from "@main/entity";
|
import { HydraApi } from "@main/services";
|
||||||
|
|
||||||
const getAchievementsDataFromApi = async (
|
const getAchievements = async (
|
||||||
objectId: string,
|
|
||||||
shop: string,
|
shop: string,
|
||||||
game: Game | null
|
|
||||||
) => {
|
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
|
||||||
where: { id: 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
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"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return achievements;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (err instanceof UserNotLoggedInError) throw err;
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGameAchievements = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop
|
userId?: string
|
||||||
): Promise<GameAchievement[]> => {
|
) => {
|
||||||
const [game, cachedAchievements] = await Promise.all([
|
const userAuth = await userAuthRepository.findOne({ where: { userId } });
|
||||||
gameRepository.findOne({
|
|
||||||
where: { objectID: objectId, shop },
|
|
||||||
}),
|
|
||||||
gameAchievementRepository.findOne({ where: { objectId, shop } }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const gameAchievements = cachedAchievements?.achievements
|
const cachedAchievements = await gameAchievementRepository.findOne({
|
||||||
|
where: { objectId, shop },
|
||||||
|
});
|
||||||
|
|
||||||
|
const achievementsData = cachedAchievements?.achievements
|
||||||
? JSON.parse(cachedAchievements.achievements)
|
? JSON.parse(cachedAchievements.achievements)
|
||||||
: await getAchievementsDataFromApi(objectId, shop, game);
|
: await getGameAchievementData(objectId, shop);
|
||||||
|
|
||||||
|
if (!userId || userAuth) {
|
||||||
const unlockedAchievements = JSON.parse(
|
const unlockedAchievements = JSON.parse(
|
||||||
cachedAchievements?.unlockedAchievements || "[]"
|
cachedAchievements?.unlockedAchievements || "[]"
|
||||||
) as { name: string; unlockTime: number }[];
|
) as UnlockedAchievement[];
|
||||||
|
|
||||||
return gameAchievements
|
return { achievementsData, unlockedAchievements };
|
||||||
.map((achievement) => {
|
}
|
||||||
|
|
||||||
|
const unlockedAchievements = await HydraApi.get<UnlockedAchievement[]>(
|
||||||
|
`/users/${userId}/games/achievements`,
|
||||||
|
{ shop, objectId, language: "en" }
|
||||||
|
);
|
||||||
|
|
||||||
|
return { achievementsData, unlockedAchievements };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGameAchievements = async (
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
userId?: string
|
||||||
|
): Promise<GameAchievement[]> => {
|
||||||
|
const { achievementsData, unlockedAchievements } = await getAchievements(
|
||||||
|
shop,
|
||||||
|
objectId,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
return achievementsData
|
||||||
|
.map((achievementData) => {
|
||||||
const unlockedAchiement = unlockedAchievements.find(
|
const unlockedAchiement = unlockedAchievements.find(
|
||||||
(localAchievement) => {
|
(localAchievement) => {
|
||||||
return (
|
return (
|
||||||
localAchievement.name.toUpperCase() ==
|
localAchievement.name.toUpperCase() ==
|
||||||
achievement.name.toUpperCase()
|
achievementData.name.toUpperCase()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (unlockedAchiement) {
|
if (unlockedAchiement) {
|
||||||
return {
|
return {
|
||||||
...achievement,
|
...achievementData,
|
||||||
unlocked: true,
|
unlocked: true,
|
||||||
unlockTime: unlockedAchiement.unlockTime,
|
unlockTime: unlockedAchiement.unlockTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...achievement, unlocked: false, unlockTime: null };
|
return { ...achievementData, unlocked: false, unlockTime: null };
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a.unlocked && !b.unlocked) return -1;
|
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 = (
|
const replaceLudusaviBackupWithCurrentUser = (
|
||||||
backupPath: string,
|
backupPath: string,
|
||||||
title: string
|
title: string
|
||||||
|
@ -44,12 +44,12 @@ const addGameToLibrary = async (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLocalUnlockedAchivements(objectId);
|
|
||||||
|
|
||||||
const game = await gameRepository.findOne({
|
const game = await gameRepository.findOne({
|
||||||
where: { objectID: objectId },
|
where: { objectID: objectId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateLocalUnlockedAchivements(game!);
|
||||||
|
|
||||||
createGame(game!).catch(() => {});
|
createGame(game!).catch(() => {});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -30,4 +30,4 @@ export const isPortableVersion = () =>
|
|||||||
process.env.PORTABLE_EXECUTABLE_FILE !== null;
|
process.env.PORTABLE_EXECUTABLE_FILE !== null;
|
||||||
|
|
||||||
export const normalizePath = (str: string) =>
|
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 { 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 () => {
|
export const watchAchievements = async () => {
|
||||||
const games = await gameRepository.find({
|
const games = await gameRepository.find({
|
||||||
@ -10,5 +24,100 @@ export const watchAchievements = async () => {
|
|||||||
|
|
||||||
if (games.length === 0) return;
|
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.skidrow,
|
||||||
Cracker.smartSteamEmu,
|
Cracker.smartSteamEmu,
|
||||||
Cracker.empress,
|
Cracker.empress,
|
||||||
|
Cracker.flt,
|
||||||
];
|
];
|
||||||
|
|
||||||
const getPathFromCracker = async (cracker: Cracker) => {
|
const getPathFromCracker = (cracker: Cracker) => {
|
||||||
if (cracker === Cracker.codex) {
|
if (cracker === Cracker.codex) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -85,6 +86,10 @@ const getPathFromCracker = async (cracker: Cracker) => {
|
|||||||
folderPath: path.join(programData, "Steam", "Player"),
|
folderPath: path.join(programData, "Steam", "Player"),
|
||||||
fileLocation: ["stats", "achievements.ini"],
|
fileLocation: ["stats", "achievements.ini"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
folderPath: path.join(programData, "Steam", "dodi"),
|
||||||
|
fileLocation: ["stats", "achievements.ini"],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,7 +136,33 @@ const getPathFromCracker = async (cracker: Cracker) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
folderPath: path.join(appData, "SmartSteamEmu"),
|
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,14 +171,22 @@ const getPathFromCracker = async (cracker: Cracker) => {
|
|||||||
throw new Error(`Cracker ${cracker} not implemented`);
|
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[] = [];
|
const achievementFiles: AchievementFile[] = [];
|
||||||
|
|
||||||
for (const cracker of crackers) {
|
for (const cracker of crackers) {
|
||||||
for (const { folderPath, fileLocation } of await getPathFromCracker(
|
for (const { folderPath, fileLocation } of getPathFromCracker(cracker)) {
|
||||||
cracker
|
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
||||||
)) {
|
const filePath = path.join(folderPath, objectId, ...fileLocation);
|
||||||
const filePath = path.join(folderPath, game.objectID, ...fileLocation);
|
|
||||||
|
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
achievementFiles.push({
|
achievementFiles.push({
|
||||||
@ -157,37 +196,47 @@ export const findAchievementFiles = async (game: Game) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return achievementFiles;
|
return achievementFiles;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findAchievementFileInExecutableDirectory = (
|
export const findAchievementFileInExecutableDirectory = (
|
||||||
game: Game
|
game: Game
|
||||||
): AchievementFile | null => {
|
): AchievementFile[] => {
|
||||||
if (!game.executablePath) {
|
if (!game.executablePath) {
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const steamDataPath = path.join(
|
return [
|
||||||
|
{
|
||||||
|
type: Cracker.userstats,
|
||||||
|
filePath: path.join(
|
||||||
game.executablePath,
|
game.executablePath,
|
||||||
"..",
|
"..",
|
||||||
"SteamData",
|
"SteamData",
|
||||||
"user_stats.ini"
|
"user_stats.ini"
|
||||||
);
|
),
|
||||||
|
},
|
||||||
return {
|
{
|
||||||
type: Cracker.userstats,
|
type: Cracker._3dm,
|
||||||
filePath: steamDataPath,
|
filePath: path.join(
|
||||||
};
|
game.executablePath,
|
||||||
|
"..",
|
||||||
|
"3DMGAME",
|
||||||
|
"Player",
|
||||||
|
"stats",
|
||||||
|
"achievements.ini"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findAllAchievementFiles = async () => {
|
export const findAllAchievementFiles = () => {
|
||||||
const gameAchievementFiles = new Map<string, AchievementFile[]>();
|
const gameAchievementFiles = new Map<string, AchievementFile[]>();
|
||||||
|
|
||||||
for (const cracker of crackers) {
|
for (const cracker of crackers) {
|
||||||
for (const { folderPath, fileLocation } of await getPathFromCracker(
|
for (const { folderPath, fileLocation } of getPathFromCracker(cracker)) {
|
||||||
cracker
|
|
||||||
)) {
|
|
||||||
if (!fs.existsSync(folderPath)) {
|
if (!fs.existsSync(folderPath)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { userPreferencesRepository } from "@main/repository";
|
import {
|
||||||
|
gameAchievementRepository,
|
||||||
|
userPreferencesRepository,
|
||||||
|
} from "@main/repository";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
|
|
||||||
export const getGameAchievementData = async (
|
export const getGameAchievementData = async (
|
||||||
@ -13,5 +16,18 @@ export const getGameAchievementData = async (
|
|||||||
shop,
|
shop,
|
||||||
objectId,
|
objectId,
|
||||||
language: userPreferences?.language || "en",
|
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 type { GameShop, UnlockedAchievement } from "@types";
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
|
import { getGameAchievements } from "@main/events/catalogue/get-game-achievements";
|
||||||
|
|
||||||
const saveAchievementsOnLocal = async (
|
const saveAchievementsOnLocal = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@ -17,11 +18,10 @@ const saveAchievementsOnLocal = async (
|
|||||||
},
|
},
|
||||||
["objectId", "shop"]
|
["objectId", "shop"]
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(async () => {
|
||||||
WindowManager.mainWindow?.webContents.send(
|
WindowManager.mainWindow?.webContents.send(
|
||||||
"on-achievement-unlocked",
|
`on-update-achievements-${objectId}-${shop}`,
|
||||||
objectId,
|
await getGameAchievements(objectId, shop as GameShop)
|
||||||
shop
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -47,7 +47,7 @@ export const mergeAchievements = async (
|
|||||||
|
|
||||||
const unlockedAchievements = JSON.parse(
|
const unlockedAchievements = JSON.parse(
|
||||||
localGameAchievement?.unlockedAchievements || "[]"
|
localGameAchievement?.unlockedAchievements || "[]"
|
||||||
);
|
).filter((achievement) => achievement.name);
|
||||||
|
|
||||||
const newAchievements = achievements
|
const newAchievements = achievements
|
||||||
.filter((achievement) => {
|
.filter((achievement) => {
|
||||||
@ -60,7 +60,7 @@ export const mergeAchievements = async (
|
|||||||
.map((achievement) => {
|
.map((achievement) => {
|
||||||
return {
|
return {
|
||||||
name: achievement.name.toUpperCase(),
|
name: achievement.name.toUpperCase(),
|
||||||
unlockTime: achievement.unlockTime * 1000,
|
unlockTime: achievement.unlockTime,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,67 +1,84 @@
|
|||||||
import { Cracker } from "@shared";
|
import { Cracker } from "@shared";
|
||||||
import { UnlockedAchievement } from "@types";
|
import { UnlockedAchievement } from "@types";
|
||||||
import { existsSync, createReadStream, readFileSync } from "node:fs";
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||||
import readline from "node:readline";
|
|
||||||
import { achievementsLogger } from "../logger";
|
import { achievementsLogger } from "../logger";
|
||||||
|
|
||||||
export const parseAchievementFile = async (
|
export const parseAchievementFile = (
|
||||||
filePath: string,
|
filePath: string,
|
||||||
type: Cracker
|
type: Cracker
|
||||||
): Promise<UnlockedAchievement[]> => {
|
): UnlockedAchievement[] => {
|
||||||
if (!existsSync(filePath)) return [];
|
if (!existsSync(filePath)) return [];
|
||||||
|
|
||||||
if (type == Cracker.codex) {
|
if (type == Cracker.codex) {
|
||||||
const parsed = await iniParse(filePath);
|
const parsed = iniParse(filePath);
|
||||||
return processDefault(parsed);
|
return processDefault(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type == Cracker.rune) {
|
if (type == Cracker.rune) {
|
||||||
const parsed = await iniParse(filePath);
|
const parsed = iniParse(filePath);
|
||||||
return processDefault(parsed);
|
return processDefault(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === Cracker.onlineFix) {
|
if (type === Cracker.onlineFix) {
|
||||||
const parsed = await iniParse(filePath);
|
const parsed = iniParse(filePath);
|
||||||
return processOnlineFix(parsed);
|
return processOnlineFix(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === Cracker.goldberg) {
|
if (type === Cracker.goldberg) {
|
||||||
const parsed = await jsonParse(filePath);
|
const parsed = jsonParse(filePath);
|
||||||
return processGoldberg(parsed);
|
return processGoldberg(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type == Cracker.userstats) {
|
if (type == Cracker.userstats) {
|
||||||
const parsed = await iniParse(filePath);
|
const parsed = iniParse(filePath);
|
||||||
return processUserStats(parsed);
|
return processUserStats(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type == Cracker.rld) {
|
if (type == Cracker.rld) {
|
||||||
const parsed = await iniParse(filePath);
|
const parsed = iniParse(filePath);
|
||||||
return processRld(parsed);
|
return processRld(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === Cracker.skidrow) {
|
if (type === Cracker.skidrow) {
|
||||||
const parsed = await iniParse(filePath);
|
const parsed = iniParse(filePath);
|
||||||
return processSkidrow(parsed);
|
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 [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const iniParse = async (filePath: string) => {
|
const iniParse = (filePath: string) => {
|
||||||
try {
|
try {
|
||||||
const file = createReadStream(filePath);
|
const lines = readFileSync(filePath, "utf-8").split(/[\r\n]+/);
|
||||||
|
|
||||||
const lines = readline.createInterface({
|
|
||||||
input: file,
|
|
||||||
crlfDelay: Infinity,
|
|
||||||
});
|
|
||||||
|
|
||||||
let objectName = "";
|
let objectName = "";
|
||||||
const object: Record<string, Record<string, string | number>> = {};
|
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.length) continue;
|
||||||
|
|
||||||
if (line.startsWith("[") && line.endsWith("]")) {
|
if (line.startsWith("[") && line.endsWith("]")) {
|
||||||
@ -69,13 +86,13 @@ const iniParse = async (filePath: string) => {
|
|||||||
object[objectName] = {};
|
object[objectName] = {};
|
||||||
} else {
|
} else {
|
||||||
const [name, ...value] = line.split("=");
|
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;
|
return object;
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
achievementsLogger.error(`Error parsing ${filePath}`, err);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -83,7 +100,8 @@ const iniParse = async (filePath: string) => {
|
|||||||
const jsonParse = (filePath: string) => {
|
const jsonParse = (filePath: string) => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(readFileSync(filePath, "utf-8"));
|
return JSON.parse(readFileSync(filePath, "utf-8"));
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
achievementsLogger.error(`Error parsing ${filePath}`, err);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -97,7 +115,28 @@ const processOnlineFix = (unlockedAchievements: any): UnlockedAchievement[] => {
|
|||||||
if (unlockedAchievement?.achieved) {
|
if (unlockedAchievement?.achieved) {
|
||||||
parsedUnlockedAchievements.push({
|
parsedUnlockedAchievements.push({
|
||||||
name: achievement,
|
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") {
|
if (unlockedAchievement[0] === "1") {
|
||||||
parsedUnlockedAchievements.push({
|
parsedUnlockedAchievements.push({
|
||||||
name: achievement,
|
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) {
|
if (unlockedAchievement?.earned) {
|
||||||
newUnlockedAchievements.push({
|
newUnlockedAchievements.push({
|
||||||
name: achievement,
|
name: achievement,
|
||||||
unlockTime: unlockedAchievement.earned_time,
|
unlockTime: unlockedAchievement.earned_time * 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newUnlockedAchievements;
|
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 processDefault = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||||
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||||
|
|
||||||
@ -148,7 +210,7 @@ const processDefault = (unlockedAchievements: any): UnlockedAchievement[] => {
|
|||||||
if (unlockedAchievement?.Achieved) {
|
if (unlockedAchievement?.Achieved) {
|
||||||
newUnlockedAchievements.push({
|
newUnlockedAchievements.push({
|
||||||
name: achievement,
|
name: achievement,
|
||||||
unlockTime: unlockedAchievement.UnlockTime,
|
unlockTime: unlockedAchievement.UnlockTime * 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,11 +229,12 @@ const processRld = (unlockedAchievements: any): UnlockedAchievement[] => {
|
|||||||
if (unlockedAchievement?.State) {
|
if (unlockedAchievement?.State) {
|
||||||
newUnlockedAchievements.push({
|
newUnlockedAchievements.push({
|
||||||
name: achievement,
|
name: achievement,
|
||||||
unlockTime: new DataView(
|
unlockTime:
|
||||||
|
new DataView(
|
||||||
new Uint8Array(
|
new Uint8Array(
|
||||||
Buffer.from(unlockedAchievement.Time.toString(), "hex")
|
Buffer.from(unlockedAchievement.Time.toString(), "hex")
|
||||||
).buffer
|
).buffer
|
||||||
).getUint32(0, true),
|
).getUint32(0, true) * 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -195,8 +258,8 @@ const processUserStats = (unlockedAchievements: any): UnlockedAchievement[] => {
|
|||||||
|
|
||||||
if (!isNaN(unlockTime)) {
|
if (!isNaN(unlockTime)) {
|
||||||
newUnlockedAchievements.push({
|
newUnlockedAchievements.push({
|
||||||
name: achievement,
|
name: achievement.replace(/"/g, ``),
|
||||||
unlockTime: unlockTime,
|
unlockTime: unlockTime * 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,96 +2,82 @@ import { gameAchievementRepository, gameRepository } from "@main/repository";
|
|||||||
import {
|
import {
|
||||||
findAllAchievementFiles,
|
findAllAchievementFiles,
|
||||||
findAchievementFiles,
|
findAchievementFiles,
|
||||||
|
findAchievementFileInExecutableDirectory,
|
||||||
|
getAlternativeObjectIds,
|
||||||
} from "./find-achivement-files";
|
} from "./find-achivement-files";
|
||||||
import { parseAchievementFile } from "./parse-achievement-file";
|
import { parseAchievementFile } from "./parse-achievement-file";
|
||||||
import { mergeAchievements } from "./merge-achievements";
|
import { mergeAchievements } from "./merge-achievements";
|
||||||
import type { UnlockedAchievement } from "@types";
|
import type { UnlockedAchievement } from "@types";
|
||||||
import { getGameAchievementData } from "./get-game-achievement-data";
|
import { getGameAchievementData } from "./get-game-achievement-data";
|
||||||
|
import { achievementsLogger } from "../logger";
|
||||||
|
import { Game } from "@main/entity";
|
||||||
|
|
||||||
export const updateAllLocalUnlockedAchievements = async () => {
|
export const updateAllLocalUnlockedAchievements = async () => {
|
||||||
const gameAchievementFilesMap = await findAllAchievementFiles();
|
const gameAchievementFilesMap = findAllAchievementFiles();
|
||||||
|
|
||||||
for (const objectId of gameAchievementFilesMap.keys()) {
|
const games = await gameRepository.find({
|
||||||
const gameAchievementFiles = gameAchievementFilesMap.get(objectId)!;
|
where: {
|
||||||
|
isDeleted: false,
|
||||||
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"]
|
});
|
||||||
);
|
|
||||||
|
for (const game of games) {
|
||||||
|
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
||||||
|
const gameAchievementFiles = gameAchievementFilesMap.get(objectId) || [];
|
||||||
|
const achievementFileInsideDirectory =
|
||||||
|
findAchievementFileInExecutableDirectory(game);
|
||||||
|
|
||||||
|
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
||||||
|
|
||||||
|
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) {
|
for (const achievementFile of gameAchievementFiles) {
|
||||||
const parsedAchievements = await parseAchievementFile(
|
const parsedAchievements = parseAchievementFile(
|
||||||
achievementFile.filePath,
|
achievementFile.filePath,
|
||||||
achievementFile.type
|
achievementFile.type
|
||||||
);
|
);
|
||||||
console.log("Parsed for", game.title, parsedAchievements);
|
|
||||||
if (parsedAchievements.length) {
|
if (parsedAchievements.length) {
|
||||||
unlockedAchievements.push(...parsedAchievements);
|
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) => {
|
export const updateLocalUnlockedAchivements = async (game: Game) => {
|
||||||
const [game, localAchievements] = await Promise.all([
|
const gameAchievementFiles = findAchievementFiles(game);
|
||||||
gameRepository.findOne({
|
|
||||||
where: { objectID: objectId, shop: "steam", isDeleted: false },
|
|
||||||
}),
|
|
||||||
gameAchievementRepository.findOne({
|
|
||||||
where: { objectId, shop: "steam" },
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!game) return;
|
const achievementFileInsideDirectory =
|
||||||
|
findAchievementFileInExecutableDirectory(game);
|
||||||
|
|
||||||
const gameAchievementFiles = await findAchievementFiles(game);
|
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
||||||
|
|
||||||
console.log("Achievements files for", game.title, gameAchievementFiles);
|
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[] = [];
|
const unlockedAchievements: UnlockedAchievement[] = [];
|
||||||
|
|
||||||
for (const achievementFile of gameAchievementFiles) {
|
for (const achievementFile of gameAchievementFiles) {
|
||||||
const localAchievementFile = await parseAchievementFile(
|
const localAchievementFile = parseAchievementFile(
|
||||||
achievementFile.filePath,
|
achievementFile.filePath,
|
||||||
achievementFile.type
|
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");
|
return path.join(logsPath, "pythoninstance.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message?.scope == "achievements") {
|
||||||
|
return path.join(logsPath, "achievements.txt");
|
||||||
|
}
|
||||||
|
|
||||||
if (message?.level === "error") {
|
if (message?.level === "error") {
|
||||||
return path.join(logsPath, "error.txt");
|
return path.join(logsPath, "error.txt");
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,6 @@ export const startMainLoop = async () => {
|
|||||||
watchAchievements(),
|
watchAchievements(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await sleep(1000);
|
await sleep(1500);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -14,6 +14,7 @@ import type {
|
|||||||
} from "@types";
|
} from "@types";
|
||||||
import type { CatalogueCategory } from "@shared";
|
import type { CatalogueCategory } from "@shared";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
|
import { GameAchievement } from "@main/entity";
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electron", {
|
contextBridge.exposeInMainWorld("electron", {
|
||||||
/* Torrenting */
|
/* Torrenting */
|
||||||
@ -50,8 +51,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
getGameStats: (objectId: string, shop: GameShop) =>
|
getGameStats: (objectId: string, shop: GameShop) =>
|
||||||
ipcRenderer.invoke("getGameStats", objectId, shop),
|
ipcRenderer.invoke("getGameStats", objectId, shop),
|
||||||
getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"),
|
getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"),
|
||||||
getGameAchievements: (objectId: string, shop: GameShop) =>
|
getGameAchievements: (objectId: string, shop: GameShop, userId?: string) =>
|
||||||
ipcRenderer.invoke("getGameAchievements", objectId, shop),
|
ipcRenderer.invoke("getGameAchievements", objectId, shop, userId),
|
||||||
onAchievementUnlocked: (
|
onAchievementUnlocked: (
|
||||||
cb: (
|
cb: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@ -69,6 +70,22 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
return () =>
|
return () =>
|
||||||
ipcRenderer.removeListener("on-achievement-unlocked", listener);
|
ipcRenderer.removeListener("on-achievement-unlocked", listener);
|
||||||
},
|
},
|
||||||
|
onUpdateAchievements: (
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
cb: (achievements: GameAchievement[]) => void
|
||||||
|
) => {
|
||||||
|
const listener = (
|
||||||
|
_event: Electron.IpcRendererEvent,
|
||||||
|
achievements: GameAchievement[]
|
||||||
|
) => cb(achievements);
|
||||||
|
ipcRenderer.on(`on-update-achievements-${objectId}-${shop}`, listener);
|
||||||
|
return () =>
|
||||||
|
ipcRenderer.removeListener(
|
||||||
|
`on-update-achievements-${objectId}-${shop}`,
|
||||||
|
listener
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
/* User preferences */
|
/* User preferences */
|
||||||
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
|
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
|
||||||
|
@ -132,13 +132,14 @@ export function GameDetailsContextProvider({
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.electron.getGameStats(objectId!, shop as GameShop).then((result) => {
|
window.electron.getGameStats(objectId, shop as GameShop).then((result) => {
|
||||||
setStats(result);
|
setStats(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.electron
|
window.electron
|
||||||
.getGameAchievements(objectId!, shop as GameShop)
|
.getGameAchievements(objectId, shop as GameShop)
|
||||||
.then((achievements) => {
|
.then((achievements) => {
|
||||||
|
// TODO: race condition
|
||||||
setAchievements(achievements);
|
setAchievements(achievements);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@ -175,14 +176,11 @@ export function GameDetailsContextProvider({
|
|||||||
}, [game?.id, isGameRunning, updateGame]);
|
}, [game?.id, isGameRunning, updateGame]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = window.electron.onAchievementUnlocked(
|
const unsubscribe = window.electron.onUpdateAchievements(
|
||||||
(objectId, shop) => {
|
objectId,
|
||||||
if (objectId !== objectId || shop !== shop) return;
|
shop,
|
||||||
|
(achievements) => {
|
||||||
window.electron
|
setAchievements(achievements);
|
||||||
.getGameAchievements(objectId!, shop as GameShop)
|
|
||||||
.then(setAchievements)
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
8
src/renderer/src/declaration.d.ts
vendored
8
src/renderer/src/declaration.d.ts
vendored
@ -66,7 +66,8 @@ declare global {
|
|||||||
getTrendingGames: () => Promise<TrendingGame[]>;
|
getTrendingGames: () => Promise<TrendingGame[]>;
|
||||||
getGameAchievements: (
|
getGameAchievements: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop
|
shop: GameShop,
|
||||||
|
userId?: string
|
||||||
) => Promise<GameAchievement[]>;
|
) => Promise<GameAchievement[]>;
|
||||||
onAchievementUnlocked: (
|
onAchievementUnlocked: (
|
||||||
cb: (
|
cb: (
|
||||||
@ -75,6 +76,11 @@ declare global {
|
|||||||
achievements?: { displayName: string; iconUrl: string }[]
|
achievements?: { displayName: string; iconUrl: string }[]
|
||||||
) => void
|
) => void
|
||||||
) => () => Electron.IpcRenderer;
|
) => () => Electron.IpcRenderer;
|
||||||
|
onUpdateAchievements: (
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
cb: (achievements: GameAchievement[]) => void
|
||||||
|
) => () => Electron.IpcRenderer;
|
||||||
|
|
||||||
/* Library */
|
/* Library */
|
||||||
addGameToLibrary: (
|
addGameToLibrary: (
|
||||||
|
@ -28,10 +28,11 @@ import {
|
|||||||
import { store } from "./store";
|
import { store } from "./store";
|
||||||
|
|
||||||
import resources from "@locales";
|
import resources from "@locales";
|
||||||
import { Achievement } from "./pages/achievement/achievement";
|
import { AchievementNotification } from "./pages/achievement/notification/achievement-notification";
|
||||||
|
|
||||||
import "./workers";
|
import "./workers";
|
||||||
import { RepacksContextProvider } from "./context";
|
import { RepacksContextProvider } from "./context";
|
||||||
|
import { Achievement } from "./pages/achievement/achievements";
|
||||||
|
|
||||||
Sentry.init({});
|
Sentry.init({});
|
||||||
|
|
||||||
@ -69,8 +70,12 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
<Route path="/search" Component={SearchResults} />
|
<Route path="/search" Component={SearchResults} />
|
||||||
<Route path="/settings" Component={Settings} />
|
<Route path="/settings" Component={Settings} />
|
||||||
<Route path="/profile/:userId" Component={Profile} />
|
<Route path="/profile/:userId" Component={Profile} />
|
||||||
|
<Route path="/achievements" Component={Achievement} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/achievement-notification" Component={Achievement} />
|
<Route
|
||||||
|
path="/achievement-notification"
|
||||||
|
Component={AchievementNotification}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</RepacksContextProvider>
|
</RepacksContextProvider>
|
||||||
|
71
src/renderer/src/pages/achievement/achievements.tsx
Normal file
71
src/renderer/src/pages/achievement/achievements.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { useDate } from "@renderer/hooks";
|
||||||
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
import { GameAchievement, GameShop } from "@types";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
|
export function Achievement() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const objectId = searchParams.get("objectId");
|
||||||
|
const shop = searchParams.get("shop");
|
||||||
|
const userId = searchParams.get("userId");
|
||||||
|
|
||||||
|
const { format } = useDate();
|
||||||
|
|
||||||
|
const [achievements, setAchievements] = useState<GameAchievement[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (objectId && shop) {
|
||||||
|
window.electron
|
||||||
|
.getGameAchievements(objectId, shop as GameShop, userId || undefined)
|
||||||
|
.then((achievements) => {
|
||||||
|
setAchievements(achievements);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [objectId, shop, userId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Achievement</h1>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
padding: `${SPACING_UNIT * 2}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{achievements.map((achievement, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
title={achievement.description}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
height: "60px",
|
||||||
|
width: "60px",
|
||||||
|
filter: achievement.unlocked ? "none" : "grayscale(100%)",
|
||||||
|
}}
|
||||||
|
src={
|
||||||
|
achievement.unlocked ? achievement.icon : achievement.icongray
|
||||||
|
}
|
||||||
|
alt={achievement.displayName}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p>{achievement.displayName}</p>
|
||||||
|
{achievement.unlockTime && format(achievement.unlockTime)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { recipe } from "@vanilla-extract/recipes";
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
import { vars } from "../../theme.css";
|
import { vars } from "../../../theme.css";
|
||||||
import { keyframes, style } from "@vanilla-extract/css";
|
import { keyframes, style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
const animationIn = keyframes({
|
const animationIn = keyframes({
|
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import achievementSound from "@renderer/assets/audio/achievement.wav";
|
import achievementSound from "@renderer/assets/audio/achievement.wav";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import * as styles from "./achievement.css";
|
import * as styles from "./achievement-notification.css";
|
||||||
|
|
||||||
interface AchievementInfo {
|
interface AchievementInfo {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -10,7 +10,7 @@ interface AchievementInfo {
|
|||||||
|
|
||||||
const NOTIFICATION_TIMEOUT = 4000;
|
const NOTIFICATION_TIMEOUT = 4000;
|
||||||
|
|
||||||
export function Achievement() {
|
export function AchievementNotification() {
|
||||||
const { t } = useTranslation("achievement");
|
const { t } = useTranslation("achievement");
|
||||||
|
|
||||||
const [isClosing, setIsClosing] = useState(false);
|
const [isClosing, setIsClosing] = useState(false);
|
@ -1,7 +1,7 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import type { HowLongToBeatCategory, SteamAppDetails } from "@types";
|
import type { HowLongToBeatCategory, SteamAppDetails } from "@types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button } from "@renderer/components";
|
import { Button, Link } from "@renderer/components";
|
||||||
|
|
||||||
import * as styles from "./sidebar.css";
|
import * as styles from "./sidebar.css";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
@ -29,6 +29,11 @@ export function Sidebar() {
|
|||||||
|
|
||||||
const { numberFormatter } = useFormat();
|
const { numberFormatter } = useFormat();
|
||||||
|
|
||||||
|
const buildGameAchievementPath = () => {
|
||||||
|
const urlParams = new URLSearchParams({ objectId: objectId!, shop });
|
||||||
|
return `/achievements?${urlParams.toString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (objectId) {
|
if (objectId) {
|
||||||
setHowLongToBeat({ isLoading: true, data: null });
|
setHowLongToBeat({ isLoading: true, data: null });
|
||||||
@ -69,15 +74,24 @@ export function Sidebar() {
|
|||||||
return (
|
return (
|
||||||
<aside className={styles.contentSidebar}>
|
<aside className={styles.contentSidebar}>
|
||||||
{achievements.length > 0 && (
|
{achievements.length > 0 && (
|
||||||
|
<SidebarSection
|
||||||
|
title={t("achievements", {
|
||||||
|
unlockedCount: achievements.filter((a) => a.unlocked).length,
|
||||||
|
achievementsCount: achievements.length,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Link to={buildGameAchievementPath()}>Ver todas</Link>
|
||||||
|
</span>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `${SPACING_UNIT}px`,
|
||||||
padding: `${SPACING_UNIT}px`,
|
padding: `${SPACING_UNIT * 2}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{achievements.map((achievement, index) => (
|
{achievements.slice(0, 6).map((achievement, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
style={{
|
style={{
|
||||||
@ -90,12 +104,14 @@ export function Sidebar() {
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
style={{
|
style={{
|
||||||
height: "72px",
|
height: "60px",
|
||||||
width: "72px",
|
width: "60px",
|
||||||
filter: achievement.unlocked ? "none" : "grayscale(100%)",
|
filter: achievement.unlocked ? "none" : "grayscale(100%)",
|
||||||
}}
|
}}
|
||||||
src={
|
src={
|
||||||
achievement.unlocked ? achievement.icon : achievement.icongray
|
achievement.unlocked
|
||||||
|
? achievement.icon
|
||||||
|
: achievement.icongray
|
||||||
}
|
}
|
||||||
alt={achievement.displayName}
|
alt={achievement.displayName}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@ -107,6 +123,7 @@ export function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</SidebarSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{stats && (
|
{stats && (
|
||||||
|
@ -35,4 +35,7 @@ export enum Cracker {
|
|||||||
skidrow = "SKIDROW",
|
skidrow = "SKIDROW",
|
||||||
creamAPI = "CreamAPI",
|
creamAPI = "CreamAPI",
|
||||||
smartSteamEmu = "SmartSteamEmu",
|
smartSteamEmu = "SmartSteamEmu",
|
||||||
|
_3dm = "3dm",
|
||||||
|
flt = "FLT",
|
||||||
|
rle = "RLE",
|
||||||
}
|
}
|
||||||
|
109
yarn.lock
109
yarn.lock
@ -1135,6 +1135,13 @@
|
|||||||
wrap-ansi "^8.1.0"
|
wrap-ansi "^8.1.0"
|
||||||
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
|
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
|
||||||
|
|
||||||
|
"@isaacs/fs-minipass@^4.0.0":
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32"
|
||||||
|
integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==
|
||||||
|
dependencies:
|
||||||
|
minipass "^7.0.4"
|
||||||
|
|
||||||
"@jimp/bmp@^0.22.12":
|
"@jimp/bmp@^0.22.12":
|
||||||
version "0.22.12"
|
version "0.22.12"
|
||||||
resolved "https://registry.yarnpkg.com/@jimp/bmp/-/bmp-0.22.12.tgz#0316044dc7b1a90274aef266d50349347fb864d4"
|
resolved "https://registry.yarnpkg.com/@jimp/bmp/-/bmp-0.22.12.tgz#0316044dc7b1a90274aef266d50349347fb864d4"
|
||||||
@ -2883,11 +2890,6 @@ acorn@^8.8.1, acorn@^8.8.2:
|
|||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c"
|
||||||
integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==
|
integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==
|
||||||
|
|
||||||
adm-zip@^0.5.16:
|
|
||||||
version "0.5.16"
|
|
||||||
resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909"
|
|
||||||
integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==
|
|
||||||
|
|
||||||
agent-base@6, agent-base@^6.0.2:
|
agent-base@6, agent-base@^6.0.2:
|
||||||
version "6.0.2"
|
version "6.0.2"
|
||||||
resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz"
|
resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz"
|
||||||
@ -3042,32 +3044,6 @@ applescript@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
|
resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
|
||||||
integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
|
integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
|
||||||
|
|
||||||
archiver-utils@^5.0.0, archiver-utils@^5.0.2:
|
|
||||||
version "5.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-5.0.2.tgz#63bc719d951803efc72cf961a56ef810760dd14d"
|
|
||||||
integrity sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==
|
|
||||||
dependencies:
|
|
||||||
glob "^10.0.0"
|
|
||||||
graceful-fs "^4.2.0"
|
|
||||||
is-stream "^2.0.1"
|
|
||||||
lazystream "^1.0.0"
|
|
||||||
lodash "^4.17.15"
|
|
||||||
normalize-path "^3.0.0"
|
|
||||||
readable-stream "^4.0.0"
|
|
||||||
|
|
||||||
archiver@^7.0.1:
|
|
||||||
version "7.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/archiver/-/archiver-7.0.1.tgz#c9d91c350362040b8927379c7aa69c0655122f61"
|
|
||||||
integrity sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==
|
|
||||||
dependencies:
|
|
||||||
archiver-utils "^5.0.2"
|
|
||||||
async "^3.2.4"
|
|
||||||
buffer-crc32 "^1.0.0"
|
|
||||||
readable-stream "^4.0.0"
|
|
||||||
readdir-glob "^1.1.2"
|
|
||||||
tar-stream "^3.0.0"
|
|
||||||
zip-stream "^6.0.1"
|
|
||||||
|
|
||||||
are-we-there-yet@^3.0.0:
|
are-we-there-yet@^3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd"
|
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd"
|
||||||
@ -3200,7 +3176,7 @@ async-exit-hook@^2.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz#8bd8b024b0ec9b1c01cccb9af9db29bd717dfaf3"
|
resolved "https://registry.yarnpkg.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz#8bd8b024b0ec9b1c01cccb9af9db29bd717dfaf3"
|
||||||
integrity sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==
|
integrity sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==
|
||||||
|
|
||||||
async@^3.2.3, async@^3.2.4:
|
async@^3.2.3:
|
||||||
version "3.2.6"
|
version "3.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
|
resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
|
||||||
integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
|
integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
|
||||||
@ -5092,18 +5068,6 @@ glob-parent@^6.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-glob "^4.0.3"
|
is-glob "^4.0.3"
|
||||||
|
|
||||||
glob@^10.0.0, glob@^10.3.12:
|
|
||||||
version "10.4.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
|
|
||||||
integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
|
|
||||||
dependencies:
|
|
||||||
foreground-child "^3.1.0"
|
|
||||||
jackspeak "^3.1.2"
|
|
||||||
minimatch "^9.0.4"
|
|
||||||
minipass "^7.1.2"
|
|
||||||
package-json-from-dist "^1.0.0"
|
|
||||||
path-scurry "^1.11.1"
|
|
||||||
|
|
||||||
glob@^10.3.10:
|
glob@^10.3.10:
|
||||||
version "10.3.15"
|
version "10.3.15"
|
||||||
resolved "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz"
|
resolved "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz"
|
||||||
@ -5115,6 +5079,18 @@ glob@^10.3.10:
|
|||||||
minipass "^7.0.4"
|
minipass "^7.0.4"
|
||||||
path-scurry "^1.11.0"
|
path-scurry "^1.11.0"
|
||||||
|
|
||||||
|
glob@^10.3.12, glob@^10.3.7:
|
||||||
|
version "10.4.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
|
||||||
|
integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
|
||||||
|
dependencies:
|
||||||
|
foreground-child "^3.1.0"
|
||||||
|
jackspeak "^3.1.2"
|
||||||
|
minimatch "^9.0.4"
|
||||||
|
minipass "^7.1.2"
|
||||||
|
package-json-from-dist "^1.0.0"
|
||||||
|
path-scurry "^1.11.1"
|
||||||
|
|
||||||
glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
|
glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
|
||||||
version "7.2.3"
|
version "7.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||||
@ -6392,7 +6368,7 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion "^1.1.7"
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
minimatch@^5.0.1, minimatch@^5.1.0:
|
minimatch@^5.0.1:
|
||||||
version "5.1.6"
|
version "5.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
|
||||||
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
|
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
|
||||||
@ -6517,6 +6493,11 @@ mkdirp@^2.1.3:
|
|||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.6.tgz#964fbcb12b2d8c5d6fbc62a963ac95a273e2cc19"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.6.tgz#964fbcb12b2d8c5d6fbc62a963ac95a273e2cc19"
|
||||||
integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==
|
integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==
|
||||||
|
|
||||||
|
mkdirp@^3.0.1:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50"
|
||||||
|
integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==
|
||||||
|
|
||||||
mlly@^1.4.2, mlly@^1.7.1:
|
mlly@^1.4.2, mlly@^1.7.1:
|
||||||
version "1.7.1"
|
version "1.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.1.tgz#e0336429bb0731b6a8e887b438cbdae522c8f32f"
|
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.1.tgz#e0336429bb0731b6a8e887b438cbdae522c8f32f"
|
||||||
@ -7313,24 +7294,6 @@ readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
|
|||||||
string_decoder "^1.1.1"
|
string_decoder "^1.1.1"
|
||||||
util-deprecate "^1.0.1"
|
util-deprecate "^1.0.1"
|
||||||
|
|
||||||
readable-stream@^4.0.0:
|
|
||||||
version "4.5.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09"
|
|
||||||
integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==
|
|
||||||
dependencies:
|
|
||||||
abort-controller "^3.0.0"
|
|
||||||
buffer "^6.0.3"
|
|
||||||
events "^3.3.0"
|
|
||||||
process "^0.11.10"
|
|
||||||
string_decoder "^1.3.0"
|
|
||||||
|
|
||||||
readdir-glob@^1.1.2:
|
|
||||||
version "1.1.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584"
|
|
||||||
integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==
|
|
||||||
dependencies:
|
|
||||||
minimatch "^5.1.0"
|
|
||||||
|
|
||||||
readdirp@~3.6.0:
|
readdirp@~3.6.0:
|
||||||
version "3.6.0"
|
version "3.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
|
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
|
||||||
@ -7828,17 +7791,6 @@ stop-iteration-iterator@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
internal-slot "^1.0.4"
|
internal-slot "^1.0.4"
|
||||||
|
|
||||||
streamx@^2.15.0:
|
|
||||||
version "2.20.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.20.1.tgz#471c4f8b860f7b696feb83d5b125caab2fdbb93c"
|
|
||||||
integrity sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==
|
|
||||||
dependencies:
|
|
||||||
fast-fifo "^1.3.2"
|
|
||||||
queue-tick "^1.0.1"
|
|
||||||
text-decoder "^1.1.0"
|
|
||||||
optionalDependencies:
|
|
||||||
bare-events "^2.2.0"
|
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0":
|
"string-width-cjs@npm:string-width@^4.2.0":
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
@ -8049,15 +8001,6 @@ tar-stream@^2.1.4:
|
|||||||
inherits "^2.0.3"
|
inherits "^2.0.3"
|
||||||
readable-stream "^3.1.1"
|
readable-stream "^3.1.1"
|
||||||
|
|
||||||
tar-stream@^3.0.0:
|
|
||||||
version "3.1.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b"
|
|
||||||
integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==
|
|
||||||
dependencies:
|
|
||||||
b4a "^1.6.4"
|
|
||||||
fast-fifo "^1.2.0"
|
|
||||||
streamx "^2.15.0"
|
|
||||||
|
|
||||||
tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.1.2:
|
tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.1.2:
|
||||||
version "6.2.1"
|
version "6.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
|
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
|
||||||
|
Loading…
Reference in New Issue
Block a user