From 5707081a423cd41b6e381aff49b9f4edf39dc9a4 Mon Sep 17 00:00:00 2001 From: prostarz Date: Wed, 8 Jan 2025 21:58:24 +0000 Subject: [PATCH] fix(achievements): linux more reliable --- backend/handlers/achievements/locator.ts | 109 ++++++++++++----------- backend/handlers/achievements/parse.ts | 3 +- backend/handlers/achievements/watcher.ts | 32 +++++-- 3 files changed, 86 insertions(+), 58 deletions(-) diff --git a/backend/handlers/achievements/locator.ts b/backend/handlers/achievements/locator.ts index e3b9dcb..9cd25e8 100644 --- a/backend/handlers/achievements/locator.ts +++ b/backend/handlers/achievements/locator.ts @@ -2,7 +2,7 @@ import { Cracker } from "@/@types"; import { AchievementFile } from "@/@types/achievements/types"; import { app } from "electron"; import { existsSync, readdirSync } from "node:fs"; -import { join } from "node:path"; +import { join, resolve } from "node:path"; import { logger } from "../logging"; type PathType = @@ -20,13 +20,22 @@ interface FilePath { class AchievementFileLocator { private static isWindows = process.platform === "win32"; - private static user = !this.isWindows - ? app.getPath("home").split("/").pop() - : undefined; + private static user = this.isWindows + ? undefined + : app.getPath("home").split("/").pop(); private static winePrefix = process.env.WINEPREFIX || join(app.getPath("home"), ".wine"); + private static validateWinePrefix(): void { + if (!existsSync(this.winePrefix)) { + logger.log( + "warn", + `Wine prefix not found at ${this.winePrefix}. Check WINEPREFIX environment variable.` + ); + } + } + private static readonly crackerPaths: Readonly> = Object.freeze({ codex: [ @@ -253,17 +262,6 @@ class AchievementFileLocator { ], }); - /** - * Allows updating the wine prefix dynamically. - * @param newPrefix The new custom Wine prefix path. - */ - static setWinePrefix(newPrefix: string): void { - if (!existsSync(newPrefix)) { - throw new Error(`Specified Wine prefix does not exist: ${newPrefix}`); - } - this.winePrefix = newPrefix; - } - private static getSystemPath(type: PathType): string { const basePaths = { appData: this.isWindows @@ -272,7 +270,7 @@ class AchievementFileLocator { this.winePrefix, "drive_c", "users", - this.user || "", + this.user || "unknown", "AppData", "Roaming" ), @@ -282,7 +280,7 @@ class AchievementFileLocator { this.winePrefix, "drive_c", "users", - this.user || "", + this.user || "unknown", "Documents" ), publicDocuments: this.isWindows @@ -294,7 +292,7 @@ class AchievementFileLocator { this.winePrefix, "drive_c", "users", - this.user || "", + this.user || "unknown", "AppData", "Local" ), @@ -304,37 +302,19 @@ class AchievementFileLocator { winePrefix: this.winePrefix, }; - return ( - basePaths[type] || - (() => { - throw new Error(`Unknown path type: ${type}`); - })() - ); - } - - static getCrackerPath(cracker: Cracker): FilePath[] { - return this.crackerPaths[cracker] || []; - } - - private static replacePlaceholders( - path: string, - gameStoreId: string - ): string { - if (!gameStoreId) { - throw new Error("Invalid gameStoreId provided"); + const path = basePaths[type]; + if (!path) { + throw new Error(`Unknown path type: ${type}`); } - return path.replace(//g, gameStoreId); + return resolve(path); } - private static buildFilePath( - folderPath: string, - fileLocations: string[], - gameStoreId: string - ): string { - const mappedLocations = fileLocations.map((location) => - this.replacePlaceholders(location, gameStoreId) - ); - return join(folderPath, ...mappedLocations); + static setWinePrefix(newPrefix: string): void { + if (!existsSync(newPrefix)) { + throw new Error(`Specified Wine prefix does not exist: ${newPrefix}`); + } + this.winePrefix = newPrefix; + this.validateWinePrefix(); } static findAllAchievementFiles( @@ -343,7 +323,11 @@ class AchievementFileLocator { if (winePrefix) this.setWinePrefix(winePrefix); const gameAchievementFiles = new Map(); - console.log(`Searching for achievement files`); + + logger.log( + "info", + `Searching for achievement files for platform: ${process.platform}` + ); for (const [cracker, paths] of Object.entries(this.crackerPaths) as [ Cracker, @@ -351,11 +335,20 @@ class AchievementFileLocator { ][]) { paths.forEach( ({ achievement_folder_location, achievement_file_location }) => { - if (!existsSync(achievement_folder_location)) return; - - console.log(`${achievement_folder_location}`); + if (!existsSync(achievement_folder_location)) { + logger.log( + "debug", + `Folder not found: ${achievement_folder_location}` + ); + return; + } + logger.log( + "debug", + `Processing folder: ${achievement_folder_location}` + ); const gameStoreIds = readdirSync(achievement_folder_location); + gameStoreIds.forEach((gameStoreId) => { const filePath = this.buildFilePath( achievement_folder_location, @@ -363,7 +356,10 @@ class AchievementFileLocator { gameStoreId ); - if (!existsSync(filePath)) return; + if (!existsSync(filePath)) { + logger.log("debug", `File not found: ${filePath}`); + return; + } const achievementFile: AchievementFile = { cracker, @@ -383,6 +379,17 @@ class AchievementFileLocator { return gameAchievementFiles; } + private static buildFilePath( + folderPath: string, + fileLocations: string[], + gameStoreId: string + ): string { + const mappedLocations = fileLocations.map((location) => + location.replace(//g, gameStoreId) + ); + return join(folderPath, ...mappedLocations); + } + static findAchievementFiles( gameStoreId: string, winePrefix?: string | null diff --git a/backend/handlers/achievements/parse.ts b/backend/handlers/achievements/parse.ts index a726a94..7ce0391 100644 --- a/backend/handlers/achievements/parse.ts +++ b/backend/handlers/achievements/parse.ts @@ -81,7 +81,8 @@ class AchievementParser { private processFlatFileAchievements(filePath: string): UnlockedAchievement[] { try { - const achievementFiles = readdirSync(filePath); + const normalizedPath = path.normalize(filePath); + const achievementFiles = readdirSync(normalizedPath); return achievementFiles.map((filename) => ({ name: filename, unlockTime: Date.now(), diff --git a/backend/handlers/achievements/watcher.ts b/backend/handlers/achievements/watcher.ts index b916e05..48d5150 100644 --- a/backend/handlers/achievements/watcher.ts +++ b/backend/handlers/achievements/watcher.ts @@ -1,25 +1,44 @@ -import { FSWatcher, watch, WatchListener } from "node:fs"; +import { FSWatcher, statSync, watch, WatchListener } from "node:fs"; +import { platform } from "node:os"; class AchievementWatcher { private filePath: string; private watcher: FSWatcher | null = null; + private isLinux: boolean = platform() === "linux"; constructor(filePath: string) { if (!filePath) { - throw new Error("filePath are required."); + throw new Error("filePath is required."); } this.filePath = filePath; + + // Validate file path exists and is accessible + try { + const stats = statSync(this.filePath); + if (!stats.isFile()) { + throw new Error("Provided path is not a file."); + } + } catch (err) { + throw new Error(`Invalid file path: ${(err as Error).message}`); + } } /** Initializes the watcher if not already started */ start(callback?: WatchListener): void { if (this.watcher) { console.warn("Watcher already running. Restarting..."); - this.restart(); + this.restart(callback); return; } + console.log(`Watching file: ${this.filePath}`); - this.watcher = watch(this.filePath, callback); + if (this.isLinux) { + console.log("Using Linux-compatible watcher settings."); + this.watcher = watch(this.filePath, { persistent: true }, callback); + } else { + console.log("Using default watcher settings."); + this.watcher = watch(this.filePath, callback); + } } /** Closes and nullifies the watcher */ @@ -33,9 +52,9 @@ class AchievementWatcher { } /** Restarts the watcher */ - restart(): void { + restart(callback?: WatchListener): void { this.destroy(); - this.start(); + this.start(callback); } /** Attaches an event listener to the watcher */ @@ -72,6 +91,7 @@ class AchievementWatcher { console.log("Watcher Status:"); console.log(`File Path: ${this.filePath}`); console.log(`Running: ${this.isRunning()}`); + console.log(`Platform: ${this.isLinux ? "Linux" : "Other (Windows/Mac)"}`); } /** Ensures that the watcher is initialized before performing operations */