From 2a74526b0f684e8c2878000e06282afcdca0cf86 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 2 Jun 2025 21:42:21 -0300 Subject: [PATCH 01/19] feat: optimize achievements sync --- .../events/library/add-game-to-library.ts | 7 ++- .../get-compared-unlocked-achievements.ts | 3 ++ .../events/user/get-unlocked-achievements.ts | 3 ++ .../achievement-watcher-manager.ts | 46 ++++++++++++++++++- .../achievements/merge-achievements.ts | 23 ++++++---- .../update-local-unlocked-achivements.ts | 31 ------------- 6 files changed, 70 insertions(+), 43 deletions(-) delete mode 100644 src/main/services/achievements/update-local-unlocked-achivements.ts diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 5b74cc8c..01495a39 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -1,13 +1,13 @@ import { registerEvent } from "../register-event"; import type { GameShop } from "@types"; import { createGame } from "@main/services/library-sync"; -import { updateLocalUnlockedAchievements } from "@main/services/achievements/update-local-unlocked-achivements"; import { downloadsSublevel, gamesShopAssetsSublevel, gamesSublevel, levelKeys, } from "@main/level"; +import { AchievementWatcherManager } from "@main/services/achievements/achievement-watcher-manager"; const addGameToLibrary = async ( _event: Electron.IpcMainInvokeEvent, @@ -43,7 +43,10 @@ const addGameToLibrary = async ( await createGame(game).catch(() => {}); - updateLocalUnlockedAchievements(game); + AchievementWatcherManager.firstSyncWithRemoteIfNeeded( + game.shop, + game.objectId + ); }; registerEvent("addGameToLibrary", addGameToLibrary); diff --git a/src/main/events/user/get-compared-unlocked-achievements.ts b/src/main/events/user/get-compared-unlocked-achievements.ts index 697ad716..be641f2a 100644 --- a/src/main/events/user/get-compared-unlocked-achievements.ts +++ b/src/main/events/user/get-compared-unlocked-achievements.ts @@ -3,6 +3,7 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; import { db, levelKeys } from "@main/level"; +import { AchievementWatcherManager } from "@main/services/achievements/achievement-watcher-manager"; const getComparedUnlockedAchievements = async ( _event: Electron.IpcMainInvokeEvent, @@ -10,6 +11,8 @@ const getComparedUnlockedAchievements = async ( shop: GameShop, userId: string ) => { + await AchievementWatcherManager.firstSyncWithRemoteIfNeeded(shop, objectId); + const userPreferences = await db.get( levelKeys.userPreferences, { diff --git a/src/main/events/user/get-unlocked-achievements.ts b/src/main/events/user/get-unlocked-achievements.ts index 21aad7a0..2bf4c76e 100644 --- a/src/main/events/user/get-unlocked-achievements.ts +++ b/src/main/events/user/get-unlocked-achievements.ts @@ -2,12 +2,15 @@ import type { GameShop, UserAchievement, UserPreferences } from "@types"; import { registerEvent } from "../register-event"; import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data"; import { db, gameAchievementsSublevel, levelKeys } from "@main/level"; +import { AchievementWatcherManager } from "@main/services/achievements/achievement-watcher-manager"; export const getUnlockedAchievements = async ( objectId: string, shop: GameShop, useCachedData: boolean ): Promise => { + AchievementWatcherManager.firstSyncWithRemoteIfNeeded(shop, objectId); + const cachedAchievements = await gameAchievementsSublevel.get( levelKeys.game(shop, objectId) ); diff --git a/src/main/services/achievements/achievement-watcher-manager.ts b/src/main/services/achievements/achievement-watcher-manager.ts index 5cf09d4f..cf9a544c 100644 --- a/src/main/services/achievements/achievement-watcher-manager.ts +++ b/src/main/services/achievements/achievement-watcher-manager.ts @@ -10,6 +10,7 @@ import { import type { AchievementFile, Game, + GameShop, UnlockedAchievement, UserPreferences, } from "@types"; @@ -146,7 +147,48 @@ const processAchievementFileDiff = async ( }; export class AchievementWatcherManager { - private static hasFinishedMergingWithRemote = false; + private static _hasFinishedMergingWithRemote = false; + + public static get hasFinishedMergingWithRemote() { + return this._hasFinishedMergingWithRemote; + } + + public static readonly alreadySyncedGames: Map = new Map(); + + public static async firstSyncWithRemoteIfNeeded( + shop: GameShop, + objectId: string + ) { + const gameKey = levelKeys.game(shop, objectId); + if (this.alreadySyncedGames.get(gameKey)) return; + + const game = await gamesSublevel.get(gameKey).catch(() => null); + if (!game) return; + + const gameAchievementFiles = findAchievementFiles(game); + + const achievementFileInsideDirectory = + findAchievementFileInExecutableDirectory(game); + + gameAchievementFiles.push(...achievementFileInsideDirectory); + + const unlockedAchievements: UnlockedAchievement[] = []; + + for (const achievementFile of gameAchievementFiles) { + const localAchievementFile = parseAchievementFile( + achievementFile.filePath, + achievementFile.type + ); + + if (localAchievementFile.length) { + unlockedAchievements.push(...localAchievementFile); + } + } + + this.alreadySyncedGames.set(gameKey, true); + + return mergeAchievements(game, unlockedAchievements, false); + } public static watchAchievements() { if (!this.hasFinishedMergingWithRemote) return; @@ -295,6 +337,6 @@ export class AchievementWatcherManager { achievementsLogger.error("Error on preSearchAchievements", err); } - this.hasFinishedMergingWithRemote = true; + this._hasFinishedMergingWithRemote = true; } } diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index 2674e451..058c2bc6 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -14,6 +14,7 @@ import { SubscriptionRequiredError } from "@shared"; import { achievementsLogger } from "../logger"; import { db, gameAchievementsSublevel, levelKeys } from "@main/level"; import { getGameAchievementData } from "./get-game-achievement-data"; +import { AchievementWatcherManager } from "./achievement-watcher-manager"; const isRareAchievement = (points: number) => { const rawPercentage = (50 - Math.sqrt(points)) * 2; @@ -56,9 +57,9 @@ export const mergeAchievements = async ( achievements: UnlockedAchievement[], publishNotification: boolean ) => { - let localGameAchievement = await gameAchievementsSublevel.get( - levelKeys.game(game.shop, game.objectId) - ); + const gameKey = levelKeys.game(game.shop, game.objectId); + + let localGameAchievement = await gameAchievementsSublevel.get(gameKey); const userPreferences = await db.get( levelKeys.userPreferences, { @@ -68,9 +69,7 @@ export const mergeAchievements = async ( if (!localGameAchievement) { await getGameAchievementData(game.objectId, game.shop, true); - localGameAchievement = await gameAchievementsSublevel.get( - levelKeys.game(game.shop, game.objectId) - ); + localGameAchievement = await gameAchievementsSublevel.get(gameKey); } const achievementsData = localGameAchievement?.achievements ?? []; @@ -153,7 +152,12 @@ export const mergeAchievements = async ( } } - if (game.remoteId) { + const shouldSyncWithRemote = + game.remoteId && + (newAchievements.length || + AchievementWatcherManager.hasFinishedMergingWithRemote); + + if (shouldSyncWithRemote) { await HydraApi.put( "/profile/games/achievements", { @@ -194,8 +198,11 @@ export const mergeAchievements = async ( mergedLocalAchievements, publishNotification ); + }) + .finally(() => { + AchievementWatcherManager.alreadySyncedGames.set(gameKey, true); }); - } else { + } else if (newAchievements.length) { await saveAchievementsOnLocal( game.objectId, game.shop, diff --git a/src/main/services/achievements/update-local-unlocked-achivements.ts b/src/main/services/achievements/update-local-unlocked-achivements.ts deleted file mode 100644 index 44f2693a..00000000 --- a/src/main/services/achievements/update-local-unlocked-achivements.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - findAchievementFiles, - findAchievementFileInExecutableDirectory, -} from "./find-achivement-files"; -import { parseAchievementFile } from "./parse-achievement-file"; -import { mergeAchievements } from "./merge-achievements"; -import type { Game, UnlockedAchievement } from "@types"; - -export const updateLocalUnlockedAchievements = async (game: Game) => { - const gameAchievementFiles = findAchievementFiles(game); - - const achievementFileInsideDirectory = - findAchievementFileInExecutableDirectory(game); - - gameAchievementFiles.push(...achievementFileInsideDirectory); - - const unlockedAchievements: UnlockedAchievement[] = []; - - for (const achievementFile of gameAchievementFiles) { - const localAchievementFile = parseAchievementFile( - achievementFile.filePath, - achievementFile.type - ); - - if (localAchievementFile.length) { - unlockedAchievements.push(...localAchievementFile); - } - } - - mergeAchievements(game, unlockedAchievements, false); -}; From 7202f740d32bde209b71ad1c8fcbbec10e6ed1b2 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 2 Jun 2025 21:46:00 -0300 Subject: [PATCH 02/19] feat: sync on open game --- src/main/services/process-watcher.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index 7a2433bc..4d8b2a80 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -8,6 +8,7 @@ import { gamesSublevel, levelKeys } from "@main/level"; import { CloudSync } from "./cloud-sync"; import { logger } from "./logger"; import path from "path"; +import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager"; export const gamesPlaytime = new Map< string, @@ -190,6 +191,11 @@ export const watchProcesses = async () => { function onOpenGame(game: Game) { const now = performance.now(); + AchievementWatcherManager.firstSyncWithRemoteIfNeeded( + game.shop, + game.objectId + ); + gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), { lastTick: now, firstTick: now, From e9032ae6e42201db8ae85de1a63179373e75f660 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 3 Jun 2025 07:12:53 -0300 Subject: [PATCH 03/19] feat: better friend code validation on add friend modal --- src/locales/en/translation.json | 3 ++- src/locales/pt-BR/translation.json | 3 ++- .../src/components/text-field/text-field.scss | 2 +- .../user-friend-modal-add-friend.tsx | 12 ++++++++++-- src/renderer/src/scss/globals.scss | 1 + 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index b01268a3..24de88cf 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -515,7 +515,8 @@ "earned_points": "Earned points", "show_achievements_on_profile": "Show your achievements on your profile", "show_points_on_profile": "Show your earned points on your profile", - "error_adding_friend": "Could not send friend request. Please check friend code" + "error_adding_friend": "Could not send friend request. Please check friend code", + "friend_code_length_error": "Friend code must have 8 characters" }, "achievement": { "achievement_unlocked": "Achievement unlocked", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index a45af998..8373b415 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -508,7 +508,8 @@ "earned_points": "Pontos ganhos", "show_achievements_on_profile": "Exiba suas conquistas no perfil", "show_points_on_profile": "Exiba seus pontos ganhos no perfil", - "error_adding_friend": "Não foi possível enviar o pedido de amizade. Verifique o código de amizade inserido" + "error_adding_friend": "Não foi possível enviar o pedido de amizade. Verifique o código de amizade inserido", + "friend_code_length_error": "Código de amigo deve ter 8 caracteres" }, "achievement": { "achievement_unlocked": "Conquista desbloqueada", diff --git a/src/renderer/src/components/text-field/text-field.scss b/src/renderer/src/components/text-field/text-field.scss index 6defc3c1..56756cda 100644 --- a/src/renderer/src/components/text-field/text-field.scss +++ b/src/renderer/src/components/text-field/text-field.scss @@ -74,6 +74,6 @@ } &__error-label { - color: globals.$danger-color; + color: globals.$error-color; } } diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx index a597c809..84248522 100644 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx +++ b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx @@ -51,6 +51,14 @@ export const UserFriendModalAddFriend = ({ } }; + const validateFriendCode = (callback: () => void) => { + if (friendCode.length === 8) { + return callback(); + } + + showErrorToast(t("friend_code_length_error")); + }; + const handleCancelFriendRequest = (userId: string) => { updateFriendRequestState(userId, "CANCEL").catch(() => { showErrorToast(t("try_again")); @@ -91,13 +99,13 @@ export const UserFriendModalAddFriend = ({ disabled={isAddingFriend} className="user-friend-modal-add-friend__button" type="button" - onClick={handleClickAddFriend} + onClick={() => validateFriendCode(handleClickAddFriend)} > {isAddingFriend ? t("sending") : t("add")}