mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feature: wip-game-achievements
refactor: rename files
This commit is contained in:
parent
fabeedaa8a
commit
8fb62af0cf
18 changed files with 739 additions and 0 deletions
|
@ -7,6 +7,7 @@ import {
|
|||
Repack,
|
||||
UserPreferences,
|
||||
UserAuth,
|
||||
GameAchievement,
|
||||
} from "@main/entity";
|
||||
|
||||
import { databasePath } from "./constants";
|
||||
|
@ -21,6 +22,7 @@ export const dataSource = new DataSource({
|
|||
DownloadSource,
|
||||
DownloadQueue,
|
||||
UserAuth,
|
||||
GameAchievement,
|
||||
],
|
||||
synchronize: false,
|
||||
database: databasePath,
|
||||
|
|
21
src/main/entity/game-achievements.entity.ts
Normal file
21
src/main/entity/game-achievements.entity.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import {
|
||||
Column,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from "typeorm";
|
||||
import { Game } from "./game.entity";
|
||||
|
||||
@Entity("game_achievement")
|
||||
export class GameAchievement {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@OneToOne(() => Game)
|
||||
@JoinColumn()
|
||||
game: Game;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
achievements: string;
|
||||
}
|
|
@ -2,6 +2,8 @@ export * from "./game.entity";
|
|||
export * from "./repack.entity";
|
||||
export * from "./user-preferences.entity";
|
||||
export * from "./game-shop-cache.entity";
|
||||
export * from "./game.entity";
|
||||
export * from "./game-achievements.entity";
|
||||
export * from "./download-source.entity";
|
||||
export * from "./download-queue.entity";
|
||||
export * from "./user-auth";
|
||||
|
|
89
src/main/events/achievements/game-achievements-observer.ts
Normal file
89
src/main/events/achievements/game-achievements-observer.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { watch } from "node:fs/promises";
|
||||
import { getGameAchievementsToWatch } from "./services/get-game-achievements-to-watch";
|
||||
import { checkUnlockedAchievements } from "./util/check-unlocked-achievements";
|
||||
import { parseAchievementFile } from "./util/parseAchievementFile";
|
||||
import { gameAchievementRepository } from "@main/repository";
|
||||
|
||||
type GameAchievementObserver = {
|
||||
[id: number]: AbortController | null;
|
||||
};
|
||||
|
||||
const gameAchievementObserver: GameAchievementObserver = {};
|
||||
|
||||
export const startGameAchievementObserver = async (gameId: number) => {
|
||||
if (
|
||||
gameAchievementObserver[gameId] === null ||
|
||||
gameAchievementObserver[gameId]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Starting: ${gameId}`);
|
||||
|
||||
const achievementsToWatch = await getGameAchievementsToWatch(gameId);
|
||||
|
||||
if (!achievementsToWatch) {
|
||||
console.log("No achievements to observe");
|
||||
gameAchievementObserver[gameId] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const { steamId, checkedAchievements, achievementFiles } =
|
||||
achievementsToWatch;
|
||||
|
||||
gameAchievementObserver[gameId] = new AbortController();
|
||||
|
||||
const achievements = checkedAchievements.all;
|
||||
|
||||
for (const file of achievementFiles) {
|
||||
const signal = gameAchievementObserver[gameId]?.signal;
|
||||
if (!signal) return;
|
||||
console.log(`cracker: ${file.type}, steamId: ${steamId}`);
|
||||
(async () => {
|
||||
try {
|
||||
const watcher = watch(file.filePath, {
|
||||
signal,
|
||||
});
|
||||
for await (const event of watcher) {
|
||||
if (event.eventType === "change") {
|
||||
console.log("file modified");
|
||||
const localAchievementFile = await parseAchievementFile(
|
||||
file.filePath
|
||||
);
|
||||
|
||||
if (!localAchievementFile) continue;
|
||||
|
||||
const checked = checkUnlockedAchievements(
|
||||
file.type,
|
||||
localAchievementFile,
|
||||
achievements
|
||||
);
|
||||
|
||||
if (checked.new) {
|
||||
console.log(checked.new);
|
||||
|
||||
gameAchievementRepository.update(
|
||||
{
|
||||
game: { id: steamId },
|
||||
},
|
||||
{
|
||||
achievements: JSON.stringify(checked.all),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.log(`cracker: ${file.type}, steamId ${steamId}`);
|
||||
if (err?.name === "AbortError") return;
|
||||
throw err;
|
||||
}
|
||||
})();
|
||||
}
|
||||
};
|
||||
|
||||
export const stopGameAchievementObserver = (gameId: number) => {
|
||||
console.log(`Stopping: ${gameId}`);
|
||||
gameAchievementObserver[gameId]?.abort();
|
||||
delete gameAchievementObserver[gameId];
|
||||
};
|
|
@ -0,0 +1,59 @@
|
|||
import { gameRepository, gameAchievementRepository } from "@main/repository";
|
||||
import { steamGetAchivement } from "../steam/steam-get-achivement";
|
||||
import { steamFindGameAchievementFiles } from "../steam/steam-find-game-achivement-files";
|
||||
import { AchievementFile, CheckedAchievements } from "../types";
|
||||
import { parseAchievementFile } from "../util/parseAchievementFile";
|
||||
import { checkUnlockedAchievements } from "../util/check-unlocked-achievements";
|
||||
|
||||
export const getGameAchievementsToWatch = async (
|
||||
gameId: number
|
||||
): Promise<
|
||||
| {
|
||||
steamId: number;
|
||||
checkedAchievements: CheckedAchievements;
|
||||
achievementFiles: AchievementFile[];
|
||||
}
|
||||
| undefined
|
||||
> => {
|
||||
const game = await gameRepository.findOne({ where: { id: gameId } });
|
||||
|
||||
if (!game || game.shop !== "steam") return;
|
||||
|
||||
const steamId = Number(game.objectID);
|
||||
|
||||
const achievements = await steamGetAchivement(game);
|
||||
console.log(achievements);
|
||||
if (!achievements || !achievements.length) return;
|
||||
|
||||
const achievementFiles = steamFindGameAchievementFiles(game.objectID)[
|
||||
steamId
|
||||
];
|
||||
console.log(achievementFiles);
|
||||
if (!achievementFiles || !achievementFiles.length) return;
|
||||
|
||||
const checkedAchievements: CheckedAchievements = {
|
||||
all: achievements,
|
||||
new: [],
|
||||
};
|
||||
|
||||
for (const achievementFile of achievementFiles) {
|
||||
const file = await parseAchievementFile(achievementFile.filePath);
|
||||
|
||||
checkedAchievements.new.push(
|
||||
...checkUnlockedAchievements(achievementFile.type, file, achievements).new
|
||||
);
|
||||
}
|
||||
|
||||
if (checkedAchievements.new.length) {
|
||||
await gameAchievementRepository.update(
|
||||
{
|
||||
game: { id: gameId },
|
||||
},
|
||||
{
|
||||
achievements: JSON.stringify(checkedAchievements.all),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return { steamId, checkedAchievements, achievementFiles };
|
||||
};
|
|
@ -0,0 +1,74 @@
|
|||
import { gameAchievementRepository, gameRepository } from "@main/repository";
|
||||
import { steamFindGameAchievementFiles } from "../steam/steam-find-game-achivement-files";
|
||||
import { steamGlobalAchievementPercentages } from "../steam/steam-global-achievement-percentages";
|
||||
import { steamAchievementInfo } from "../steam/steam-achievement-info";
|
||||
import { steamAchievementMerge } from "../steam/steam-achievement-merge";
|
||||
import { parseAchievementFile } from "../util/parseAchievementFile";
|
||||
import { checkUnlockedAchievements } from "../util/check-unlocked-achievements";
|
||||
import { CheckedAchievements } from "../types";
|
||||
|
||||
export const saveAllLocalSteamAchivements = async () => {
|
||||
const gameAchievementFiles = steamFindGameAchievementFiles();
|
||||
|
||||
for (const key of Object.keys(gameAchievementFiles)) {
|
||||
const objectId = key;
|
||||
|
||||
const game = await gameRepository.findOne({
|
||||
where: { objectID: objectId },
|
||||
});
|
||||
|
||||
if (!game) continue;
|
||||
|
||||
const hasOnDb = await gameAchievementRepository.existsBy({
|
||||
game: game,
|
||||
});
|
||||
|
||||
if (hasOnDb) continue;
|
||||
|
||||
const achievementPercentage =
|
||||
await steamGlobalAchievementPercentages(objectId);
|
||||
|
||||
if (!achievementPercentage) {
|
||||
await gameAchievementRepository.save({
|
||||
game,
|
||||
achievements: "[]",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const achievementInfo = await steamAchievementInfo(objectId);
|
||||
|
||||
if (!achievementInfo) continue;
|
||||
|
||||
const achievements = steamAchievementMerge(
|
||||
achievementPercentage,
|
||||
achievementInfo
|
||||
);
|
||||
|
||||
if (!achievements) continue;
|
||||
|
||||
const checkedAchievements: CheckedAchievements = {
|
||||
all: achievements,
|
||||
new: [],
|
||||
};
|
||||
|
||||
for (const achievementFile of gameAchievementFiles[key]) {
|
||||
const localAchievementFile = await parseAchievementFile(
|
||||
achievementFile.filePath
|
||||
);
|
||||
|
||||
checkedAchievements.new.push(
|
||||
...checkUnlockedAchievements(
|
||||
achievementFile.type,
|
||||
localAchievementFile,
|
||||
achievements
|
||||
).new
|
||||
);
|
||||
}
|
||||
|
||||
await gameAchievementRepository.save({
|
||||
game,
|
||||
achievements: JSON.stringify(checkedAchievements.all),
|
||||
});
|
||||
}
|
||||
};
|
54
src/main/events/achievements/steam/steam-achievement-info.ts
Normal file
54
src/main/events/achievements/steam/steam-achievement-info.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { logger } from "@main/services";
|
||||
import { AchievementInfo } from "../types";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
export const steamAchievementInfo = async (
|
||||
objectId: string
|
||||
): Promise<AchievementInfo[] | undefined> => {
|
||||
const fetchUrl = `https://steamcommunity.com/stats/${objectId}/achievements`;
|
||||
|
||||
const achievementInfosHtmlText = await fetch(fetchUrl, {
|
||||
method: "GET",
|
||||
//headers: { "Accept-Language": "" },
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.status === 200) return res.text();
|
||||
throw new Error();
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(err, { method: "getSteamGameAchievements" });
|
||||
return;
|
||||
});
|
||||
|
||||
if (!achievementInfosHtmlText) return;
|
||||
|
||||
const achievementInfos: AchievementInfo[] = [];
|
||||
|
||||
const window = new JSDOM(achievementInfosHtmlText).window;
|
||||
|
||||
const itens = Array.from(
|
||||
window.document.getElementsByClassName("achieveRow")
|
||||
);
|
||||
|
||||
for (const item of itens) {
|
||||
const imageUrl = item
|
||||
.getElementsByClassName("achieveImgHolder")?.[0]
|
||||
.getElementsByTagName("img")?.[0]?.src;
|
||||
|
||||
const achievementName = item
|
||||
.getElementsByClassName("achieveTxt")?.[0]
|
||||
.getElementsByTagName("h3")?.[0].innerHTML;
|
||||
|
||||
const achievementDescription = item
|
||||
.getElementsByClassName("achieveTxt")?.[0]
|
||||
.getElementsByTagName("h5")?.[0].innerHTML;
|
||||
|
||||
achievementInfos.push({
|
||||
imageUrl: imageUrl ?? "",
|
||||
title: achievementName ?? "",
|
||||
description: achievementDescription ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
return achievementInfos;
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import { Achievement, AchievementInfo, AchievementPercentage } from "../types";
|
||||
|
||||
export const steamAchievementMerge = (
|
||||
achievementPercentage: AchievementPercentage[],
|
||||
achievementInfo: AchievementInfo[]
|
||||
): Achievement[] | undefined => {
|
||||
if (achievementPercentage.length > achievementInfo.length) return;
|
||||
|
||||
const size = achievementPercentage.length;
|
||||
|
||||
const achievements: Achievement[] = new Array(size);
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
achievements[i] = {
|
||||
id: achievementPercentage[i].name,
|
||||
percent: achievementPercentage[i].percent,
|
||||
imageUrl: achievementInfo[i].imageUrl,
|
||||
title: achievementInfo[i].title,
|
||||
description: achievementInfo[i].description,
|
||||
achieved: false,
|
||||
curProgress: 0,
|
||||
maxProgress: 0,
|
||||
unlockTime: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return achievements;
|
||||
};
|
|
@ -0,0 +1,86 @@
|
|||
import path from "node:path";
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import { Cracker, GameAchievementFiles } from "../types";
|
||||
import { app } from "electron";
|
||||
|
||||
const addGame = (
|
||||
achievementFiles: GameAchievementFiles,
|
||||
gamePath: string,
|
||||
objectId: string,
|
||||
fileLocation: string[],
|
||||
type: Cracker
|
||||
) => {
|
||||
const filePath = path.join(gamePath, objectId, ...fileLocation);
|
||||
|
||||
if (existsSync(filePath)) {
|
||||
const achivementFile = {
|
||||
type,
|
||||
filePath: filePath,
|
||||
};
|
||||
|
||||
achievementFiles[objectId]
|
||||
? achievementFiles[objectId].push(achivementFile)
|
||||
: (achievementFiles[objectId] = [achivementFile]);
|
||||
}
|
||||
};
|
||||
|
||||
export const steamFindGameAchievementFiles = (
|
||||
objectId?: string
|
||||
): GameAchievementFiles => {
|
||||
//TODO: change to a automatized method
|
||||
const publicDir = path.join("C:", "Users", "Public", "Documents");
|
||||
const appData = app.getPath("appData");
|
||||
|
||||
const gameAchievementFiles: GameAchievementFiles = {};
|
||||
|
||||
const crackers: Cracker[] = [
|
||||
Cracker.codex,
|
||||
Cracker.goldberg,
|
||||
Cracker.rune,
|
||||
Cracker.onlineFix,
|
||||
];
|
||||
|
||||
for (const cracker of crackers) {
|
||||
let achievementPath: string;
|
||||
let fileLocation: string[];
|
||||
|
||||
if (cracker === Cracker.onlineFix) {
|
||||
achievementPath = path.join(publicDir, Cracker.onlineFix);
|
||||
fileLocation = ["Stats", "Achievements.ini"];
|
||||
} else if (cracker === Cracker.goldberg) {
|
||||
achievementPath = path.join(appData, "Goldberg SteamEmu Saves");
|
||||
fileLocation = ["achievements.json"];
|
||||
} else {
|
||||
achievementPath = path.join(publicDir, "Steam", cracker);
|
||||
fileLocation = ["achievements.ini"];
|
||||
}
|
||||
|
||||
if (!existsSync(achievementPath)) continue;
|
||||
|
||||
const objectIds = readdirSync(achievementPath);
|
||||
|
||||
if (objectId) {
|
||||
if (objectIds.includes(objectId)) {
|
||||
addGame(
|
||||
gameAchievementFiles,
|
||||
achievementPath,
|
||||
objectId,
|
||||
fileLocation,
|
||||
cracker
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (const objectId of objectIds) {
|
||||
addGame(
|
||||
gameAchievementFiles,
|
||||
achievementPath,
|
||||
objectId,
|
||||
fileLocation,
|
||||
cracker
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gameAchievementFiles;
|
||||
};
|
48
src/main/events/achievements/steam/steam-get-achivement.ts
Normal file
48
src/main/events/achievements/steam/steam-get-achivement.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { gameAchievementRepository } from "@main/repository";
|
||||
import { steamGlobalAchievementPercentages } from "./steam-global-achievement-percentages";
|
||||
import { steamAchievementInfo } from "./steam-achievement-info";
|
||||
import { steamAchievementMerge } from "./steam-achievement-merge";
|
||||
import { Achievement } from "../types";
|
||||
import { Game } from "@main/entity";
|
||||
|
||||
export const steamGetAchivement = async (
|
||||
game: Game
|
||||
): Promise<Achievement[] | undefined> => {
|
||||
const gameAchivement = await gameAchievementRepository.findOne({
|
||||
where: { game: game },
|
||||
});
|
||||
|
||||
if (!gameAchivement) {
|
||||
const achievementPercentage = await steamGlobalAchievementPercentages(
|
||||
game.objectID
|
||||
);
|
||||
console.log(achievementPercentage);
|
||||
if (!achievementPercentage) {
|
||||
await gameAchievementRepository.save({
|
||||
game,
|
||||
achievements: "[]",
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
const achievementInfo = await steamAchievementInfo(game.objectID);
|
||||
console.log(achievementInfo);
|
||||
if (!achievementInfo) return;
|
||||
|
||||
const achievements = steamAchievementMerge(
|
||||
achievementPercentage,
|
||||
achievementInfo
|
||||
);
|
||||
|
||||
if (!achievements) return;
|
||||
|
||||
await gameAchievementRepository.save({
|
||||
game,
|
||||
achievements: JSON.stringify(achievements),
|
||||
});
|
||||
|
||||
return achievements;
|
||||
} else {
|
||||
return JSON.parse(gameAchivement.achievements);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
import { logger } from "@main/services";
|
||||
import { AchievementPercentage } from "../types";
|
||||
|
||||
interface GlobalAchievementPercentages {
|
||||
achievementpercentages: {
|
||||
achievements: Array<AchievementPercentage>;
|
||||
};
|
||||
}
|
||||
|
||||
export const steamGlobalAchievementPercentages = async (
|
||||
objectId: string
|
||||
): Promise<AchievementPercentage[] | undefined> => {
|
||||
const fetchUrl = `https://api.steampowered.com/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v0002/?gameid=${objectId}`;
|
||||
|
||||
const achievementPercentages: Array<AchievementPercentage> | undefined = (
|
||||
await fetch(fetchUrl, {
|
||||
method: "GET",
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.status === 200) return res.json();
|
||||
return;
|
||||
})
|
||||
.then((data: GlobalAchievementPercentages) => data)
|
||||
.catch((err) => {
|
||||
logger.error(err, { method: "getSteamGameAchievements" });
|
||||
return;
|
||||
})
|
||||
)?.achievementpercentages.achievements;
|
||||
|
||||
if (!achievementPercentages) return;
|
||||
|
||||
return achievementPercentages;
|
||||
};
|
52
src/main/events/achievements/types/index.ts
Normal file
52
src/main/events/achievements/types/index.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
export enum Cracker {
|
||||
codex = "CODEX",
|
||||
rune = "RUNE",
|
||||
onlineFix = "OnlineFix",
|
||||
goldberg = "Goldberg",
|
||||
}
|
||||
|
||||
export interface CheckedAchievements {
|
||||
all: Achievement[];
|
||||
new: Achievement[];
|
||||
}
|
||||
|
||||
export interface Achievement {
|
||||
id: string;
|
||||
percent: number;
|
||||
imageUrl: string;
|
||||
title: string;
|
||||
description: string;
|
||||
achieved: boolean;
|
||||
curProgress: number;
|
||||
maxProgress: number;
|
||||
unlockTime: number;
|
||||
}
|
||||
|
||||
export interface AchievementInfo {
|
||||
imageUrl: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface AchievementPercentage {
|
||||
name: string;
|
||||
percent: number;
|
||||
}
|
||||
|
||||
export interface CheckedAchievement {
|
||||
all: Achievement[];
|
||||
new: Achievement[];
|
||||
}
|
||||
|
||||
export interface AchievementFile {
|
||||
type: Cracker;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
export type GameAchievementFiles = {
|
||||
[id: string]: AchievementFile[];
|
||||
};
|
||||
|
||||
export type GameAchievementFile = {
|
||||
[id: string]: AchievementFile[];
|
||||
};
|
102
src/main/events/achievements/util/check-unlocked-achievements.ts
Normal file
102
src/main/events/achievements/util/check-unlocked-achievements.ts
Normal file
|
@ -0,0 +1,102 @@
|
|||
import { Achievement, CheckedAchievements, Cracker } from "../types";
|
||||
|
||||
export const checkUnlockedAchievements = (
|
||||
type: Cracker,
|
||||
unlockedAchievements: any,
|
||||
achievements: Achievement[]
|
||||
): CheckedAchievements => {
|
||||
if (type === Cracker.onlineFix)
|
||||
return onlineFixMerge(unlockedAchievements, achievements);
|
||||
if (type === Cracker.goldberg)
|
||||
return goldbergUnlockedAchievements(unlockedAchievements, achievements);
|
||||
return defaultMerge(unlockedAchievements, achievements);
|
||||
};
|
||||
|
||||
const onlineFixMerge = (
|
||||
unlockedAchievements: any,
|
||||
achievements: Achievement[]
|
||||
): CheckedAchievements => {
|
||||
const newUnlockedAchievements: Achievement[] = [];
|
||||
|
||||
for (const achievement of achievements) {
|
||||
if (achievement.achieved) continue;
|
||||
|
||||
const unlockedAchievement = unlockedAchievements[achievement.id];
|
||||
|
||||
if (!unlockedAchievement) continue;
|
||||
|
||||
achievement.achieved = Boolean(
|
||||
unlockedAchievement?.achieved ?? achievement.achieved
|
||||
);
|
||||
|
||||
achievement.unlockTime =
|
||||
unlockedAchievement?.timestamp ?? achievement.unlockTime;
|
||||
|
||||
if (achievement.achieved) {
|
||||
newUnlockedAchievements.push(achievement);
|
||||
}
|
||||
}
|
||||
|
||||
return { all: achievements, new: newUnlockedAchievements };
|
||||
};
|
||||
|
||||
const goldbergUnlockedAchievements = (
|
||||
unlockedAchievements: any,
|
||||
achievements: Achievement[]
|
||||
): CheckedAchievements => {
|
||||
const newUnlockedAchievements: Achievement[] = [];
|
||||
|
||||
for (const achievement of achievements) {
|
||||
if (achievement.achieved) continue;
|
||||
|
||||
const unlockedAchievement = unlockedAchievements[achievement.id];
|
||||
|
||||
if (!unlockedAchievement) continue;
|
||||
|
||||
achievement.achieved = Boolean(
|
||||
unlockedAchievement?.earned ?? achievement.achieved
|
||||
);
|
||||
|
||||
achievement.unlockTime =
|
||||
unlockedAchievement?.earned_time ?? achievement.unlockTime;
|
||||
|
||||
if (achievement.achieved) {
|
||||
newUnlockedAchievements.push(achievement);
|
||||
}
|
||||
}
|
||||
return { all: achievements, new: newUnlockedAchievements };
|
||||
};
|
||||
|
||||
const defaultMerge = (
|
||||
unlockedAchievements: any,
|
||||
achievements: Achievement[]
|
||||
): CheckedAchievements => {
|
||||
const newUnlockedAchievements: Achievement[] = [];
|
||||
console.log("checkUnlockedAchievements");
|
||||
for (const achievement of achievements) {
|
||||
if (achievement.achieved) continue;
|
||||
|
||||
const unlockedAchievement = unlockedAchievements[achievement.id];
|
||||
|
||||
if (!unlockedAchievement) continue;
|
||||
|
||||
achievement.achieved = Boolean(
|
||||
unlockedAchievement?.Achieved ?? achievement.achieved
|
||||
);
|
||||
|
||||
achievement.curProgress =
|
||||
unlockedAchievement?.CurProgress ?? achievement.curProgress;
|
||||
|
||||
achievement.maxProgress =
|
||||
unlockedAchievement?.MaxProgress ?? achievement.maxProgress;
|
||||
|
||||
achievement.unlockTime =
|
||||
unlockedAchievement?.UnlockTime ?? achievement.unlockTime;
|
||||
|
||||
if (achievement.achieved) {
|
||||
newUnlockedAchievements.push(achievement);
|
||||
}
|
||||
}
|
||||
console.log("newUnlocked: ", newUnlockedAchievements);
|
||||
return { all: achievements, new: newUnlockedAchievements };
|
||||
};
|
57
src/main/events/achievements/util/parseAchievementFile.ts
Normal file
57
src/main/events/achievements/util/parseAchievementFile.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { existsSync, createReadStream, readFileSync } from "node:fs";
|
||||
import readline from "node:readline";
|
||||
|
||||
export const parseAchievementFile = async (
|
||||
filePath: string
|
||||
): Promise<any | null> => {
|
||||
if (existsSync(filePath)) {
|
||||
if (filePath.endsWith(".ini")) {
|
||||
return iniParse(filePath);
|
||||
}
|
||||
|
||||
if (filePath.endsWith(".json")) {
|
||||
return jsonParse(filePath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const iniParse = async (filePath: string) => {
|
||||
try {
|
||||
const file = createReadStream(filePath);
|
||||
|
||||
const lines = readline.createInterface({
|
||||
input: file,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
let objectName = "";
|
||||
const object: any = {};
|
||||
|
||||
for await (const line of lines) {
|
||||
if (line.startsWith("###") || !line.length) continue;
|
||||
|
||||
if (line.startsWith("[") && line.endsWith("]")) {
|
||||
objectName = line.slice(1, -1);
|
||||
object[objectName] = {};
|
||||
} else {
|
||||
const [name, value] = line.split("=");
|
||||
|
||||
const number = Number(value);
|
||||
|
||||
object[objectName][name] = isNaN(number) ? value : number;
|
||||
}
|
||||
}
|
||||
|
||||
return object;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const jsonParse = (filePath: string) => {
|
||||
try {
|
||||
return JSON.parse(readFileSync(filePath, "utf-8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -6,6 +6,7 @@ import { UpdateUserLanguage } from "./migrations/20240913213944_update_user_lang
|
|||
import { EnsureRepackUris } from "./migrations/20240915035339_ensure_repack_uris";
|
||||
import { app } from "electron";
|
||||
import { FixMissingColumns } from "./migrations/20240918001920_FixMissingColumns";
|
||||
import { CreateGameAchievement } from "./migrations/20240919030940_create_game_achievement";
|
||||
|
||||
export type HydraMigration = Knex.Migration & { name: string };
|
||||
|
||||
|
@ -17,6 +18,7 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
|||
UpdateUserLanguage,
|
||||
EnsureRepackUris,
|
||||
FixMissingColumns,
|
||||
CreateGameAchievement,
|
||||
]);
|
||||
}
|
||||
getMigrationName(migration: HydraMigration): string {
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const CreateGameAchievement: HydraMigration = {
|
||||
name: "CreateGameAchievement",
|
||||
up: async (knex: Knex) => {
|
||||
await knex.schema.createTable("game_achievement", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("gameId").notNullable();
|
||||
table.text("achievements");
|
||||
table.foreign("gameId").references("game.id").onDelete("CASCADE");
|
||||
});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {
|
||||
await knex.schema.dropTable("game_achievement");
|
||||
},
|
||||
};
|
|
@ -7,6 +7,7 @@ import {
|
|||
Repack,
|
||||
UserPreferences,
|
||||
UserAuth,
|
||||
GameAchievement,
|
||||
} from "@main/entity";
|
||||
|
||||
export const gameRepository = dataSource.getRepository(Game);
|
||||
|
@ -24,3 +25,6 @@ export const downloadSourceRepository =
|
|||
export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
|
||||
|
||||
export const userAuthRepository = dataSource.getRepository(UserAuth);
|
||||
|
||||
export const gameAchievementRepository =
|
||||
dataSource.getRepository(GameAchievement);
|
||||
|
|
|
@ -5,6 +5,10 @@ import { createGame, updateGamePlaytime } from "./library-sync";
|
|||
import { GameRunning } from "@types";
|
||||
import { PythonInstance } from "./download";
|
||||
import { Game } from "@main/entity";
|
||||
import {
|
||||
startGameAchievementObserver,
|
||||
stopGameAchievementObserver,
|
||||
} from "@main/events/achievements/game-achievements-observer";
|
||||
|
||||
export const gamesPlaytime = new Map<
|
||||
number,
|
||||
|
@ -74,6 +78,8 @@ function onOpenGame(game: Game) {
|
|||
} else {
|
||||
createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {});
|
||||
}
|
||||
|
||||
startGameAchievementObserver(game.id);
|
||||
}
|
||||
|
||||
function onTickGame(game: Game) {
|
||||
|
@ -125,4 +131,6 @@ const onCloseGame = (game: Game) => {
|
|||
} else {
|
||||
createGame(game).catch(() => {});
|
||||
}
|
||||
|
||||
stopGameAchievementObserver(game.id);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue