mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 08:43:48 +03:00
Merge branch 'feature/game-achievements' into chore/test-preview
This commit is contained in:
commit
3ed4547dfe
@ -9,12 +9,11 @@ const getGameStats = async (
|
|||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop
|
shop: GameShop
|
||||||
) => {
|
) => {
|
||||||
const response = await HydraApi.get<GameStats>(
|
return HydraApi.get<GameStats>(
|
||||||
`/games/stats`,
|
`/games/stats`,
|
||||||
{ objectId, shop },
|
{ objectId, shop },
|
||||||
{ needsAuth: false }
|
{ needsAuth: false }
|
||||||
);
|
);
|
||||||
return response;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("getGameStats", getGameStats);
|
registerEvent("getGameStats", getGameStats);
|
||||||
|
@ -44,7 +44,7 @@ const addGameToLibrary = async (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLocalUnlockedAchivements(true, objectID);
|
updateLocalUnlockedAchivements(objectID);
|
||||||
|
|
||||||
const game = await gameRepository.findOne({ where: { objectID } });
|
const game = await gameRepository.findOne({ where: { objectID } });
|
||||||
|
|
||||||
|
@ -1,39 +1,34 @@
|
|||||||
import { checkUnlockedAchievements } from "./check-unlocked-achievements";
|
|
||||||
import { parseAchievementFile } from "./parse-achievement-file";
|
import { parseAchievementFile } from "./parse-achievement-file";
|
||||||
import { Game } from "@main/entity";
|
import { Game } from "@main/entity";
|
||||||
import { mergeAchievements } from "./merge-achievements";
|
import { mergeAchievements } from "./merge-achievements";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import {
|
import {
|
||||||
findAchievementFileInExecutableDirectory,
|
findAchievementFileInExecutableDirectory,
|
||||||
findAllSteamGameAchievementFiles,
|
findAllAchievementFiles,
|
||||||
} from "./find-steam-game-achivement-files";
|
} from "./find-achivement-files";
|
||||||
import type { AchievementFile } from "@types";
|
import type { AchievementFile } from "@types";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
|
|
||||||
const fileStats: Map<string, number> = new Map();
|
const fileStats: Map<string, number> = new Map();
|
||||||
|
|
||||||
const processAchievementFile = async (game: Game, file: AchievementFile) => {
|
const processAchievementFileDiff = async (
|
||||||
const localAchievementFile = await parseAchievementFile(
|
game: Game,
|
||||||
|
file: AchievementFile
|
||||||
|
) => {
|
||||||
|
const unlockedAchievements = await parseAchievementFile(
|
||||||
file.filePath,
|
file.filePath,
|
||||||
file.type
|
file.type
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.log("Parsed achievements file", file.filePath, localAchievementFile);
|
logger.log("Achievements from file", file.filePath, unlockedAchievements);
|
||||||
if (localAchievementFile) {
|
|
||||||
const unlockedAchievements = checkUnlockedAchievements(
|
|
||||||
file.type,
|
|
||||||
localAchievementFile
|
|
||||||
);
|
|
||||||
logger.log("Achievements from file", file.filePath, unlockedAchievements);
|
|
||||||
|
|
||||||
if (unlockedAchievements.length) {
|
if (unlockedAchievements.length) {
|
||||||
return mergeAchievements(
|
return mergeAchievements(
|
||||||
game.objectID,
|
game.objectID,
|
||||||
game.shop,
|
game.shop,
|
||||||
unlockedAchievements,
|
unlockedAchievements,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,14 +48,14 @@ const compareFile = async (game: Game, file: AchievementFile) => {
|
|||||||
stat.mtimeMs,
|
stat.mtimeMs,
|
||||||
fileStats.get(file.filePath)
|
fileStats.get(file.filePath)
|
||||||
);
|
);
|
||||||
await processAchievementFile(game, file);
|
await processAchievementFileDiff(game, file);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
fileStats.set(file.filePath, -1);
|
fileStats.set(file.filePath, -1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const startGameAchievementObserver = async (games: Game[]) => {
|
export const checkAchievementFileChange = async (games: Game[]) => {
|
||||||
const achievementFiles = findAllSteamGameAchievementFiles();
|
const achievementFiles = await findAllAchievementFiles();
|
||||||
|
|
||||||
for (const game of games) {
|
for (const game of games) {
|
||||||
const gameAchievementFiles = achievementFiles.get(game.objectID) || [];
|
const gameAchievementFiles = achievementFiles.get(game.objectID) || [];
|
@ -1,5 +1,5 @@
|
|||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
import { startGameAchievementObserver as searchForAchievements } from "./game-achievements-observer";
|
import { checkAchievementFileChange as searchForAchievements } from "./achievement-file-observer";
|
||||||
|
|
||||||
export const watchAchievements = async () => {
|
export const watchAchievements = async () => {
|
||||||
const games = await gameRepository.find({
|
const games = await gameRepository.find({
|
||||||
|
@ -1,82 +0,0 @@
|
|||||||
import { Cracker } from "@shared";
|
|
||||||
import type { UnlockedAchievement } from "@types";
|
|
||||||
|
|
||||||
export const checkUnlockedAchievements = (
|
|
||||||
type: Cracker,
|
|
||||||
unlockedAchievements: any
|
|
||||||
): UnlockedAchievement[] => {
|
|
||||||
if (type === Cracker.onlineFix) return onlineFixMerge(unlockedAchievements);
|
|
||||||
if (type === Cracker.goldberg || type === Cracker.goldberg2)
|
|
||||||
return goldbergUnlockedAchievements(unlockedAchievements);
|
|
||||||
if (type == Cracker.generic) return genericMerge(unlockedAchievements);
|
|
||||||
return defaultMerge(unlockedAchievements);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onlineFixMerge = (unlockedAchievements: any): UnlockedAchievement[] => {
|
|
||||||
const parsedUnlockedAchievements: UnlockedAchievement[] = [];
|
|
||||||
|
|
||||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
|
||||||
const unlockedAchievement = unlockedAchievements[achievement];
|
|
||||||
|
|
||||||
if (unlockedAchievement?.achieved) {
|
|
||||||
parsedUnlockedAchievements.push({
|
|
||||||
name: achievement,
|
|
||||||
unlockTime: unlockedAchievement.timestamp,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedUnlockedAchievements;
|
|
||||||
};
|
|
||||||
|
|
||||||
const goldbergUnlockedAchievements = (
|
|
||||||
unlockedAchievements: any
|
|
||||||
): UnlockedAchievement[] => {
|
|
||||||
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
|
||||||
|
|
||||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
|
||||||
const unlockedAchievement = unlockedAchievements[achievement];
|
|
||||||
|
|
||||||
if (unlockedAchievement?.earned) {
|
|
||||||
newUnlockedAchievements.push({
|
|
||||||
name: achievement,
|
|
||||||
unlockTime: unlockedAchievement.earned_time,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newUnlockedAchievements;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultMerge = (unlockedAchievements: any): UnlockedAchievement[] => {
|
|
||||||
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
|
||||||
|
|
||||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
|
||||||
const unlockedAchievement = unlockedAchievements[achievement];
|
|
||||||
|
|
||||||
if (unlockedAchievement?.Achieved) {
|
|
||||||
newUnlockedAchievements.push({
|
|
||||||
name: achievement,
|
|
||||||
unlockTime: unlockedAchievement.UnlockTime,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newUnlockedAchievements;
|
|
||||||
};
|
|
||||||
|
|
||||||
const genericMerge = (unlockedAchievements: any): UnlockedAchievement[] => {
|
|
||||||
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
|
||||||
|
|
||||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
|
||||||
const unlockedAchievement = unlockedAchievements[achievement];
|
|
||||||
|
|
||||||
if (unlockedAchievement?.unlocked) {
|
|
||||||
newUnlockedAchievements.push({
|
|
||||||
name: achievement,
|
|
||||||
unlockTime: unlockedAchievement.time,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newUnlockedAchievements;
|
|
||||||
};
|
|
183
src/main/services/achievements/find-achivement-files.ts
Normal file
183
src/main/services/achievements/find-achivement-files.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import { app } from "electron";
|
||||||
|
import type { AchievementFile } from "@types";
|
||||||
|
import { Cracker } from "@shared";
|
||||||
|
import { Game } from "@main/entity";
|
||||||
|
|
||||||
|
//TODO: change to a automatized method
|
||||||
|
const publicDir = path.join("C:", "Users", "Public", "Documents");
|
||||||
|
const programData = path.join("C:", "ProgramData");
|
||||||
|
const appData = app.getPath("appData");
|
||||||
|
const documents = app.getPath("documents");
|
||||||
|
|
||||||
|
const crackers = [
|
||||||
|
Cracker.codex,
|
||||||
|
Cracker.goldberg,
|
||||||
|
Cracker.rune,
|
||||||
|
Cracker.onlineFix,
|
||||||
|
Cracker.userstats,
|
||||||
|
Cracker.rld,
|
||||||
|
Cracker.creamAPI,
|
||||||
|
Cracker.skidrow,
|
||||||
|
Cracker.smartSteamEmu,
|
||||||
|
Cracker.empress,
|
||||||
|
];
|
||||||
|
|
||||||
|
const getPathFromCracker = async (cracker: Cracker) => {
|
||||||
|
if (cracker === Cracker.smartSteamEmu) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
folderPath: path.join(appData, "SmartSteamEmu"),
|
||||||
|
fileLocation: ["User", "Achievements"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cracker === Cracker.onlineFix) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
folderPath: path.join(publicDir, Cracker.onlineFix),
|
||||||
|
fileLocation: ["Stats", "Achievements.ini"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cracker === Cracker.goldberg) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
folderPath: path.join(appData, "Goldberg SteamEmu Saves"),
|
||||||
|
fileLocation: ["achievements.json"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
folderPath: path.join(appData, "GSE Saves"),
|
||||||
|
fileLocation: ["achievements.json"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cracker === Cracker.rld) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
folderPath: path.join(programData, "RLD!"),
|
||||||
|
fileLocation: ["achievements.ini"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cracker === Cracker.creamAPI) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
folderPath: path.join(appData, "CreamAPI"),
|
||||||
|
fileLocation: ["achievements.ini"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cracker === Cracker.skidrow) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
folderPath: path.join(documents, "SKIDROW"),
|
||||||
|
fileLocation: ["SteamEmu", "UserStats", "achiev.ini"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
folderPath: path.join(documents, "Player"),
|
||||||
|
fileLocation: ["SteamEmu", "UserStats", "achiev.ini"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cracker === Cracker.codex) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
folderPath: path.join(publicDir, "Steam", "CODEX"),
|
||||||
|
fileLocation: ["achievements.ini"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
folderPath: path.join(appData, "Steam", "CODEX"),
|
||||||
|
fileLocation: ["achievements.ini"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
folderPath: path.join(publicDir, "Steam", cracker),
|
||||||
|
fileLocation: ["achievements.ini"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findAchievementFiles = async (game: Game) => {
|
||||||
|
const achievementFiles: AchievementFile[] = [];
|
||||||
|
|
||||||
|
for (const cracker of crackers) {
|
||||||
|
for (const { folderPath, fileLocation } of await getPathFromCracker(
|
||||||
|
cracker
|
||||||
|
)) {
|
||||||
|
const filePath = path.join(folderPath, game.objectID, ...fileLocation);
|
||||||
|
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
achievementFiles.push({
|
||||||
|
type: cracker,
|
||||||
|
filePath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return achievementFiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findAchievementFileInExecutableDirectory = (
|
||||||
|
game: Game
|
||||||
|
): AchievementFile | null => {
|
||||||
|
if (!game.executablePath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const steamDataPath = path.join(
|
||||||
|
game.executablePath,
|
||||||
|
"..",
|
||||||
|
"SteamData",
|
||||||
|
"user_stats.ini"
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: Cracker.userstats,
|
||||||
|
filePath: steamDataPath,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findAllAchievementFiles = async () => {
|
||||||
|
const gameAchievementFiles = new Map<string, AchievementFile[]>();
|
||||||
|
|
||||||
|
for (const cracker of crackers) {
|
||||||
|
for (const { folderPath, fileLocation } of await getPathFromCracker(
|
||||||
|
cracker
|
||||||
|
)) {
|
||||||
|
if (!fs.existsSync(folderPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectIds = fs.readdirSync(folderPath);
|
||||||
|
|
||||||
|
for (const objectId of objectIds) {
|
||||||
|
const filePath = path.join(folderPath, objectId, ...fileLocation);
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) continue;
|
||||||
|
|
||||||
|
const achivementFile = {
|
||||||
|
type: cracker,
|
||||||
|
filePath,
|
||||||
|
};
|
||||||
|
|
||||||
|
gameAchievementFiles.get(objectId)
|
||||||
|
? gameAchievementFiles.get(objectId)!.push(achivementFile)
|
||||||
|
: gameAchievementFiles.set(objectId, [achivementFile]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gameAchievementFiles;
|
||||||
|
};
|
@ -1,138 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import { app } from "electron";
|
|
||||||
import type { AchievementFile } from "@types";
|
|
||||||
import { Cracker } from "@shared";
|
|
||||||
import { Game } from "@main/entity";
|
|
||||||
|
|
||||||
//TODO: change to a automatized method
|
|
||||||
const publicDir = path.join("C:", "Users", "Public", "Documents");
|
|
||||||
const appData = app.getPath("appData");
|
|
||||||
|
|
||||||
const crackers = [
|
|
||||||
Cracker.codex,
|
|
||||||
Cracker.goldberg,
|
|
||||||
Cracker.goldberg2,
|
|
||||||
Cracker.rune,
|
|
||||||
Cracker.onlineFix,
|
|
||||||
Cracker.generic,
|
|
||||||
];
|
|
||||||
|
|
||||||
const addGame = (
|
|
||||||
achievementFiles: Map<string, AchievementFile[]>,
|
|
||||||
achievementPath: string,
|
|
||||||
objectId: string,
|
|
||||||
fileLocation: string[],
|
|
||||||
type: Cracker
|
|
||||||
) => {
|
|
||||||
const filePath = path.join(achievementPath, objectId, ...fileLocation);
|
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) return;
|
|
||||||
|
|
||||||
const achivementFile = {
|
|
||||||
type,
|
|
||||||
filePath,
|
|
||||||
};
|
|
||||||
|
|
||||||
achievementFiles.get(objectId)
|
|
||||||
? achievementFiles.get(objectId)!.push(achivementFile)
|
|
||||||
: achievementFiles.set(objectId, [achivementFile]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getObjectIdsInFolder = (path: string) => {
|
|
||||||
if (fs.existsSync(path)) {
|
|
||||||
return fs.readdirSync(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findSteamGameAchievementFiles = (game: Game) => {
|
|
||||||
const achievementFiles: AchievementFile[] = [];
|
|
||||||
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 if (cracker === Cracker.goldberg2) {
|
|
||||||
achievementPath = path.join(appData, "GSE Saves");
|
|
||||||
fileLocation = ["achievements.json"];
|
|
||||||
} else {
|
|
||||||
achievementPath = path.join(publicDir, "Steam", cracker);
|
|
||||||
fileLocation = ["achievements.ini"];
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = path.join(achievementPath, game.objectID, ...fileLocation);
|
|
||||||
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
achievementFiles.push({
|
|
||||||
type: cracker,
|
|
||||||
filePath: path.join(achievementPath, game.objectID, ...fileLocation),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return achievementFiles;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findAchievementFileInExecutableDirectory = (
|
|
||||||
game: Game
|
|
||||||
): AchievementFile | null => {
|
|
||||||
if (!game.executablePath) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const steamDataPath = path.join(
|
|
||||||
game.executablePath,
|
|
||||||
"..",
|
|
||||||
"SteamData",
|
|
||||||
"user_stats.ini"
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: Cracker.generic,
|
|
||||||
filePath: steamDataPath,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findAllSteamGameAchievementFiles = () => {
|
|
||||||
const gameAchievementFiles = new Map<string, AchievementFile[]>();
|
|
||||||
|
|
||||||
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.goldberg2) {
|
|
||||||
achievementPath = path.join(appData, "GSE Saves");
|
|
||||||
fileLocation = ["achievements.json"];
|
|
||||||
} 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"];
|
|
||||||
}
|
|
||||||
|
|
||||||
const objectIds = getObjectIdsInFolder(achievementPath);
|
|
||||||
|
|
||||||
for (const objectId of objectIds) {
|
|
||||||
addGame(
|
|
||||||
gameAchievementFiles,
|
|
||||||
achievementPath,
|
|
||||||
objectId,
|
|
||||||
fileLocation,
|
|
||||||
cracker
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return gameAchievementFiles;
|
|
||||||
};
|
|
@ -1,60 +1,52 @@
|
|||||||
import { Cracker } from "@shared";
|
import { Cracker } from "@shared";
|
||||||
|
import { UnlockedAchievement } from "@types";
|
||||||
import { existsSync, createReadStream, readFileSync } from "node:fs";
|
import { existsSync, createReadStream, readFileSync } from "node:fs";
|
||||||
import readline from "node:readline";
|
import readline from "node:readline";
|
||||||
|
|
||||||
export const parseAchievementFile = async (
|
export const parseAchievementFile = async (
|
||||||
filePath: string,
|
filePath: string,
|
||||||
type: Cracker
|
type: Cracker
|
||||||
): Promise<any | null> => {
|
): Promise<UnlockedAchievement[]> => {
|
||||||
if (existsSync(filePath)) {
|
if (!existsSync(filePath)) return [];
|
||||||
if (type === Cracker.generic) {
|
|
||||||
return genericParse(filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filePath.endsWith(".ini")) {
|
if (type === Cracker.empress) {
|
||||||
return iniParse(filePath);
|
return [];
|
||||||
}
|
|
||||||
|
|
||||||
if (filePath.endsWith(".json")) {
|
|
||||||
return jsonParse(filePath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const genericParse = async (filePath: string) => {
|
if (type === Cracker.skidrow) {
|
||||||
try {
|
const parsed = await iniParse(filePath);
|
||||||
const file = createReadStream(filePath);
|
return processSkidrow(parsed);
|
||||||
|
|
||||||
const lines = readline.createInterface({
|
|
||||||
input: file,
|
|
||||||
crlfDelay: Infinity,
|
|
||||||
});
|
|
||||||
|
|
||||||
const object: Record<string, Record<string, string | number>> = {};
|
|
||||||
|
|
||||||
for await (const line of lines) {
|
|
||||||
if (line.startsWith("###") || !line.length) continue;
|
|
||||||
|
|
||||||
if (line.startsWith("[") && line.endsWith("]")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [name, ...value] = line.split(" = ");
|
|
||||||
const objectName = name.slice(1, -1);
|
|
||||||
object[objectName] = {};
|
|
||||||
|
|
||||||
const joinedValue = value.join("=").slice(1, -1);
|
|
||||||
|
|
||||||
for (const teste of joinedValue.split(",")) {
|
|
||||||
const [name, value] = teste.split("=");
|
|
||||||
object[objectName][name.trim()] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(object);
|
|
||||||
return object;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === Cracker.smartSteamEmu) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === Cracker.creamAPI) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === Cracker.onlineFix) {
|
||||||
|
const parsed = await iniParse(filePath);
|
||||||
|
return processOnlineFix(parsed);
|
||||||
|
}
|
||||||
|
if (type === Cracker.goldberg) {
|
||||||
|
const parsed = await jsonParse(filePath);
|
||||||
|
return processGoldberg(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == Cracker.userstats) {
|
||||||
|
const parsed = await iniParse(filePath);
|
||||||
|
return processUserStats(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == Cracker.rld) {
|
||||||
|
const parsed = await iniParse(filePath);
|
||||||
|
return processRld(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = await iniParse(filePath);
|
||||||
|
return processDefault(parsed);
|
||||||
};
|
};
|
||||||
|
|
||||||
const iniParse = async (filePath: string) => {
|
const iniParse = async (filePath: string) => {
|
||||||
@ -77,17 +69,11 @@ const iniParse = async (filePath: string) => {
|
|||||||
object[objectName] = {};
|
object[objectName] = {};
|
||||||
} else {
|
} else {
|
||||||
const [name, ...value] = line.split("=");
|
const [name, ...value] = line.split("=");
|
||||||
console.log(line);
|
object[objectName][name.trim()] = value.join("").trim();
|
||||||
console.log(name, value);
|
|
||||||
|
|
||||||
const joinedValue = value.join("").trim();
|
|
||||||
|
|
||||||
const number = Number(joinedValue);
|
|
||||||
|
|
||||||
object[objectName][name.trim()] = isNaN(number) ? joinedValue : number;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("Parsed ini", object);
|
||||||
return object;
|
return object;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@ -101,3 +87,119 @@ const jsonParse = (filePath: string) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const processOnlineFix = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||||
|
const parsedUnlockedAchievements: UnlockedAchievement[] = [];
|
||||||
|
|
||||||
|
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||||
|
const unlockedAchievement = unlockedAchievements[achievement];
|
||||||
|
|
||||||
|
if (unlockedAchievement?.achieved) {
|
||||||
|
parsedUnlockedAchievements.push({
|
||||||
|
name: achievement,
|
||||||
|
unlockTime: unlockedAchievement.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedUnlockedAchievements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processSkidrow = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||||
|
const parsedUnlockedAchievements: UnlockedAchievement[] = [];
|
||||||
|
const achievements = unlockedAchievements["Achievements"];
|
||||||
|
|
||||||
|
for (const achievement of Object.keys(achievements)) {
|
||||||
|
const unlockedAchievement = achievements[achievement].split("@");
|
||||||
|
|
||||||
|
if (unlockedAchievement[0] === "1") {
|
||||||
|
parsedUnlockedAchievements.push({
|
||||||
|
name: achievement,
|
||||||
|
unlockTime: unlockedAchievement[unlockedAchievement.length - 1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedUnlockedAchievements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processGoldberg = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||||
|
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||||
|
|
||||||
|
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||||
|
const unlockedAchievement = unlockedAchievements[achievement];
|
||||||
|
|
||||||
|
if (unlockedAchievement?.earned) {
|
||||||
|
newUnlockedAchievements.push({
|
||||||
|
name: achievement,
|
||||||
|
unlockTime: unlockedAchievement.earned_time,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newUnlockedAchievements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processDefault = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||||
|
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||||
|
|
||||||
|
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||||
|
const unlockedAchievement = unlockedAchievements[achievement];
|
||||||
|
|
||||||
|
if (unlockedAchievement?.Achieved) {
|
||||||
|
newUnlockedAchievements.push({
|
||||||
|
name: achievement,
|
||||||
|
unlockTime: unlockedAchievement.UnlockTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newUnlockedAchievements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processRld = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||||
|
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||||
|
|
||||||
|
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||||
|
if (achievement === "Steam") continue;
|
||||||
|
|
||||||
|
const unlockedAchievement = unlockedAchievements[achievement];
|
||||||
|
|
||||||
|
if (unlockedAchievement?.State) {
|
||||||
|
newUnlockedAchievements.push({
|
||||||
|
name: achievement,
|
||||||
|
unlockTime: new DataView(
|
||||||
|
new Uint8Array(
|
||||||
|
Buffer.from(unlockedAchievement.Time.toString(), "hex")
|
||||||
|
).buffer
|
||||||
|
).getUint32(0, true),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newUnlockedAchievements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processUserStats = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||||
|
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||||
|
|
||||||
|
const achievements = unlockedAchievements["ACHIEVEMENTS"];
|
||||||
|
|
||||||
|
if (!achievements) return [];
|
||||||
|
|
||||||
|
for (const achievement of Object.keys(achievements)) {
|
||||||
|
const unlockedAchievement = achievements[achievement];
|
||||||
|
|
||||||
|
const unlockTime = Number(
|
||||||
|
unlockedAchievement.slice(1, -1).replace("unlocked = true, time = ", "")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isNaN(unlockTime)) {
|
||||||
|
newUnlockedAchievements.push({
|
||||||
|
name: achievement,
|
||||||
|
unlockTime: unlockTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newUnlockedAchievements;
|
||||||
|
};
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import { gameAchievementRepository, gameRepository } from "@main/repository";
|
import { gameAchievementRepository, gameRepository } from "@main/repository";
|
||||||
import {
|
import {
|
||||||
findAllSteamGameAchievementFiles,
|
findAllAchievementFiles,
|
||||||
findSteamGameAchievementFiles,
|
findAchievementFiles,
|
||||||
} from "./find-steam-game-achivement-files";
|
} from "./find-achivement-files";
|
||||||
import { parseAchievementFile } from "./parse-achievement-file";
|
import { parseAchievementFile } from "./parse-achievement-file";
|
||||||
import { checkUnlockedAchievements } from "./check-unlocked-achievements";
|
|
||||||
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";
|
||||||
|
|
||||||
export const updateAllLocalUnlockedAchievements = async () => {
|
export const updateAllLocalUnlockedAchievements = async () => {
|
||||||
const gameAchievementFilesMap = findAllSteamGameAchievementFiles();
|
const gameAchievementFilesMap = await findAllAchievementFiles();
|
||||||
|
|
||||||
for (const objectId of gameAchievementFilesMap.keys()) {
|
for (const objectId of gameAchievementFilesMap.keys()) {
|
||||||
const gameAchievementFiles = gameAchievementFilesMap.get(objectId)!;
|
const gameAchievementFiles = gameAchievementFilesMap.get(objectId)!;
|
||||||
@ -26,8 +25,6 @@ export const updateAllLocalUnlockedAchievements = async () => {
|
|||||||
|
|
||||||
if (!game) continue;
|
if (!game) continue;
|
||||||
|
|
||||||
console.log("Achievements files for", game.title, gameAchievementFiles);
|
|
||||||
|
|
||||||
if (!localAchievements || !localAchievements.achievements) {
|
if (!localAchievements || !localAchievements.achievements) {
|
||||||
await getGameAchievementData(objectId, "steam")
|
await getGameAchievementData(objectId, "steam")
|
||||||
.then((achievements) => {
|
.then((achievements) => {
|
||||||
@ -46,18 +43,13 @@ export const updateAllLocalUnlockedAchievements = async () => {
|
|||||||
const unlockedAchievements: UnlockedAchievement[] = [];
|
const unlockedAchievements: UnlockedAchievement[] = [];
|
||||||
|
|
||||||
for (const achievementFile of gameAchievementFiles) {
|
for (const achievementFile of gameAchievementFiles) {
|
||||||
const localAchievementFile = await parseAchievementFile(
|
const parsedAchievements = await parseAchievementFile(
|
||||||
achievementFile.filePath,
|
achievementFile.filePath,
|
||||||
achievementFile.type
|
achievementFile.type
|
||||||
);
|
);
|
||||||
|
console.log("Parsed for", game.title, parsedAchievements);
|
||||||
if (localAchievementFile) {
|
if (parsedAchievements.length) {
|
||||||
unlockedAchievements.push(
|
unlockedAchievements.push(...parsedAchievements);
|
||||||
...checkUnlockedAchievements(
|
|
||||||
achievementFile.type,
|
|
||||||
localAchievementFile
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,10 +57,7 @@ export const updateAllLocalUnlockedAchievements = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateLocalUnlockedAchivements = async (
|
export const updateLocalUnlockedAchivements = async (objectId: string) => {
|
||||||
publishNotification: boolean,
|
|
||||||
objectId: string
|
|
||||||
) => {
|
|
||||||
const [game, localAchievements] = await Promise.all([
|
const [game, localAchievements] = await Promise.all([
|
||||||
gameRepository.findOne({
|
gameRepository.findOne({
|
||||||
where: { objectID: objectId, shop: "steam", isDeleted: false },
|
where: { objectID: objectId, shop: "steam", isDeleted: false },
|
||||||
@ -80,7 +69,7 @@ export const updateLocalUnlockedAchivements = async (
|
|||||||
|
|
||||||
if (!game) return;
|
if (!game) return;
|
||||||
|
|
||||||
const gameAchievementFiles = findSteamGameAchievementFiles(game);
|
const gameAchievementFiles = await findAchievementFiles(game);
|
||||||
|
|
||||||
console.log("Achievements files for", game.title, gameAchievementFiles);
|
console.log("Achievements files for", game.title, gameAchievementFiles);
|
||||||
|
|
||||||
@ -107,17 +96,10 @@ export const updateLocalUnlockedAchivements = async (
|
|||||||
achievementFile.type
|
achievementFile.type
|
||||||
);
|
);
|
||||||
|
|
||||||
if (localAchievementFile) {
|
if (localAchievementFile.length) {
|
||||||
unlockedAchievements.push(
|
unlockedAchievements.push(...localAchievementFile);
|
||||||
...checkUnlockedAchievements(achievementFile.type, localAchievementFile)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeAchievements(
|
mergeAchievements(objectId, "steam", unlockedAchievements, false);
|
||||||
objectId,
|
|
||||||
"steam",
|
|
||||||
unlockedAchievements,
|
|
||||||
publishNotification
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -154,10 +154,10 @@ export class WindowManager {
|
|||||||
focusable: false,
|
focusable: false,
|
||||||
skipTaskbar: true,
|
skipTaskbar: true,
|
||||||
frame: false,
|
frame: false,
|
||||||
width: 240,
|
width: 350,
|
||||||
height: 60,
|
height: 104,
|
||||||
x: 25,
|
x: 0,
|
||||||
y: 25,
|
y: 0,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, "../preload/index.mjs"),
|
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||||
sandbox: false,
|
sandbox: false,
|
||||||
|
44
src/renderer/src/pages/achievement/achievement.css.ts
Normal file
44
src/renderer/src/pages/achievement/achievement.css.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
|
import { vars } from "../../theme.css";
|
||||||
|
import { keyframes, style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
const animationIn = keyframes({
|
||||||
|
"0%": { transform: `translateY(-240px)` },
|
||||||
|
"100%": { transform: "translateY(0)" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const animationOut = keyframes({
|
||||||
|
"0%": { transform: `translateY(0)` },
|
||||||
|
"100%": { transform: "translateY(-240px)" },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const container = recipe({
|
||||||
|
base: {
|
||||||
|
marginTop: "24px",
|
||||||
|
marginLeft: "24px",
|
||||||
|
animationDuration: "1.0s",
|
||||||
|
height: "60px",
|
||||||
|
display: "flex",
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
closing: {
|
||||||
|
true: {
|
||||||
|
animationName: animationOut,
|
||||||
|
transform: "translateY(-240px)",
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
animationName: animationIn,
|
||||||
|
transform: "translateY(0)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const content = style({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: "8px",
|
||||||
|
alignItems: "center",
|
||||||
|
background: vars.color.background,
|
||||||
|
paddingRight: "8px",
|
||||||
|
});
|
@ -1,7 +1,7 @@
|
|||||||
import { 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 { vars } from "@renderer/theme.css";
|
import * as styles from "./achievement.css";
|
||||||
|
|
||||||
interface AchievementInfo {
|
interface AchievementInfo {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -11,8 +11,16 @@ interface AchievementInfo {
|
|||||||
export function Achievement() {
|
export function Achievement() {
|
||||||
const { t } = useTranslation("achievement");
|
const { t } = useTranslation("achievement");
|
||||||
|
|
||||||
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
const [achievements, setAchievements] = useState<AchievementInfo[]>([]);
|
const [achievements, setAchievements] = useState<AchievementInfo[]>([]);
|
||||||
|
const [currentAchievement, setCurrentAchievement] =
|
||||||
|
useState<AchievementInfo | null>(null);
|
||||||
|
|
||||||
const achievementAnimation = useRef(-1);
|
const achievementAnimation = useRef(-1);
|
||||||
|
const closingAnimation = useRef(-1);
|
||||||
|
const visibleAnimation = useRef(-1);
|
||||||
|
|
||||||
const audio = useMemo(() => {
|
const audio = useMemo(() => {
|
||||||
const audio = new Audio(achievementSound);
|
const audio = new Audio(achievementSound);
|
||||||
@ -24,11 +32,9 @@ export function Achievement() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = window.electron.onAchievementUnlocked(
|
const unsubscribe = window.electron.onAchievementUnlocked(
|
||||||
(_object, _shop, achievements) => {
|
(_object, _shop, achievements) => {
|
||||||
if (!achievements) return;
|
if (!achievements || !achievements.length) return;
|
||||||
|
|
||||||
if (achievements.length) {
|
setAchievements((ach) => ach.concat(achievements));
|
||||||
setAchievements((ach) => ach.concat(achievements));
|
|
||||||
}
|
|
||||||
|
|
||||||
audio.play();
|
audio.play();
|
||||||
}
|
}
|
||||||
@ -41,12 +47,37 @@ export function Achievement() {
|
|||||||
|
|
||||||
const hasAchievementsPending = achievements.length > 0;
|
const hasAchievementsPending = achievements.length > 0;
|
||||||
|
|
||||||
|
const startAnimateClosing = useCallback(() => {
|
||||||
|
cancelAnimationFrame(closingAnimation.current);
|
||||||
|
cancelAnimationFrame(visibleAnimation.current);
|
||||||
|
cancelAnimationFrame(achievementAnimation.current);
|
||||||
|
|
||||||
|
setIsClosing(true);
|
||||||
|
|
||||||
|
const zero = performance.now();
|
||||||
|
closingAnimation.current = requestAnimationFrame(
|
||||||
|
function animateClosing(time) {
|
||||||
|
if (time - zero <= 1000) {
|
||||||
|
closingAnimation.current = requestAnimationFrame(animateClosing);
|
||||||
|
} else {
|
||||||
|
setIsVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasAchievementsPending) {
|
if (hasAchievementsPending) {
|
||||||
|
setIsClosing(false);
|
||||||
|
setIsVisible(true);
|
||||||
|
|
||||||
let zero = performance.now();
|
let zero = performance.now();
|
||||||
|
cancelAnimationFrame(closingAnimation.current);
|
||||||
|
cancelAnimationFrame(visibleAnimation.current);
|
||||||
|
cancelAnimationFrame(achievementAnimation.current);
|
||||||
achievementAnimation.current = requestAnimationFrame(
|
achievementAnimation.current = requestAnimationFrame(
|
||||||
function animateLock(time) {
|
function animateLock(time) {
|
||||||
if (time - zero > 3000) {
|
if (time - zero > 2500) {
|
||||||
zero = performance.now();
|
zero = performance.now();
|
||||||
setAchievements((ach) => ach.slice(1));
|
setAchievements((ach) => ach.slice(1));
|
||||||
}
|
}
|
||||||
@ -54,30 +85,30 @@ export function Achievement() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
cancelAnimationFrame(achievementAnimation.current);
|
startAnimateClosing();
|
||||||
}
|
}
|
||||||
}, [hasAchievementsPending]);
|
}, [hasAchievementsPending]);
|
||||||
|
|
||||||
if (!hasAchievementsPending) return null;
|
useEffect(() => {
|
||||||
|
if (achievements.length) {
|
||||||
|
setCurrentAchievement(achievements[0]);
|
||||||
|
}
|
||||||
|
}, [achievements]);
|
||||||
|
|
||||||
|
if (!isVisible || !currentAchievement) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={styles.container({ closing: isClosing })}>
|
||||||
style={{
|
<div className={styles.content}>
|
||||||
display: "flex",
|
<img
|
||||||
flexDirection: "row",
|
src={currentAchievement.iconUrl}
|
||||||
gap: "8px",
|
alt={currentAchievement.displayName}
|
||||||
alignItems: "center",
|
style={{ flex: 1, width: "60px" }}
|
||||||
background: vars.color.background,
|
/>
|
||||||
}}
|
<div>
|
||||||
>
|
<p>{t("achievement_unlocked")}</p>
|
||||||
<img
|
<p>{currentAchievement.displayName}</p>
|
||||||
src={achievements[0].iconUrl}
|
</div>
|
||||||
alt={achievements[0].displayName}
|
|
||||||
style={{ width: 60, height: 60 }}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p>{t("achievement_unlocked")}</p>
|
|
||||||
<p>{achievements[0].displayName}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -29,6 +29,10 @@ export enum Cracker {
|
|||||||
rune = "RUNE",
|
rune = "RUNE",
|
||||||
onlineFix = "OnlineFix",
|
onlineFix = "OnlineFix",
|
||||||
goldberg = "Goldberg",
|
goldberg = "Goldberg",
|
||||||
goldberg2 = "Goldberg2",
|
userstats = "user_stats",
|
||||||
generic = "Generic",
|
rld = "RLD!",
|
||||||
|
empress = "EMPRESS",
|
||||||
|
skidrow = "SKIDROW",
|
||||||
|
creamAPI = "CreamAPI",
|
||||||
|
smartSteamEmu = "SmartSteamEmu",
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user