Skip to content

Commit 5707081

Browse files
author
prostarz
committed
fix(achievements): linux more reliable
1 parent 6e65f6a commit 5707081

File tree

3 files changed

+86
-58
lines changed

3 files changed

+86
-58
lines changed

backend/handlers/achievements/locator.ts

Lines changed: 58 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Cracker } from "@/@types";
22
import { AchievementFile } from "@/@types/achievements/types";
33
import { app } from "electron";
44
import { existsSync, readdirSync } from "node:fs";
5-
import { join } from "node:path";
5+
import { join, resolve } from "node:path";
66
import { logger } from "../logging";
77

88
type PathType =
@@ -20,13 +20,22 @@ interface FilePath {
2020

2121
class AchievementFileLocator {
2222
private static isWindows = process.platform === "win32";
23-
private static user = !this.isWindows
24-
? app.getPath("home").split("/").pop()
25-
: undefined;
23+
private static user = this.isWindows
24+
? undefined
25+
: app.getPath("home").split("/").pop();
2626

2727
private static winePrefix =
2828
process.env.WINEPREFIX || join(app.getPath("home"), ".wine");
2929

30+
private static validateWinePrefix(): void {
31+
if (!existsSync(this.winePrefix)) {
32+
logger.log(
33+
"warn",
34+
`Wine prefix not found at ${this.winePrefix}. Check WINEPREFIX environment variable.`
35+
);
36+
}
37+
}
38+
3039
private static readonly crackerPaths: Readonly<Record<Cracker, FilePath[]>> =
3140
Object.freeze({
3241
codex: [
@@ -253,17 +262,6 @@ class AchievementFileLocator {
253262
],
254263
});
255264

256-
/**
257-
* Allows updating the wine prefix dynamically.
258-
* @param newPrefix The new custom Wine prefix path.
259-
*/
260-
static setWinePrefix(newPrefix: string): void {
261-
if (!existsSync(newPrefix)) {
262-
throw new Error(`Specified Wine prefix does not exist: ${newPrefix}`);
263-
}
264-
this.winePrefix = newPrefix;
265-
}
266-
267265
private static getSystemPath(type: PathType): string {
268266
const basePaths = {
269267
appData: this.isWindows
@@ -272,7 +270,7 @@ class AchievementFileLocator {
272270
this.winePrefix,
273271
"drive_c",
274272
"users",
275-
this.user || "",
273+
this.user || "unknown",
276274
"AppData",
277275
"Roaming"
278276
),
@@ -282,7 +280,7 @@ class AchievementFileLocator {
282280
this.winePrefix,
283281
"drive_c",
284282
"users",
285-
this.user || "",
283+
this.user || "unknown",
286284
"Documents"
287285
),
288286
publicDocuments: this.isWindows
@@ -294,7 +292,7 @@ class AchievementFileLocator {
294292
this.winePrefix,
295293
"drive_c",
296294
"users",
297-
this.user || "",
295+
this.user || "unknown",
298296
"AppData",
299297
"Local"
300298
),
@@ -304,37 +302,19 @@ class AchievementFileLocator {
304302
winePrefix: this.winePrefix,
305303
};
306304

307-
return (
308-
basePaths[type] ||
309-
(() => {
310-
throw new Error(`Unknown path type: ${type}`);
311-
})()
312-
);
313-
}
314-
315-
static getCrackerPath(cracker: Cracker): FilePath[] {
316-
return this.crackerPaths[cracker] || [];
317-
}
318-
319-
private static replacePlaceholders(
320-
path: string,
321-
gameStoreId: string
322-
): string {
323-
if (!gameStoreId) {
324-
throw new Error("Invalid gameStoreId provided");
305+
const path = basePaths[type];
306+
if (!path) {
307+
throw new Error(`Unknown path type: ${type}`);
325308
}
326-
return path.replace(/<game_store_id>/g, gameStoreId);
309+
return resolve(path);
327310
}
328311

329-
private static buildFilePath(
330-
folderPath: string,
331-
fileLocations: string[],
332-
gameStoreId: string
333-
): string {
334-
const mappedLocations = fileLocations.map((location) =>
335-
this.replacePlaceholders(location, gameStoreId)
336-
);
337-
return join(folderPath, ...mappedLocations);
312+
static setWinePrefix(newPrefix: string): void {
313+
if (!existsSync(newPrefix)) {
314+
throw new Error(`Specified Wine prefix does not exist: ${newPrefix}`);
315+
}
316+
this.winePrefix = newPrefix;
317+
this.validateWinePrefix();
338318
}
339319

340320
static findAllAchievementFiles(
@@ -343,27 +323,43 @@ class AchievementFileLocator {
343323
if (winePrefix) this.setWinePrefix(winePrefix);
344324

345325
const gameAchievementFiles = new Map<string, AchievementFile[]>();
346-
console.log(`Searching for achievement files`);
326+
327+
logger.log(
328+
"info",
329+
`Searching for achievement files for platform: ${process.platform}`
330+
);
347331

348332
for (const [cracker, paths] of Object.entries(this.crackerPaths) as [
349333
Cracker,
350334
FilePath[],
351335
][]) {
352336
paths.forEach(
353337
({ achievement_folder_location, achievement_file_location }) => {
354-
if (!existsSync(achievement_folder_location)) return;
355-
356-
console.log(`${achievement_folder_location}`);
338+
if (!existsSync(achievement_folder_location)) {
339+
logger.log(
340+
"debug",
341+
`Folder not found: ${achievement_folder_location}`
342+
);
343+
return;
344+
}
357345

346+
logger.log(
347+
"debug",
348+
`Processing folder: ${achievement_folder_location}`
349+
);
358350
const gameStoreIds = readdirSync(achievement_folder_location);
351+
359352
gameStoreIds.forEach((gameStoreId) => {
360353
const filePath = this.buildFilePath(
361354
achievement_folder_location,
362355
achievement_file_location,
363356
gameStoreId
364357
);
365358

366-
if (!existsSync(filePath)) return;
359+
if (!existsSync(filePath)) {
360+
logger.log("debug", `File not found: ${filePath}`);
361+
return;
362+
}
367363

368364
const achievementFile: AchievementFile = {
369365
cracker,
@@ -383,6 +379,17 @@ class AchievementFileLocator {
383379
return gameAchievementFiles;
384380
}
385381

382+
private static buildFilePath(
383+
folderPath: string,
384+
fileLocations: string[],
385+
gameStoreId: string
386+
): string {
387+
const mappedLocations = fileLocations.map((location) =>
388+
location.replace(/<game_store_id>/g, gameStoreId)
389+
);
390+
return join(folderPath, ...mappedLocations);
391+
}
392+
386393
static findAchievementFiles(
387394
gameStoreId: string,
388395
winePrefix?: string | null

backend/handlers/achievements/parse.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ class AchievementParser {
8181

8282
private processFlatFileAchievements(filePath: string): UnlockedAchievement[] {
8383
try {
84-
const achievementFiles = readdirSync(filePath);
84+
const normalizedPath = path.normalize(filePath);
85+
const achievementFiles = readdirSync(normalizedPath);
8586
return achievementFiles.map((filename) => ({
8687
name: filename,
8788
unlockTime: Date.now(),

backend/handlers/achievements/watcher.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,44 @@
1-
import { FSWatcher, watch, WatchListener } from "node:fs";
1+
import { FSWatcher, statSync, watch, WatchListener } from "node:fs";
2+
import { platform } from "node:os";
23

34
class AchievementWatcher {
45
private filePath: string;
56
private watcher: FSWatcher | null = null;
7+
private isLinux: boolean = platform() === "linux";
68

79
constructor(filePath: string) {
810
if (!filePath) {
9-
throw new Error("filePath are required.");
11+
throw new Error("filePath is required.");
1012
}
1113
this.filePath = filePath;
14+
15+
// Validate file path exists and is accessible
16+
try {
17+
const stats = statSync(this.filePath);
18+
if (!stats.isFile()) {
19+
throw new Error("Provided path is not a file.");
20+
}
21+
} catch (err) {
22+
throw new Error(`Invalid file path: ${(err as Error).message}`);
23+
}
1224
}
1325

1426
/** Initializes the watcher if not already started */
1527
start(callback?: WatchListener<string>): void {
1628
if (this.watcher) {
1729
console.warn("Watcher already running. Restarting...");
18-
this.restart();
30+
this.restart(callback);
1931
return;
2032
}
33+
2134
console.log(`Watching file: ${this.filePath}`);
22-
this.watcher = watch(this.filePath, callback);
35+
if (this.isLinux) {
36+
console.log("Using Linux-compatible watcher settings.");
37+
this.watcher = watch(this.filePath, { persistent: true }, callback);
38+
} else {
39+
console.log("Using default watcher settings.");
40+
this.watcher = watch(this.filePath, callback);
41+
}
2342
}
2443

2544
/** Closes and nullifies the watcher */
@@ -33,9 +52,9 @@ class AchievementWatcher {
3352
}
3453

3554
/** Restarts the watcher */
36-
restart(): void {
55+
restart(callback?: WatchListener<string>): void {
3756
this.destroy();
38-
this.start();
57+
this.start(callback);
3958
}
4059

4160
/** Attaches an event listener to the watcher */
@@ -72,6 +91,7 @@ class AchievementWatcher {
7291
console.log("Watcher Status:");
7392
console.log(`File Path: ${this.filePath}`);
7493
console.log(`Running: ${this.isRunning()}`);
94+
console.log(`Platform: ${this.isLinux ? "Linux" : "Other (Windows/Mac)"}`);
7595
}
7696

7797
/** Ensures that the watcher is initialized before performing operations */

0 commit comments

Comments
 (0)