From f62f61c05180f18fbc819b57d1d1161c1dc0fff9 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Sat, 30 Sep 2023 21:46:25 -0400 Subject: [PATCH 01/92] fix(reorderSiteAwards): add btn class to Save All Changes (#1901) --- public/reorderSiteAwards.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/reorderSiteAwards.php b/public/reorderSiteAwards.php index 01eaeeb8b7..4f8b848660 100644 --- a/public/reorderSiteAwards.php +++ b/public/reorderSiteAwards.php @@ -332,7 +332,7 @@ class='$rowClassNames' - + HTML; } else { From f9c52d4fc80303663822282a86fb02a2ca1b4a65 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Sat, 30 Sep 2023 21:54:31 -0400 Subject: [PATCH 02/92] feat(individualdevstats): migrate game tables to blade and add beaten stats (#1890) --- app/Community/AppServiceProvider.php | 2 + .../Components/DeveloperGameStatsTable.php | 116 ++++ app/Helpers/database/user.php | 17 +- public/individualdevstats.php | 600 +++--------------- .../developer/game-stats-table-row.blade.php | 8 + .../developer/game-stats-table.blade.php | 240 +++++++ 6 files changed, 450 insertions(+), 533 deletions(-) create mode 100644 app/Community/Components/DeveloperGameStatsTable.php create mode 100644 resources/views/community/components/developer/game-stats-table-row.blade.php create mode 100644 resources/views/community/components/developer/game-stats-table.blade.php diff --git a/app/Community/AppServiceProvider.php b/app/Community/AppServiceProvider.php index d817973650..bcfa102778 100644 --- a/app/Community/AppServiceProvider.php +++ b/app/Community/AppServiceProvider.php @@ -14,6 +14,7 @@ use App\Community\Commands\SyncTickets; use App\Community\Commands\SyncUserRelations; use App\Community\Commands\SyncVotes; +use App\Community\Components\DeveloperGameStatsTable; use App\Community\Components\GlobalStatistics; use App\Community\Components\MessageIcon; use App\Community\Components\UserCard; @@ -104,6 +105,7 @@ public function boot(): void TriggerTicketComment::disableSearchSyncing(); UserComment::disableSearchSyncing(); + Blade::component('developer-game-stats-table', DeveloperGameStatsTable::class); Blade::component('global-statistics', GlobalStatistics::class); Blade::component('user-card', UserCard::class); Blade::component('user-progression-status', UserProgressionStatus::class); diff --git a/app/Community/Components/DeveloperGameStatsTable.php b/app/Community/Components/DeveloperGameStatsTable.php new file mode 100644 index 0000000000..ca144d1c2d --- /dev/null +++ b/app/Community/Components/DeveloperGameStatsTable.php @@ -0,0 +1,116 @@ +easiestGame = $easiestGame; + $this->hardestGame = $hardestGame; + $this->numGamesWithLeaderboards = $numGamesWithLeaderboards; + $this->numGamesWithRichPresence = $numGamesWithRichPresence; + $this->numTotalLeaderboards = $numTotalLeaderboards; + $this->statsKind = $statsKind; + $this->targetDeveloperUsername = $targetDeveloperUsername; + $this->targetGameIds = $targetGameIds; + } + + public function render(): View + { + $builtStats = $this->buildStats($this->targetDeveloperUsername, $this->targetGameIds); + + return view('community.components.developer.game-stats-table', array_merge( + $builtStats, [ + 'easiestGame' => $this->easiestGame, + 'hardestGame' => $this->hardestGame, + 'numGamesWithLeaderboards' => $this->numGamesWithLeaderboards, + 'numGamesWithRichPresence' => $this->numGamesWithRichPresence, + 'numTotalLeaderboards' => $this->numTotalLeaderboards, + 'statsKind' => $this->statsKind, + 'targetDeveloperUsername' => $this->targetDeveloperUsername, + 'targetGameIds' => $this->targetGameIds, + ], + )); + } + + private function buildStats(string $targetDeveloperUsername, array $targetGameIds): array + { + $ownAwards = []; + $mostBeatenSoftcoreGame = $mostBeatenHardcoreGame = $mostCompletedGame = $mostMasteredGame = []; + $userMostBeatenSoftcore = $userMostBeatenHardcore = $userMostCompleted = $userMostMastered = []; + $beatenSoftcoreAwards = $beatenHardcoreAwards = $completedAwards = $masteredAwards = 0; + + $mostAwardedGames = getMostAwardedGames($targetGameIds); + foreach ($mostAwardedGames as $game) { + $mostBeatenSoftcoreGame = $this->findMost($game, 'BeatenSoftcore', $mostBeatenSoftcoreGame); + $mostBeatenHardcoreGame = $this->findMost($game, 'BeatenHardcore', $mostBeatenHardcoreGame); + $mostCompletedGame = $this->findMost($game, 'Completed', $mostCompletedGame); + $mostMasteredGame = $this->findMost($game, 'Mastered', $mostMasteredGame); + } + + $mostAwardedUsers = getMostAwardedUsers($targetGameIds); + foreach ($mostAwardedUsers as $userInfo) { + $userMostBeatenSoftcore = $this->findMost($userInfo, 'BeatenSoftcore', $userMostBeatenSoftcore); + $userMostBeatenHardcore = $this->findMost($userInfo, 'BeatenHardcore', $userMostBeatenHardcore); + $userMostCompleted = $this->findMost($userInfo, 'Completed', $userMostCompleted); + $userMostMastered = $this->findMost($userInfo, 'Mastered', $userMostMastered); + + if (strcmp($targetDeveloperUsername, $userInfo['User']) == 0) { + $ownAwards = $userInfo; + } + + $beatenSoftcoreAwards += $userInfo['BeatenSoftcore']; + $beatenHardcoreAwards += $userInfo['BeatenHardcore']; + $completedAwards += $userInfo['Completed']; + $masteredAwards += $userInfo['Mastered']; + } + + return compact( + 'mostBeatenSoftcoreGame', + 'mostBeatenHardcoreGame', + 'mostCompletedGame', + 'mostMasteredGame', + 'ownAwards', + 'beatenSoftcoreAwards', + 'beatenHardcoreAwards', + 'completedAwards', + 'masteredAwards', + 'userMostBeatenSoftcore', + 'userMostBeatenHardcore', + 'userMostCompleted', + 'userMostMastered', + ); + } + + private function findMost(array $record, string $key, array $currentMost): array + { + if (empty($currentMost) && (int) $record[$key] > 0) { + return $record; + } + + return isset($currentMost[$key]) && ($currentMost[$key] < (int) $record[$key]) ? $record : $currentMost; + } +} diff --git a/app/Helpers/database/user.php b/app/Helpers/database/user.php index c1ec7d8863..a419af5d96 100644 --- a/app/Helpers/database/user.php +++ b/app/Helpers/database/user.php @@ -1,5 +1,6 @@ 0) { - $anyDevMostCompletedGame = $game; - } - } else { - if ($anyDevMostCompletedGame['Completed'] < $game['Completed']) { - $anyDevMostCompletedGame = $game; - } - } - - if (empty($anyDevMostMasteredGame)) { - if ($game['Mastered'] > 0) { - $anyDevMostMasteredGame = $game; - } - } else { - if ($anyDevMostMasteredGame['Mastered'] < $game['Mastered']) { - $anyDevMostMasteredGame = $game; - } - } -} - -// Initialize any dev user award variables -$anyDevOwnAwards = []; -$anyDevCompletedAwards = 0; -$anyDevMasteredAwards = 0; -$anyDevUserMostCompleted = []; -$anyDevUserMostMastered = []; - -// Get user award data for any developed games -$anyDevAwardInfo = getMostAwardedUsers($anyDevGameIDs); -foreach ($anyDevAwardInfo as $userInfo) { - if (empty($anyDevUserMostCompleted)) { - if ($userInfo['Completed'] > 0) { - $anyDevUserMostCompleted = $userInfo; - } - } else { - if ($anyDevUserMostCompleted['Completed'] < $userInfo['Completed']) { - $anyDevUserMostCompleted = $userInfo; - } - } - - if (empty($anyDevUserMostMastered)) { - if ($userInfo['Mastered'] > 0) { - $anyDevUserMostMastered = $userInfo; - } - } else { - if ($anyDevUserMostMastered['Mastered'] < $userInfo['Mastered']) { - $anyDevUserMostMastered = $userInfo; - } - } - - if (strcmp($dev, $userInfo['User']) == 0) { - $anyDevOwnAwards = $userInfo; - } - $anyDevCompletedAwards += $userInfo['Completed']; - $anyDevMasteredAwards += $userInfo['Mastered']; -} - -// Initialize majority dev game award variables -$majorityDevMostCompletedGame = []; -$majorityDevMostMasteredGame = []; - -// Get user award data for majority developed games -$majorityDevCompletedMasteredGames = getMostAwardedGames($majorityDevGameIDs); -foreach ($majorityDevCompletedMasteredGames as $game) { - if (empty($majorityDevMostCompletedGame)) { - if ($game['Completed'] > 0) { - $majorityDevMostCompletedGame = $game; - } - } else { - if ($majorityDevMostCompletedGame['Completed'] < $game['Completed']) { - $majorityDevMostCompletedGame = $game; - } - } - - if (empty($majorityDevMostMasteredGame)) { - if ($game['Mastered'] > 0) { - $majorityDevMostMasteredGame = $game; - } - } else { - if ($majorityDevMostMasteredGame['Mastered'] < $game['Mastered']) { - $majorityDevMostMasteredGame = $game; - } - } -} - -// Initialize majority dev user award variables -$majorityDevOwnAwards = []; -$majorityDevCompletedAwards = 0; -$majorityDevMasteredAwards = 0; -$majorityDevUserMostCompleted = []; -$majorityDevUserMostMastered = []; - -// Get user award data for majority developed games -$majorityDevAwardInfo = getMostAwardedUsers($majorityDevGameIDs); -foreach ($majorityDevAwardInfo as $userInfo) { - if (empty($majorityDevUserMostCompleted)) { - if ($userInfo['Completed'] > 0) { - $majorityDevUserMostCompleted = $userInfo; - } - } else { - if ($majorityDevUserMostCompleted['Completed'] < $userInfo['Completed']) { - $majorityDevUserMostCompleted = $userInfo; - } - } - - if (empty($majorityDevUserMostMastered)) { - if ($userInfo['Mastered'] > 0) { - $majorityDevUserMostMastered = $userInfo; - } - } else { - if ($majorityDevUserMostMastered['Mastered'] < $userInfo['Mastered']) { - $majorityDevUserMostMastered = $userInfo; - } - } - - if (strcmp($dev, $userInfo['User']) == 0) { - $majorityDevOwnAwards = $userInfo; - } - $majorityDevCompletedAwards += $userInfo['Completed']; - $majorityDevMasteredAwards += $userInfo['Mastered']; -} - -// Initialize sole dev game award variables -$onlyDevMostCompletedGame = []; -$onlyDevMostMasteredGame = []; - -// Get user award data for solely developed games -$onlyDevCompletedMasteredGames = getMostAwardedGames($onlyDevGameIDs); -foreach ($onlyDevCompletedMasteredGames as $game) { - if (empty($onlyDevMostCompletedGame)) { - if ($game['Completed'] > 0) { - $onlyDevMostCompletedGame = $game; - } - } else { - if ($onlyDevMostCompletedGame['Completed'] < $game['Completed']) { - $onlyDevMostCompletedGame = $game; - } - } - - if (empty($onlyDevMostMasteredGame)) { - if ($game['Mastered'] > 0) { - $onlyDevMostMasteredGame = $game; - } - } else { - if ($onlyDevMostMasteredGame['Mastered'] < $game['Mastered']) { - $onlyDevMostMasteredGame = $game; - } - } -} - -// Initialize sole dev user award variables -$onlyDevOwnAwards = []; -$onlyDevCompletedAwards = 0; -$onlyDevMasteredAwards = 0; -$onlyDevUserMostCompleted = []; -$onlyDevUserMostMastered = []; - -// Get user award data for solely developed games -$onlyDevAwardInfo = getMostAwardedUsers($onlyDevGameIDs); -foreach ($onlyDevAwardInfo as $userInfo) { - if (empty($onlyDevUserMostCompleted)) { - if ($userInfo['Completed'] > 0) { - $onlyDevUserMostCompleted = $userInfo; - } - } else { - if ($onlyDevUserMostCompleted['Completed'] < $userInfo['Completed']) { - $onlyDevUserMostCompleted = $userInfo; - } - } - if (empty($onlyDevUserMostMastered)) { - if ($userInfo['Mastered'] > 0) { - $onlyDevUserMostMastered = $userInfo; - } - } else { - if ($onlyDevUserMostMastered['Mastered'] < $userInfo['Mastered']) { - $onlyDevUserMostMastered = $userInfo; - } - } - - if (strcmp($dev, $userInfo['User']) == 0) { - $onlyDevOwnAwards = $userInfo; - } - $onlyDevCompletedAwards += $userInfo['Completed']; - $onlyDevMasteredAwards += $userInfo['Mastered']; -} - // Initialize user achievement variables $defaultBadges = [ "00000", @@ -683,7 +490,7 @@ function drawChart() { $dev's Developer Stats"; + echo "

$dev's Developer Stats

"; // Only show stats if the user has a contribute count if ($userContribCount > 0) { @@ -698,342 +505,81 @@ function drawChart() { /* * Games */ - echo "

Games

"; - - /* - * Any Development - */ - echo ""; - echo ""; - echo ""; - - // Any Development - Games developed for - echo ""; - - // Any Development - Games with Rich Presence - echo ""; - - // Any Development - Games with Leaderboards and Leaderboard count - echo ""; - - // Any Development - Easiest game by retro ratio - echo ""; - - // Any Development - Hardest game by retro ratio - echo ""; - - // Any Development - Complete/Mastered games - echo ""; - - // Any Development - Own Complete/Mastered games - echo ""; - - // Any Development - Most completed game - echo ""; - - // Any Development - Most mastered game - echo ""; - - // Any Development - User with most completed awards - echo ""; - - // Any Development - User with most mastered awards - echo ""; - echo "
Any Development
Stats below are for games that $dev has published at least one achievement for.
Games Developed For:" . count($anyDevGameIDs) . "
Games with Rich Presence:"; - if (!empty($anyDevGameIDs)) { - echo $anyDevRichPresenceCount . " - " . number_format($anyDevRichPresenceCount / count($anyDevGameIDs) * 100, 2, '.', '') . "%"; - } else { - echo "N/A"; - } - echo "
Games with Leaderboards:"; - if (!empty($anyDevGameIDs)) { - echo $anyDevLeaderboardCount . " - " . number_format($anyDevLeaderboardCount / count($anyDevGameIDs) * 100, 2, '.', '') . "%
" . $anyDevLeaderboardTotal . " Unique Leaderboards"; - } else { - echo "N/A"; - } - echo "
Easiest Game by Retro Ratio:"; - if (!empty($anyDevEasiestGame)) { - echo number_format($anyDevEasiestGame['TotalTruePoints'] / $anyDevEasiestGame['MaxPointsAvailable'], 2, '.', '') . " - "; - echo gameAvatar($anyDevEasiestGame); - echo "
" . $anyDevEasiestGame['MyAchievements'] . " of " . $anyDevEasiestGame['NumAchievements'] . " Achievements Created"; - } else { - echo "N/A"; - } - echo "
Hardest Game by Retro Ratio"; - if (!empty($anyDevHardestGame)) { - echo number_format($anyDevHardestGame['TotalTruePoints'] / $anyDevHardestGame['MaxPointsAvailable'], 2, '.', '') . " - "; - echo gameAvatar($anyDevHardestGame); - echo "
" . $anyDevHardestGame['MyAchievements'] . " of " . $anyDevHardestGame['NumAchievements'] . " Achievements Created
"; - } else { - echo "N/A"; - } - echo "
Completed/Mastered Awards:"; - if (!empty($anyDevGameIDs)) { - echo $anyDevCompletedAwards . " (" . $anyDevMasteredAwards . ")
"; - } else { - echo "N/A"; - } - echo "
Own Completed/Mastered Awards:"; - if (!empty($anyDevOwnAwards)) { - echo $anyDevOwnAwards['Completed'] . " (" . $anyDevOwnAwards['Mastered'] . ")
"; - } else { - echo "N/A"; - } - echo "
Most Completed Game:"; - if (!empty($anyDevMostCompletedGame)) { - echo $anyDevMostCompletedGame['Completed'] . " - "; - echo gameAvatar($anyDevMostCompletedGame); - } else { - echo "N/A"; - } - echo "
Most Mastered Game:"; - if (!empty($anyDevMostMasteredGame)) { - echo $anyDevMostMasteredGame['Mastered'] . " - "; - echo gameAvatar($anyDevMostMasteredGame); - } else { - echo "N/A"; - } - echo "
User with Most Completed Awards:"; - if (!empty($anyDevUserMostCompleted)) { - echo $anyDevUserMostCompleted['Completed'] . " - "; - echo userAvatar($anyDevUserMostCompleted['User']); - } else { - echo "N/A"; - } - echo "
User with Most Mastered Awards:"; - if (!empty($anyDevUserMostMastered)) { - echo $anyDevUserMostMastered['Mastered'] . " - "; - echo userAvatar($anyDevUserMostMastered['User']); - } else { - echo "N/A"; - } - echo "
"; - echo "
"; - - /* - * Majority Developer - */ - echo ""; - echo ""; - echo ""; - - // Majority Developer - Games developed for - echo ""; - - // Majority Developer - Games with Rich Presence - echo ""; - - // Majority Developer - Games with Leaderboards and Leaderboard count - echo ""; - - // Majority Developer - Easiest game by retro ratio - echo ""; - - // Majority Developer - Hardest game by retro ratio - echo ""; - - // Majority Developer - Complete/Mastered games - echo ""; - - // Majority Developer - Own Complete/Mastered games - echo ""; - - // Majority Developer - Most completed game - echo ""; - - // Majority Developer - Most mastered game - echo ""; - - // Majority Developer - User with most completed awards - echo ""; - - // Majority Developer - User with most mastered awards - echo ""; - echo "
Majority Developer
Stats below are for games that $dev has published at least half the achievements for.
Games Developed For:" . count($majorityDevGameIDs) . "
Games with Rich Presence:"; - if (!empty($majorityDevGameIDs)) { - echo $majorityDevRichPresenceCount . " - " . number_format($majorityDevRichPresenceCount / count($majorityDevGameIDs) * 100, 2, '.', '') . "%"; - } else { - echo "N/A"; - } - echo "
Games with Leaderboards:"; - if (!empty($majorityDevGameIDs)) { - echo $majorityDevLeaderboardCount . " - " . number_format($majorityDevLeaderboardCount / count($majorityDevGameIDs) * 100, 2, '.', '') . "%
" . $majorityDevLeaderboardTotal . " Unique Leaderboards"; - } else { - echo "N/A"; - } - echo "
Easiest Game by Retro Ratio:"; - if (!empty($majorityDevEasiestGame)) { - echo number_format($majorityDevEasiestGame['TotalTruePoints'] / $majorityDevEasiestGame['MaxPointsAvailable'], 2, '.', '') . " - "; - echo gameAvatar($majorityDevEasiestGame); - echo "
" . $majorityDevEasiestGame['MyAchievements'] . " of " . $majorityDevEasiestGame['NumAchievements'] . " Achievements Created"; - } else { - echo "N/A"; - } - echo "
Hardest Game by Retro Ratio:"; - if (!empty($majorityDevHardestGame)) { - echo number_format($majorityDevHardestGame['TotalTruePoints'] / $majorityDevHardestGame['MaxPointsAvailable'], 2, '.', '') . " - "; - echo gameAvatar($majorityDevHardestGame); - echo "
" . $majorityDevHardestGame['MyAchievements'] . " of " . $majorityDevHardestGame['NumAchievements'] . " Achievements Created"; - } else { - echo "N/A"; - } - echo "
Completed/Mastered Awards:"; - if (!empty($majorityDevGameIDs)) { - echo $majorityDevCompletedAwards . " (" . $majorityDevMasteredAwards . ")
"; - } else { - echo "N/A"; - } - echo "
Own Completed/Mastered Awards:"; - if (!empty($majorityDevOwnAwards)) { - echo $majorityDevOwnAwards['Completed'] . " (" . $majorityDevOwnAwards['Mastered'] . ")
"; - } else { - echo "N/A"; - } - echo "
Most Completed Game:"; - if (!empty($majorityDevMostCompletedGame)) { - echo $majorityDevMostCompletedGame['Completed'] . " - "; - echo gameAvatar($majorityDevMostCompletedGame); - } else { - echo "N/A"; - } - echo "
Most Mastered Game:"; - if (!empty($majorityDevMostMasteredGame)) { - echo $majorityDevMostMasteredGame['Mastered'] . " - "; - echo gameAvatar($majorityDevMostMasteredGame); - } else { - echo "N/A"; - } - echo "
User with Most Completed Awards:"; - if (!empty($majorityDevUserMostCompleted)) { - echo $majorityDevUserMostCompleted['Completed'] . " - "; - echo userAvatar($majorityDevUserMostCompleted['User']); - } else { - echo "N/A"; - } - echo "
User with Most Mastered Awards:"; - if (!empty($majorityDevUserMostMastered)) { - echo $majorityDevUserMostMastered['Mastered'] . " - "; - echo userAvatar($majorityDevUserMostMastered['User']); - } else { - echo "N/A"; - } - echo "
"; - echo "
"; - - /* - * Sole Developer - */ - echo ""; - echo ""; - echo ""; - - // Sole Developer - Games developed for - echo ""; - - // Sole Developer - Games with Rich Presence - echo ""; - - // Sole Developer - Games with Leaderboards and Leaderboard count - echo ""; - - // Sole Developer - Easiest game by retro ratio - echo ""; - - // Sole Developer - Hardest game by retro ratio - echo ""; - - // Sole Developer - Complete/Mastered games - echo ""; - - // Sole Developer - Own Complete/Mastered games - echo ""; - - // Sole Developer - Most completed game - echo ""; - - // Sole Developer - Most mastered game - echo ""; + echo "

Games

"; + + // Any development + echo Blade::render(' + + ', [ + 'easiestGame' => $anyDevEasiestGame, + 'hardestGame' => $anyDevHardestGame, + 'numGamesWithLeaderboards' => $anyDevLeaderboardCount, + 'numGamesWithRichPresence' => $anyDevRichPresenceCount, + 'numTotalLeaderboards' => $anyDevLeaderboardTotal, + 'statsKind' => 'any', + 'targetDeveloperUsername' => $dev, + 'targetGameIds' => $anyDevGameIDs, + ]); - // Sole Developer - User with most completed awards - echo ""; + // Majority development + echo Blade::render(' + + ', [ + 'easiestGame' => $majorityDevEasiestGame, + 'hardestGame' => $majorityDevHardestGame, + 'numGamesWithLeaderboards' => $majorityDevLeaderboardCount, + 'numGamesWithRichPresence' => $majorityDevRichPresenceCount, + 'numTotalLeaderboards' => $majorityDevLeaderboardTotal, + 'statsKind' => 'majority', + 'targetDeveloperUsername' => $dev, + 'targetGameIds' => $majorityDevGameIDs, + ]); - // Sole Developer - User with most mastered awards - echo ""; - echo "
Sole Developer
Stats below are for games that $dev has published all the achievements for.
Games Developed For:" . count($onlyDevGameIDs) . "
Games with Rich Presence:"; - if (!empty($onlyDevGameIDs)) { - echo $onlyDevRichPresenceCount . " - " . number_format($onlyDevRichPresenceCount / count($onlyDevGameIDs) * 100, 2, '.', '') . "%"; - } else { - echo "N/A"; - } - echo "
Games with Leaderboards:"; - if (!empty($onlyDevGameIDs)) { - echo $onlyDevLeaderboardCount . " - " . number_format($onlyDevLeaderboardCount / count($onlyDevGameIDs) * 100, 2, '.', '') . "%
" . $onlyDevLeaderboardTotal . " Unique Leaderboards"; - } else { - echo "N/A"; - } - echo "
Easiest Game by Retro Ratio:"; - if (!empty($onlyDevEasiestGame)) { - echo number_format($onlyDevEasiestGame['TotalTruePoints'] / $onlyDevEasiestGame['MaxPointsAvailable'], 2, '.', '') . " - "; - echo gameAvatar($onlyDevEasiestGame); - echo "
" . $onlyDevEasiestGame['MyAchievements'] . " of " . $onlyDevEasiestGame['NumAchievements'] . " Achievements Created"; - } else { - echo "N/A"; - } - echo "
Hardest Game by Retro Ratio:"; - if (!empty($onlyDevHardestGame)) { - echo number_format($onlyDevHardestGame['TotalTruePoints'] / $onlyDevHardestGame['MaxPointsAvailable'], 2, '.', '') . " - "; - echo gameAvatar($onlyDevHardestGame); - echo "
" . $onlyDevHardestGame['MyAchievements'] . " of " . $onlyDevHardestGame['NumAchievements'] . " Achievements Created"; - } else { - echo "N/A"; - } - echo "
Completed/Mastered Awards:"; - if (!empty($onlyDevGameIDs)) { - echo $onlyDevCompletedAwards . " (" . $onlyDevMasteredAwards . ")
"; - } else { - echo "N/A"; - } - echo "
Own Completed/Mastered Awards:"; - if (!empty($onlyDevOwnAwards)) { - echo $onlyDevOwnAwards['Completed'] . " (" . $onlyDevOwnAwards['Mastered'] . ")
"; - } else { - echo "N/A"; - } - echo "
Most Completed Game:"; - if (!empty($onlyDevMostCompletedGame)) { - echo $onlyDevMostCompletedGame['Completed'] . " - "; - echo gameAvatar($onlyDevMostCompletedGame); - } else { - echo "N/A"; - } - echo "
Most Mastered Game:"; - if (!empty($onlyDevMostMasteredGame)) { - echo $onlyDevMostMasteredGame['Mastered'] . " - "; - echo gameAvatar($onlyDevMostMasteredGame); - } else { - echo "N/A"; - } - echo "
User with Most Completed Awards:"; - if (!empty($onlyDevUserMostCompleted)) { - echo $onlyDevUserMostCompleted['Completed'] . " - "; - echo userAvatar($onlyDevUserMostCompleted['User']); - } else { - echo "N/A"; - } - echo "
User with Most Mastered Awards:"; - if (!empty($onlyDevUserMostMastered)) { - echo $onlyDevUserMostMastered['Mastered'] . " - "; - echo userAvatar($onlyDevUserMostMastered['User']); - } else { - echo "N/A"; - } - echo "
"; - echo "

"; + // Sole development + echo Blade::render(' + + ', [ + 'easiestGame' => $onlyDevEasiestGame, + 'hardestGame' => $onlyDevHardestGame, + 'numGamesWithLeaderboards' => $onlyDevLeaderboardCount, + 'numGamesWithRichPresence' => $onlyDevRichPresenceCount, + 'numTotalLeaderboards' => $onlyDevLeaderboardTotal, + 'statsKind' => 'sole', + 'targetDeveloperUsername' => $dev, + 'targetGameIds' => $onlyDevGameIDs, + ]); /* * Achievements */ - echo "

Achievements

"; + echo "

Achievements

"; echo ""; // Any Development - Achievements created @@ -1196,7 +742,7 @@ function drawChart() { /* * Code Notes */ - echo "

Code Notes

"; + echo "

Code Notes

"; echo "
"; // Code notes created @@ -1227,7 +773,7 @@ function drawChart() { /* * Tickets */ - echo "

Tickets

"; + echo "

Tickets

"; echo "
"; // Total tickets created diff --git a/resources/views/community/components/developer/game-stats-table-row.blade.php b/resources/views/community/components/developer/game-stats-table-row.blade.php new file mode 100644 index 0000000000..215ed576e1 --- /dev/null +++ b/resources/views/community/components/developer/game-stats-table-row.blade.php @@ -0,0 +1,8 @@ +@props([ + 'headingLabel' => '', +]) + + + + + diff --git a/resources/views/community/components/developer/game-stats-table.blade.php b/resources/views/community/components/developer/game-stats-table.blade.php new file mode 100644 index 0000000000..0903907a47 --- /dev/null +++ b/resources/views/community/components/developer/game-stats-table.blade.php @@ -0,0 +1,240 @@ +@props([ + 'beatenHardcoreAwards', + 'beatenSoftcoreAwards', + 'completedAwards', + 'easiestGame', + 'hardestGame', + 'masteredAwards', + 'mostBeatenHardcoreGame', + 'mostBeatenSoftcoreGame', + 'mostCompletedGame', + 'mostMasteredGame', + 'numGamesWithLeaderboards', + 'numGamesWithRichPresence', + 'numTotalLeaderboards', + 'ownAwards', + 'statsKind', + 'targetDeveloperUsername', + 'targetGameIds', + 'userMostBeatenHardcore', + 'userMostBeatenSoftcore', + 'userMostCompleted', + 'userMostMastered', +]) + +
{{ $headingLabel }}{{ $slot }}
+ + + + + + + + + + + + + {{ count($targetGameIds) }} + + + + @if (empty($targetGameIds)) + N/A + @else + {{ $numGamesWithRichPresence }} + – + {{ number_format($numGamesWithRichPresence / count($targetGameIds) * 100, 2, '.', '') }}% + @endif + + + + @if (empty($targetGameIds)) + N/A + @else +
+ + {{ $numGamesWithLeaderboards }} + – + {{ number_format($numGamesWithLeaderboards / count($targetGameIds) * 100, 2, '.', '') }}% + + + {{ $numTotalLeaderboards }} Unique Leaderboards + +
+ @endif +
+ + + @if (empty($easiestGame)) + N/A + @else +
+
+ {{ number_format($easiestGame['TotalTruePoints'] / $easiestGame['MaxPointsAvailable'], 2, '.', '') }} + – + {!! gameAvatar($easiestGame) !!} +
+
+ {{ $easiestGame['MyAchievements'] }} + of + {{ $easiestGame['NumAchievements'] }} + Achievements Created +
+
+ @endif +
+ + + @if (empty($hardestGame)) + N/A + @else +
+
+ {{ number_format($hardestGame['TotalTruePoints'] / $hardestGame['MaxPointsAvailable'], 2, '.', '') }} + – + {!! gameAvatar($hardestGame) !!} +
+
+ {{ $hardestGame['MyAchievements'] }} + of + {{ $hardestGame['NumAchievements'] }} + Achievements Created +
+
+ @endif +
+ + + @if ($beatenSoftcoreAwards === 0 && $beatenHardcoreAwards === 0) + N/A + @else + {{ localized_number($beatenSoftcoreAwards) }} + ({{ localized_number($beatenHardcoreAwards) }}) + @endif + + + + @if ($completedAwards === 0 && $masteredAwards === 0) + N/A + @else + {{ localized_number($completedAwards) }} + ({{ localized_number($masteredAwards) }}) + @endif + + + + @if (($ownAwards['BeatenSoftcore'] ?? 0) === 0 && ($ownAwards['BeatenHardcore'] ?? 0) === 0) + N/A + @else + {{ $ownAwards['BeatenSoftcore'] }} + ({{ $ownAwards['BeatenHardcore'] }}) + @endif + + + + @if (($ownAwards['Completed'] ?? 0) === 0 && ($ownAwards['Mastered'] ?? 0) === 0) + N/A + @else + {{ $ownAwards['Completed'] }} + ({{ $ownAwards['Mastered'] }}) + @endif + + + + @if (($mostBeatenSoftcoreGame['BeatenSoftcore'] ?? 0) === 0) + N/A + @else + {{ localized_number($mostBeatenSoftcoreGame['BeatenSoftcore'] ?? 0) }} + – + {!! gameAvatar($mostBeatenSoftcoreGame) !!} + @endif + + + + @if (($mostBeatenHardcoreGame['BeatenHardcore'] ?? 0) === 0) + N/A + @else + {{ localized_number($mostBeatenHardcoreGame['BeatenHardcore'] ?? 0) }} + – + {!! gameAvatar($mostBeatenHardcoreGame) !!} + @endif + + + + @if (($mostCompletedGame['Completed'] ?? 0) === 0) + N/A + @else + {{ localized_number($mostCompletedGame['Completed'] ?? 0) }} + – + {!! gameAvatar($mostCompletedGame) !!} + @endif + + + + @if (($mostMasteredGame['Mastered'] ?? 0) === 0) + N/A + @else + {{ localized_number($mostMasteredGame['Mastered'] ?? 0) }} + – + {!! gameAvatar($mostMasteredGame) !!} + @endif + + + + @if (empty($userMostBeatenSoftcore)) + N/A + @else + {{ localized_number($userMostBeatenSoftcore['BeatenSoftcore']) }} + – + {!! userAvatar($userMostBeatenSoftcore['User']) !!} + @endif + + + + @if (empty($userMostBeatenHardcore)) + N/A + @else + {{ localized_number($userMostBeatenHardcore['BeatenHardcore']) }} + – + {!! userAvatar($userMostBeatenHardcore['User']) !!} + @endif + + + + @if (empty($userMostCompleted)) + N/A + @else + {{ localized_number($userMostCompleted['Completed']) }} + – + {!! userAvatar($userMostCompleted['User']) !!} + @endif + + + + @if (empty($userMostMastered)) + N/A + @else + {{ localized_number($userMostMastered['Mastered']) }} + – + {!! userAvatar($userMostMastered['User']) !!} + @endif + + +
+ @if ($statsKind === 'any') + Any Development + @elseif ($statsKind === 'majority') + Majority Developer + @elseif ($statsKind === 'sole') + Sole Developer + @endif +
+ @if ($statsKind === 'any') + Stats below are for games that {{ $targetDeveloperUsername }} has published at least one achievement for. + @elseif ($statsKind === 'majority') + Stats below are for games that {{ $targetDeveloperUsername }} has published at least half the achievements for. + @elseif ($statsKind === 'sole') + Stats below are for games that {{ $targetDeveloperUsername }} has published all the achievements for. + @endif +
From 61d208363722d6964b9afb96bc61f33457337914 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 1 Oct 2023 09:05:20 +0200 Subject: [PATCH 03/92] chore: migrations cleanup to match production schema more closely (#1898) --- .../2012_10_03_133633_create_base_tables.php | 128 +++++++++--------- .../2023_05_01_000002_add_foreign_keys.php | 2 +- 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/database/migrations/2012_10_03_133633_create_base_tables.php b/database/migrations/2012_10_03_133633_create_base_tables.php index 1db131b692..519ab7a863 100644 --- a/database/migrations/2012_10_03_133633_create_base_tables.php +++ b/database/migrations/2012_10_03_133633_create_base_tables.php @@ -40,20 +40,20 @@ public function up(): void $table->increments('ID'); $table->unsignedInteger('GameID'); $table->string('Title', 64); - $table->string('Description'); + $table->string('Description')->nullable(); $table->text('MemAddr'); $table->string('Progress')->nullable(); $table->string('ProgressMax')->nullable(); $table->string('ProgressFormat', 50)->nullable(); $table->unsignedSmallInteger('Points')->default(0); - $table->unsignedTinyInteger('Flags')->default(0); + $table->unsignedTinyInteger('Flags')->default(5); $table->string('Author', 32); $table->timestampTz('DateCreated')->nullable(); $table->timestampTz('DateModified')->nullable()->useCurrent(); $table->unsignedSmallInteger('VotesPos')->default(0); $table->unsignedSmallInteger('VotesNeg')->default(0); - $table->string('BadgeName', 8)->default('00001'); - $table->unsignedSmallInteger('DisplayOrder')->default(0); + $table->string('BadgeName', 8)->nullable()->default('00001'); + $table->smallInteger('DisplayOrder')->default(0); $table->string('AssocVideo')->nullable(); $table->unsignedInteger('TrueRatio')->default(0); @@ -80,10 +80,10 @@ public function up(): void if (!Schema::hasTable('Activity')) { Schema::create('Activity', function (Blueprint $table) { - $table->increments('ID'); + $table->increments('ID'); // NOTE not unsigned on production but table will be dropped $table->timestampTz('timestamp')->useCurrent(); $table->timestampTz('lastupdate')->nullable(); - $table->unsignedSmallInteger('activitytype'); + $table->smallInteger('activitytype'); $table->string('User', 32); $table->string('data', 20)->nullable(); $table->string('data2', 12)->nullable(); @@ -103,9 +103,9 @@ public function up(): void $table->primary(['User', 'AchievementID', 'HardcoreMode']); $table->string('User', 32); - $table->unsignedInteger('AchievementID'); + $table->integer('AchievementID'); $table->timestampTz('Date')->nullable()->useCurrent(); - $table->boolean('HardcoreMode')->default(0); + $table->unsignedTinyInteger('HardcoreMode')->default(0); $table->index('User'); $table->index('AchievementID'); @@ -185,7 +185,7 @@ public function up(): void if (!Schema::hasTable('EmailConfirmations')) { Schema::create('EmailConfirmations', function (Blueprint $table) { - $table->string('User', 32); + $table->string('User', 20); $table->string('EmailCookie', 20)->index(); $table->date('Expires'); }); @@ -196,9 +196,9 @@ public function up(): void $table->increments('ID'); $table->unsignedInteger('CategoryID')->index(); $table->string('Title', 50); - $table->string('Description'); + $table->string('Description', 250); $table->unsignedInteger('LatestCommentID')->nullable(); - $table->unsignedInteger('DisplayOrder')->default(0); + $table->integer('DisplayOrder')->default(0); }); } @@ -220,8 +220,8 @@ public function up(): void if (!Schema::hasTable('ForumCategory')) { Schema::create('ForumCategory', function (Blueprint $table) { $table->increments('ID'); - $table->string('Name'); - $table->string('Description'); + $table->string('Name', 250); + $table->string('Description', 250); $table->unsignedInteger('DisplayOrder')->default(0); }); } @@ -246,11 +246,11 @@ public function up(): void $table->increments('ID'); $table->unsignedInteger('ForumID')->index(); $table->string('Title'); - $table->string('Author', 32); + $table->string('Author', 50); $table->unsignedInteger('AuthorID'); $table->timestampTz('DateCreated')->useCurrent(); $table->unsignedInteger('LatestCommentID'); - $table->unsignedSmallInteger('RequiredPermissions')->default(0); + $table->smallInteger('RequiredPermissions')->default(0); }); } @@ -271,10 +271,10 @@ public function up(): void $table->increments('ID'); $table->unsignedInteger('ForumTopicID')->index(); $table->text('Payload'); - $table->string('Author', 32); + $table->string('Author', 50); $table->unsignedInteger('AuthorID'); - $table->timestampTz('DateCreated')->index(); - $table->timestampTz('DateModified')->useCurrent()->useCurrentOnUpdate(); + $table->timestampTz('DateCreated')->nullable()->index(); + $table->timestampTz('DateModified')->nullable()->useCurrent()->useCurrentOnUpdate(); $table->unsignedTinyInteger('Authorised')->nullable(); }); } @@ -309,8 +309,8 @@ public function up(): void if (!Schema::hasTable('GameAlternatives')) { Schema::create('GameAlternatives', function (Blueprint $table) { - $table->unsignedInteger('gameID')->index(); - $table->unsignedInteger('gameIDAlt')->index(); + $table->unsignedInteger('gameID')->nullable()->index(); + $table->unsignedInteger('gameIDAlt')->nullable()->index(); }); } @@ -334,8 +334,8 @@ public function up(): void $table->increments('ID'); $table->string('Title', 80); $table->unsignedTinyInteger('ConsoleID')->index(); - $table->unsignedInteger('ForumTopicID')->nullable(); - $table->unsignedInteger('Flags')->nullable(); + $table->integer('ForumTopicID')->nullable(); + $table->integer('Flags')->nullable(); $table->string('ImageIcon', 50)->nullable()->default('/Images/000001.png'); $table->string('ImageTitle', 50)->nullable()->default('/Images/000002.png'); $table->string('ImageIngame', 50)->nullable()->default('/Images/000002.png'); @@ -411,9 +411,9 @@ public function up(): void if (!Schema::hasTable('LeaderboardDef')) { Schema::create('LeaderboardDef', function (Blueprint $table) { $table->increments('ID'); - $table->unsignedInteger('GameID')->index(); + $table->unsignedInteger('GameID')->default(0)->index(); $table->text('Mem'); - $table->string('Format', 50); + $table->string('Format', 50)->nullable()->default(''); $table->string('Title')->default('Leaderboard Title'); $table->string('Description')->default('Leaderboard Description'); $table->boolean('LowerIsBetter')->default(0); @@ -447,8 +447,8 @@ public function up(): void if (!Schema::hasTable('LeaderboardEntry')) { Schema::create('LeaderboardEntry', function (Blueprint $table) { - $table->unsignedInteger('LeaderboardID')->index(); - $table->unsignedInteger('UserID'); + $table->unsignedInteger('LeaderboardID')->default(0)->index(); + $table->unsignedInteger('UserID')->default(0); $table->integer('Score')->default(0); $table->dateTimeTz('DateSubmitted'); @@ -493,7 +493,7 @@ public function up(): void $table->timestampTz('Timestamp')->useCurrent(); $table->string('Title')->nullable(); $table->text('Payload'); - $table->string('Author', 32); + $table->string('Author', 50)->nullable(); $table->string('Link')->nullable(); $table->string('Image')->nullable(); }); @@ -515,10 +515,10 @@ public function up(): void if (!Schema::hasTable('Rating')) { Schema::create('Rating', function (Blueprint $table) { - $table->string('User', 32); - $table->unsignedSmallInteger('RatingObjectType'); - $table->unsignedSmallInteger('RatingID'); - $table->unsignedSmallInteger('RatingValue'); + $table->string('User'); + $table->smallInteger('RatingObjectType'); + $table->smallInteger('RatingID'); + $table->smallInteger('RatingValue'); if (DB::connection()->getDriverName() === 'sqlite') { // SQLite does not allow changing a primary key after a table has been created so it has to be done here @@ -551,13 +551,13 @@ public function up(): void $table->increments('ID'); $table->string('User', 32); $table->unsignedInteger('GameID'); - $table->unsignedInteger('ClaimType'); - $table->unsignedInteger('SetType'); - $table->unsignedInteger('Status'); - $table->unsignedInteger('Extension'); - $table->unsignedInteger('Special'); + $table->unsignedInteger('ClaimType')->comment('0 - Primary (counts against claim total), 1 - Collaboration (does not count against claim total)'); + $table->unsignedInteger('SetType')->comment('0 - New set, 1 - Revision'); + $table->unsignedInteger('Status')->comment('0 - Active, 1 - Complete, 2 - Dropped'); + $table->unsignedInteger('Extension')->comment('Number of times the claim has been extended'); + $table->unsignedInteger('Special')->comment('0 - Standard claim, 1 - Own Revision, 2 - Free Rollout claim, 3 - Future release approved. >=1 does not count against claim count'); $table->timestampTz('Created')->useCurrent(); - $table->timestampTz('Finished')->useCurrent(); + $table->timestampTz('Finished')->useCurrent()->comment('Timestamp for when the claim is completed, dropped or will expire'); $table->timestampTz('Updated')->useCurrent(); }); } @@ -566,7 +566,7 @@ public function up(): void Schema::create('SetRequest', function (Blueprint $table) { $table->string('User', 32); $table->unsignedInteger('GameID'); - $table->timestampTz('Updated')->useCurrent(); + $table->timestampTz('Updated')->useCurrent()->nullable(); if (DB::connection()->getDriverName() === 'sqlite') { // SQLite does not allow changing a primary key after a table has been created so it has to be done here @@ -580,10 +580,10 @@ public function up(): void if (!Schema::hasTable('SiteAwards')) { Schema::create('SiteAwards', function (Blueprint $table) { $table->dateTimeTz('AwardDate'); - $table->string('User', 32)->index(); - $table->unsignedInteger('AwardType')->index(); - $table->unsignedInteger('AwardData')->nullable(); - $table->unsignedInteger('AwardDataExtra')->nullable(); + $table->string('User', 50)->index(); + $table->integer('AwardType')->index(); + $table->integer('AwardData')->nullable(); + $table->integer('AwardDataExtra')->default(0); $table->unique(['User', 'AwardData', 'AwardType', 'AwardDataExtra']); }); @@ -592,7 +592,7 @@ public function up(): void // https://github.com/RetroAchievements/RAWeb/blob/master/database/20190702_233400_Add_DisplayOrder_to_SiteAwards.sql if (!Schema::hasColumn('SiteAwards', 'DisplayOrder')) { Schema::table('SiteAwards', function (Blueprint $table) { - $table->unsignedSmallInteger('DisplayOrder')->default(0)->after('AwardDataExtra')->comment('Display order to show site awards in'); + $table->smallInteger('DisplayOrder')->default(0)->after('AwardDataExtra')->comment('Display order to show site awards in'); }); } @@ -604,9 +604,9 @@ public function up(): void $table->unsignedInteger('NumRegisteredUsers'); $table->unsignedInteger('TotalPointsEarned'); $table->unsignedInteger('LastAchievementEarnedID'); - $table->string('LastAchievementEarnedByUser', 32); + $table->string('LastAchievementEarnedByUser', 50); $table->timestampTz('LastAchievementEarnedAt')->useCurrent()->useCurrentOnUpdate(); - $table->string('LastRegisteredUser', 32); + $table->string('LastRegisteredUser', 50); $table->timestampTz('LastRegisteredUserAt')->nullable(); $table->unsignedInteger('LastUpdatedGameID'); $table->unsignedInteger('LastUpdatedAchievementID'); @@ -638,7 +638,7 @@ public function up(): void ]); $table->unsignedInteger('SubjectID'); $table->unsignedInteger('UserID'); - $table->boolean('State'); + $table->unsignedTinyInteger('State')->comment('Whether UserID is subscribed (1) or unsubscribed (0)'); $table->primary(['SubjectType', 'SubjectID', 'UserID']); }); @@ -668,7 +668,7 @@ public function up(): void $table->timestampTz('ReportedAt')->nullable()->index(); $table->timestampTz('ResolvedAt')->nullable(); $table->unsignedInteger('ResolvedByUserID')->nullable(); - $table->unsignedTinyInteger('ReportState')->default(1); + $table->unsignedTinyInteger('ReportState')->default(1)->comment('1=submitted,2=resolved,3=declined'); $table->unique(['AchievementID', 'ReportedByUserID']); }); @@ -695,34 +695,34 @@ public function up(): void if (!Schema::hasTable('UserAccounts')) { Schema::create('UserAccounts', function (Blueprint $table) { - $table->increments('ID'); + $table->increments('ID'); // NOTE PRIMARY KEY ('ID', 'User') on production $table->string('User', 32)->unique(); $table->string('SaltedPass', 32); $table->string('EmailAddress', 64); - $table->tinyInteger('Permissions')->comment('-2=spam, -1=ban, 0=unconfirmed, 1=confirmed, 2=jr-dev, 3=dev, 4=admin'); - $table->unsignedInteger('RAPoints'); - $table->unsignedBigInteger('fbUser'); - $table->unsignedSmallInteger('fbPrefs')->nullable(); + $table->tinyInteger('Permissions')->comment('-2=spam, -1=banned, 0=unconfirmed, 1=confirmed, 2=jr-developer, 3=developer, 4=moderator'); + $table->integer('RAPoints'); + $table->bigInteger('fbUser'); + $table->smallInteger('fbPrefs')->nullable(); $table->string('cookie', 100)->nullable(); $table->string('appToken', 60)->nullable(); $table->dateTimeTz('appTokenExpiry')->nullable(); - $table->unsignedSmallInteger('websitePrefs')->default(0); + $table->unsignedSmallInteger('websitePrefs')->nullable()->default(0); $table->timestampTz('LastLogin')->nullable(); - $table->unsignedInteger('LastActivityID')->nullable(); - $table->string('Motto', 50)->nullable(); - $table->unsignedInteger('ContribCount')->nullable()->comment('The Number of awarded achievements that this user was the author of'); - $table->unsignedInteger('ContribYield')->nullable()->comment('The total points allocated for achievements that this user has been the author of'); + $table->unsignedInteger('LastActivityID')->default(0); + $table->string('Motto', 50)->default(''); + $table->unsignedInteger('ContribCount')->default(0)->comment('The Number of awarded achievements that this user was the author of'); + $table->unsignedInteger('ContribYield')->default(0)->comment('The total points allocated for achievements that this user has been the author of'); $table->string('APIKey', 60)->nullable(); - $table->unsignedInteger('APIUses')->nullable(); - $table->unsignedInteger('LastGameID')->nullable(); + $table->unsignedInteger('APIUses')->default(0); + $table->unsignedInteger('LastGameID')->default(0); $table->string('RichPresenceMsg', 100)->nullable(); $table->dateTimeTz('RichPresenceMsgDate')->nullable(); - $table->boolean('ManuallyVerified')->default(0)->comment('If 0, cannot post directly to forums without manual permission'); + $table->unsignedTinyInteger('ManuallyVerified')->nullable()->default(0)->comment('If 0, cannot post directly to forums without manual permission'); $table->unsignedInteger('UnreadMessageCount')->nullable(); $table->unsignedInteger('TrueRAPoints')->nullable(); $table->boolean('UserWallActive')->default(1)->comment('Allow Posting to user wall'); $table->string('PasswordResetToken', 32)->nullable(); - $table->boolean('Untracked'); + $table->boolean('Untracked')->default(0); $table->string('email_backup')->nullable(); $table->index(['User', 'Untracked']); @@ -774,7 +774,7 @@ public function up(): void // https://github.com/RetroAchievements/RAWeb/blob/master/database/20220615_000000_Add_UserAccount_SoftcorePoints.sql if (!Schema::hasColumns('UserAccounts', ['RASoftcorePoints'])) { Schema::table('UserAccounts', function (Blueprint $table) { - $table->unsignedInteger('RASoftcorePoints')->default(0)->after('RAPoints'); + $table->integer('RASoftcorePoints')->nullable()->default(0)->after('RAPoints'); // https://github.com/RetroAchievements/RAWeb/blob/master/database/20220810_000000_Add_UserAccount_SoftcorePoints_Key.sql $table->index(['RASoftcorePoints', 'Untracked']); @@ -783,9 +783,9 @@ public function up(): void if (!Schema::hasTable('Votes')) { Schema::create('Votes', function (Blueprint $table) { - $table->string('User', 32); + $table->string('User', 50); $table->unsignedInteger('AchievementID'); - $table->unsignedTinyInteger('Vote'); + $table->tinyInteger('Vote'); if (DB::connection()->getDriverName() === 'sqlite') { // SQLite does not allow changing a primary key after a table has been created so it has to be done here diff --git a/database/migrations/upcoming/2023_05_01_000002_add_foreign_keys.php b/database/migrations/upcoming/2023_05_01_000002_add_foreign_keys.php index 77bc1b7709..4a14a5ed80 100644 --- a/database/migrations/upcoming/2023_05_01_000002_add_foreign_keys.php +++ b/database/migrations/upcoming/2023_05_01_000002_add_foreign_keys.php @@ -16,7 +16,7 @@ public function up(): void Schema::table('CodeNotes', function (Blueprint $table) { // TODO clean up failing relations - $table->foreign('AuthorID', 'memory_notes_user_id_foreign')->references('ID')->on('UserAccounts')->onDelete('cascade'); + $table->foreign('AuthorID', 'memory_notes_user_id_foreign')->references('ID')->on('UserAccounts')->onDelete('set null'); }); Schema::table('Comment', function (Blueprint $table) { From 4555743554cad6b3104cacc474f102666154f0cb Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 1 Oct 2023 09:07:55 +0200 Subject: [PATCH 04/92] feat: add primary key to SiteAwards table (#1899) --- app/Platform/Models/PlayerBadge.php | 4 --- .../2012_10_03_133633_create_base_tables.php | 5 ++++ ..._09_29_000000_update_site_awards_table.php | 26 +++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 database/migrations/platform/2023_09_29_000000_update_site_awards_table.php diff --git a/app/Platform/Models/PlayerBadge.php b/app/Platform/Models/PlayerBadge.php index f63f4b1a2f..262cf93773 100644 --- a/app/Platform/Models/PlayerBadge.php +++ b/app/Platform/Models/PlayerBadge.php @@ -18,10 +18,6 @@ class PlayerBadge extends BaseModel // TODO Note: will be renamed and split into Community/UserBadge and Platform/PlayerBadge protected $table = 'SiteAwards'; - // TODO introduce a primary key - or do the split as mentioned above - protected $primaryKey; - public $incrementing = false; - public const CREATED_AT = 'AwardDate'; public const UPDATED_AT = null; diff --git a/database/migrations/2012_10_03_133633_create_base_tables.php b/database/migrations/2012_10_03_133633_create_base_tables.php index 519ab7a863..6db00d2802 100644 --- a/database/migrations/2012_10_03_133633_create_base_tables.php +++ b/database/migrations/2012_10_03_133633_create_base_tables.php @@ -579,6 +579,11 @@ public function up(): void if (!Schema::hasTable('SiteAwards')) { Schema::create('SiteAwards', function (Blueprint $table) { + if (DB::connection()->getDriverName() === 'sqlite') { + // SQLite does not allow changing a primary key after a table has been created so it has to be done here + $table->bigIncrements('id')->first(); + } + $table->dateTimeTz('AwardDate'); $table->string('User', 50)->index(); $table->integer('AwardType')->index(); diff --git a/database/migrations/platform/2023_09_29_000000_update_site_awards_table.php b/database/migrations/platform/2023_09_29_000000_update_site_awards_table.php new file mode 100644 index 0000000000..3cb4bc8f38 --- /dev/null +++ b/database/migrations/platform/2023_09_29_000000_update_site_awards_table.php @@ -0,0 +1,26 @@ +getDriverName() !== 'sqlite') { + $table->bigIncrements('id')->first(); + } + }); + } + + public function down(): void + { + Schema::table('SiteAwards', function (Blueprint $table) { + $table->dropColumn('id'); + }); + } +}; From db7bc5a38192f2d343ba2579c8803d5cec3e30f5 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Sun, 1 Oct 2023 09:31:03 -0400 Subject: [PATCH 05/92] fix(game): show beaten progress on an optimistic basis (#1900) --- public/gameInfo.php | 49 +++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/public/gameInfo.php b/public/gameInfo.php index 1eacc8391f..cdea04fe3c 100644 --- a/public/gameInfo.php +++ b/public/gameInfo.php @@ -134,6 +134,8 @@ $isGameBeatable = false; $isBeatenHardcore = false; $isBeatenSoftcore = false; +$hasBeatenHardcoreAward = false; +$hasBeatenSoftcoreAward = false; $userGameProgressionAwards = [ 'beaten-softcore' => null, 'beaten-hardcore' => null, @@ -157,8 +159,8 @@ // Determine if the logged in user has any progression awards for this set $userGameProgressionAwards = getUserGameProgressionAwards($gameID, $user); - $isBeatenHardcore = !is_null($userGameProgressionAwards['beaten-hardcore']); - $isBeatenSoftcore = !is_null($userGameProgressionAwards['beaten-softcore']); + $hasBeatenSoftcoreAward = !is_null($userGameProgressionAwards['beaten-hardcore']); + $hasBeatenHardcoreAward = !is_null($userGameProgressionAwards['beaten-softcore']); } $screenshotWidth = 200; @@ -171,11 +173,13 @@ $numEarnedHardcore = 0; $totalPossible = 0; - // Quickly calculate if the player potentially has an unawarded beaten game award + // Quickly calculate the player's beaten status on an optimistic basis $totalProgressionAchievements = 0; $totalWinConditionAchievements = 0; $totalEarnedProgression = 0; + $totalEarnedProgressionHardcore = 0; $totalEarnedWinCondition = 0; + $totalEarnedWinConditionHardcore = 0; $totalEarnedTrueRatio = 0; $totalPossibleTrueRatio = 0; @@ -209,14 +213,20 @@ if ($nextAch['type'] == AchievementType::Progression) { $totalProgressionAchievements++; - if (isset($nextAch['DateEarned']) || isset($nextAch['DateEarnedHardcore'])) { + if (isset($nextAch['DateEarned'])) { $totalEarnedProgression++; } + if (isset($nextAch['DateEarnedHardcore'])) { + $totalEarnedProgressionHardcore++; + } } elseif ($nextAch['type'] == AchievementType::WinCondition) { $totalWinConditionAchievements++; - if (isset($nextAch['DateEarned']) || isset($nextAch['DateEarnedHardcore'])) { + if (isset($nextAch['DateEarned'])) { $totalEarnedWinCondition++; } + if (isset($nextAch['DateEarnedHardcore'])) { + $totalEarnedWinConditionHardcore++; + } } } @@ -234,20 +244,29 @@ array_multisort($authorCount, SORT_DESC, $authorInfo); } - // If the game is beatable, the user has met the requirements to receive the - // beaten game award, and they do not currently have that award, give it to them. + // Show the beaten award display in the progress component optimistically. + // The actual award metadata is updated async via actions/background jobs. if ($isGameBeatable) { $neededProgressions = $totalProgressionAchievements > 0 ? $totalProgressionAchievements : 0; $neededWinConditions = $totalWinConditionAchievements > 0 ? 1 : 0; - if ( - $totalEarnedProgression === $neededProgressions + + $isBeatenSoftcore = ( + $totalEarnedProgression === $totalProgressionAchievements && $totalEarnedWinCondition >= $neededWinConditions - && !$isBeatenHardcore - && !$isBeatenSoftcore - ) { - $beatenGameRetVal = testBeatenGame($gameID, $user, true); - $isBeatenHardcore = $beatenGameRetVal['isBeatenHardcore']; - $isBeatenSoftcore = $beatenGameRetVal['isBeatenSoftcore']; + ); + + $isBeatenHardcore = ( + $totalEarnedProgressionHardcore === $totalProgressionAchievements + && $totalEarnedWinConditionHardcore >= $neededWinConditions + ); + + // TODO: Remove this side effect when switching to aggregate queries. + // Without aggregate queries, the side effect is part of the beaten games + // self-healing mechanism. + if (!config('feature.aggregate_queries')) { + if ($isBeatenSoftcore !== $hasBeatenSoftcoreAward || $isBeatenHardcore !== $hasBeatenHardcoreAward) { + $beatenGameRetVal = testBeatenGame($gameID, $user, true); + } } } From 16dc5c119a1e50b1ef60514c1825c9aa60d041dc Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Sun, 1 Oct 2023 13:17:19 -0400 Subject: [PATCH 06/92] feat(contact): add new section (#1895) --- resources/views/community/contact.blade.php | 38 +++++++++++++++------ 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/resources/views/community/contact.blade.php b/resources/views/community/contact.blade.php index 12ab260bf7..a4b137d984 100644 --- a/resources/views/community/contact.blade.php +++ b/resources/views/community/contact.blade.php @@ -46,9 +46,37 @@
  • Getting involved in a QA sub-team.
  • + + +

    DevQuest

    + +

    + Send a message to DevQuest for + submissions, questions, ideas, or reporting issues related to + DevQuest. +

    +
    + +

    Cheating Reports

    + +

    + Send a message to RACheats + if you believe someone is in violation of our + Global Leaderboard and Achievement Hunting Rules. +

    + +

    + Please include as much evidence as possible to support your claim. This could + include screenshots, videos, links to suspicious profiles, or any other relevant + information that demonstrates the alleged violation. Describe each piece of evidence in + detail, explaining why it suggests a violation of the rules. The more comprehensive and clear + your submission, the more efficiently we can evaluate and address the issue. +

    +
    +

    RANews

    @@ -79,16 +107,6 @@ The Unwanted.

    - - -

    DevQuest

    - -

    - Send a message to DevQuest for - submissions, questions, ideas, or reporting issues related to - DevQuest. -

    -
    From 66c7e40faf027603246a44dc9a005d4d13b2608d Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Sun, 1 Oct 2023 17:01:06 -0400 Subject: [PATCH 07/92] fix(achievementinspector): show checkmarks for jr dev collabs (#1888) --- public/achievementinspector.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/public/achievementinspector.php b/public/achievementinspector.php index bbb6a04891..92e2906b73 100644 --- a/public/achievementinspector.php +++ b/public/achievementinspector.php @@ -1,6 +1,5 @@ Date: Thu, 5 Oct 2023 16:01:07 +0200 Subject: [PATCH 08/92] feat: async double write and metrics aggregation (#1874) --- app/Community/Enums/AwardType.php | 2 + app/Community/Enums/UserActivityType.php | 6 +- app/Community/EventServiceProvider.php | 18 +- app/Community/Listeners/WriteUserActivity.php | 40 +-- app/Connect/Concerns/AchievementRequests.php | 35 +- app/Connect/Concerns/AuthRequests.php | 2 +- app/Connect/Concerns/DevelopmentRequests.php | 6 +- app/Connect/Concerns/GameRequests.php | 8 +- app/Connect/Concerns/HeartbeatRequests.php | 38 +-- app/Console/Kernel.php | 2 - app/Helpers/database/achievement-creator.php | 60 ---- app/Helpers/database/achievement.php | 46 +-- app/Helpers/database/player-achievement.php | 74 +--- app/Helpers/database/player-game.php | 247 +++++--------- app/Helpers/database/player-rank.php | 39 +-- app/Helpers/database/site-award.php | 42 +-- app/Helpers/database/static.php | 24 ++ app/Helpers/database/user-activity.php | 29 +- ...ageToGameAction.php => AddImageToGame.php} | 2 +- .../{AddTriggerAction.php => AddTrigger.php} | 2 +- app/Platform/Actions/AttachPlayerGame.php | 18 +- ...ashToGameAction.php => LinkHashToGame.php} | 7 +- ...tion.php => LinkLatestEmulatorRelease.php} | 2 +- ...n.php => LinkLatestIntegrationRelease.php} | 2 +- app/Platform/Actions/ResetPlayerProgress.php | 103 ++++++ .../Actions/ResetPlayerProgressAction.php | 95 ------ app/Platform/Actions/ResumePlayerSession.php | 81 +++++ .../Actions/ResumePlayerSessionAction.php | 73 ---- ...validateAchievementSetBadgeEligibility.php | 180 ++++++++++ .../Actions/UnlockPlayerAchievement.php | 85 +++++ .../Actions/UnlockPlayerAchievementAction.php | 129 ------- .../UpdateDeveloperContributionYield.php | 171 ++++++++++ .../Actions/UpdateGameAchievementsMetrics.php | 60 ++++ app/Platform/Actions/UpdateGameMetrics.php | 114 +++++++ .../Actions/UpdateGameMetricsAction.php | 50 --- .../Actions/UpdateGameWeightedPoints.php | 65 ---- .../Actions/UpdatePlayerGameMetrics.php | 250 ++++++++++++++ .../Actions/UpdatePlayerGameMetricsAction.php | 114 ------- app/Platform/Actions/UpdatePlayerMetrics.php | 26 ++ ...ionAction.php => UpsertTriggerVersion.php} | 2 +- app/Platform/AppServiceProvider.php | 49 +-- .../DeleteOrphanedLeaderboardEntries.php | 2 +- app/Platform/Commands/NoIntroImport.php | 2 +- app/Platform/Commands/SyncGameHashes.php | 7 +- app/Platform/Commands/SyncGames.php | 8 +- app/Platform/Commands/SyncPlayerGames.php | 6 - .../Commands/UnlockPlayerAchievement.php | 37 +- .../Commands/UpdateAllAchievementsMetrics.php | 22 -- .../Commands/UpdateAllGamesMetrics.php | 34 -- .../Commands/UpdateAllPlayerGamesMetrics.php | 22 -- .../Commands/UpdateAwardsStaticData.php | 8 +- .../UpdateDeveloperContributionYield.php | 113 +------ .../UpdateGameAchievementsMetrics.php | 40 +++ app/Platform/Commands/UpdateGameMetrics.php | 28 +- .../Commands/UpdateGameWeightedPoints.php | 45 --- .../Commands/UpdatePlayerGameMetrics.php | 64 ++-- .../Commands/UpdatePlayerMasteries.php | 137 -------- app/Platform/Commands/UpdatePlayerMetrics.php | 15 +- app/Platform/Commands/UpdatePlayerPoints.php | 53 --- app/Platform/Concerns/ActsAsPlayer.php | 14 +- .../AchievementPlayerController.php | 11 +- .../Controllers/EmulatorReleaseController.php | 10 +- app/Platform/Controllers/GameController.php | 2 - .../IntegrationReleaseController.php | 10 +- app/Platform/EventServiceProvider.php | 79 ++++- .../Events/AchievementPointsChanged.php | 28 ++ app/Platform/Events/AchievementSetBeaten.php | 31 ++ .../Events/AchievementSetCompleted.php | 6 +- .../Events/AchievementTypeChanged.php | 28 ++ .../Events/AchievementUnpublished.php | 28 ++ .../DeveloperContributionYieldUpdated.php | 28 ++ ...mentUpdated.php => GameMetricsUpdated.php} | 8 +- .../Events/PlayerAchievementLocked.php | 30 ++ .../Events/PlayerAchievementUnlocked.php | 5 +- app/Platform/Events/PlayerBadgeAwarded.php | 28 ++ app/Platform/Events/PlayerBadgeLost.php | 28 ++ app/Platform/Events/PlayerGameBeaten.php | 34 ++ app/Platform/Events/PlayerGameCompleted.php | 34 ++ .../Events/PlayerGameMetricsUpdated.php | 30 ++ app/Platform/Events/PlayerGameRemoved.php | 30 ++ .../Events/PlayerRankedStatusChanged.php | 29 ++ .../Events/PlayerSessionHeartbeat.php | 7 +- app/Platform/Events/SiteBadgeAwarded.php | 28 ++ .../Jobs/UnlockPlayerAchievementJob.php | 42 +++ .../UpdateDeveloperContributionYieldJob.php | 31 ++ .../Jobs/UpdateGameAchievementsMetricsJob.php | 31 ++ app/Platform/Jobs/UpdateGameMetricsJob.php | 31 ++ .../Jobs/UpdatePlayerGameMetricsJob.php | 36 ++ app/Platform/Jobs/UpdatePlayerMetricsJob.php | 31 ++ ...tchUpdateDeveloperContributionYieldJob.php | 48 +++ .../DispatchUpdateGameMetricsJob.php | 50 +++ .../DispatchUpdatePlayerGameMetricsJob.php | 48 +++ .../DispatchUpdatePlayerMetricsJob.php | 29 ++ .../Listeners/ResetPlayerProgress.php | 6 +- .../Listeners/ResumePlayerSession.php | 53 +++ app/Platform/Models/Achievement.php | 73 +++- app/Platform/Models/Game.php | 27 +- app/Platform/Models/PlayerAchievement.php | 2 +- app/Platform/Models/PlayerBadge.php | 2 + app/Platform/Models/PlayerGame.php | 8 +- app/Site/EventServiceProvider.php | 8 + app/Site/Models/StaticData.php | 3 +- app/Site/Models/User.php | 36 +- app/Site/Responses/LoginResponse.php | 3 +- config/horizon.php | 11 +- ...10_03_000001_create_player_games_table.php | 6 +- ...1_create_player_achievement_sets_table.php | 6 +- .../2023_09_01_154331_update_games_table.php | 59 ++++ ...e_player_games_player_achievement_sets.php | 86 +++++ public/admin.php | 20 +- public/controlpanel.php | 9 - public/dorequest.php | 92 +++-- public/gameInfo.php | 10 +- public/request/auth/register.php | 2 +- .../request/game/recalculate-points-ratio.php | 22 -- public/request/user/recalculate-score.php | 23 -- public/request/user/reset-achievements.php | 4 +- public/userInfo.php | 8 - .../progression-status/recalc-cta.blade.php | 31 -- .../user/progression-status/root.blade.php | 6 - .../views/components/menu/account.blade.php | 12 +- tests/Feature/Api/V1/GameExtendedTest.php | 3 - .../Api/V1/GameInfoAndUserProgressTest.php | 3 - .../Feature/Connect/AwardAchievementTest.php | 82 +++-- tests/Feature/Connect/PingTest.php | 19 ++ tests/Feature/Connect/StartSessionTest.php | 26 ++ tests/Feature/Connect/UnlocksTest.php | 3 +- .../Action/ResetPlayerProgressActionTest.php | 316 +++++++++--------- tests/Feature/Platform/BeatenGameTest.php | 179 ++++------ .../Components/AchievementsListTest.php | 15 +- .../Concerns/TestsPlayerAchievements.php | 24 +- .../Platform/Concerns/TestsPlayerBadges.php | 6 - tests/TestCase.php | 4 +- 133 files changed, 3205 insertions(+), 2087 deletions(-) rename app/Platform/Actions/{AddImageToGameAction.php => AddImageToGame.php} (98%) rename app/Platform/Actions/{AddTriggerAction.php => AddTrigger.php} (91%) rename app/Platform/Actions/{LinkHashToGameAction.php => LinkHashToGame.php} (92%) rename app/Platform/Actions/{LinkLatestEmulatorReleaseAction.php => LinkLatestEmulatorRelease.php} (91%) rename app/Platform/Actions/{LinkLatestIntegrationReleaseAction.php => LinkLatestIntegrationRelease.php} (98%) create mode 100644 app/Platform/Actions/ResetPlayerProgress.php delete mode 100644 app/Platform/Actions/ResetPlayerProgressAction.php create mode 100644 app/Platform/Actions/ResumePlayerSession.php delete mode 100644 app/Platform/Actions/ResumePlayerSessionAction.php create mode 100644 app/Platform/Actions/RevalidateAchievementSetBadgeEligibility.php create mode 100644 app/Platform/Actions/UnlockPlayerAchievement.php delete mode 100644 app/Platform/Actions/UnlockPlayerAchievementAction.php create mode 100644 app/Platform/Actions/UpdateDeveloperContributionYield.php create mode 100644 app/Platform/Actions/UpdateGameAchievementsMetrics.php create mode 100644 app/Platform/Actions/UpdateGameMetrics.php delete mode 100644 app/Platform/Actions/UpdateGameMetricsAction.php delete mode 100644 app/Platform/Actions/UpdateGameWeightedPoints.php create mode 100644 app/Platform/Actions/UpdatePlayerGameMetrics.php delete mode 100644 app/Platform/Actions/UpdatePlayerGameMetricsAction.php create mode 100644 app/Platform/Actions/UpdatePlayerMetrics.php rename app/Platform/Actions/{UpsertTriggerVersionAction.php => UpsertTriggerVersion.php} (98%) delete mode 100644 app/Platform/Commands/UpdateAllAchievementsMetrics.php delete mode 100644 app/Platform/Commands/UpdateAllGamesMetrics.php delete mode 100644 app/Platform/Commands/UpdateAllPlayerGamesMetrics.php create mode 100644 app/Platform/Commands/UpdateGameAchievementsMetrics.php delete mode 100644 app/Platform/Commands/UpdateGameWeightedPoints.php delete mode 100644 app/Platform/Commands/UpdatePlayerMasteries.php delete mode 100644 app/Platform/Commands/UpdatePlayerPoints.php create mode 100644 app/Platform/Events/AchievementPointsChanged.php create mode 100644 app/Platform/Events/AchievementSetBeaten.php create mode 100644 app/Platform/Events/AchievementTypeChanged.php create mode 100644 app/Platform/Events/AchievementUnpublished.php create mode 100644 app/Platform/Events/DeveloperContributionYieldUpdated.php rename app/Platform/Events/{AchievementUpdated.php => GameMetricsUpdated.php} (78%) create mode 100644 app/Platform/Events/PlayerAchievementLocked.php create mode 100644 app/Platform/Events/PlayerBadgeAwarded.php create mode 100644 app/Platform/Events/PlayerBadgeLost.php create mode 100644 app/Platform/Events/PlayerGameBeaten.php create mode 100644 app/Platform/Events/PlayerGameCompleted.php create mode 100644 app/Platform/Events/PlayerGameMetricsUpdated.php create mode 100644 app/Platform/Events/PlayerGameRemoved.php create mode 100644 app/Platform/Events/PlayerRankedStatusChanged.php create mode 100644 app/Platform/Events/SiteBadgeAwarded.php create mode 100644 app/Platform/Jobs/UnlockPlayerAchievementJob.php create mode 100644 app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php create mode 100644 app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php create mode 100644 app/Platform/Jobs/UpdateGameMetricsJob.php create mode 100644 app/Platform/Jobs/UpdatePlayerGameMetricsJob.php create mode 100644 app/Platform/Jobs/UpdatePlayerMetricsJob.php create mode 100644 app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php create mode 100644 app/Platform/Listeners/DispatchUpdateGameMetricsJob.php create mode 100644 app/Platform/Listeners/DispatchUpdatePlayerGameMetricsJob.php create mode 100644 app/Platform/Listeners/DispatchUpdatePlayerMetricsJob.php create mode 100644 app/Platform/Listeners/ResumePlayerSession.php create mode 100644 database/migrations/platform/2023_09_01_154331_update_games_table.php create mode 100644 database/migrations/platform/2023_09_15_000000_update_player_games_player_achievement_sets.php delete mode 100644 public/request/game/recalculate-points-ratio.php delete mode 100644 public/request/user/recalculate-score.php delete mode 100644 resources/views/community/components/user/progression-status/recalc-cta.blade.php diff --git a/app/Community/Enums/AwardType.php b/app/Community/Enums/AwardType.php index 569714ef9b..575df67be7 100644 --- a/app/Community/Enums/AwardType.php +++ b/app/Community/Enums/AwardType.php @@ -6,6 +6,7 @@ abstract class AwardType { + // TODO refactor to AchievementSetCompleted public const Mastery = 1; public const AchievementUnlocksYield = 2; @@ -20,6 +21,7 @@ abstract class AwardType public const CertifiedLegend = 7; + // TODO refactor to AchievementSetBeaten public const GameBeaten = 8; public static function cases(): array diff --git a/app/Community/Enums/UserActivityType.php b/app/Community/Enums/UserActivityType.php index 43a3c22744..909a19757e 100644 --- a/app/Community/Enums/UserActivityType.php +++ b/app/Community/Enums/UserActivityType.php @@ -16,7 +16,8 @@ abstract class UserActivityType public const EditAchievement = 'achievement.update'; - public const CompleteGame = 'achievement-set.complete'; + public const CompleteAchievementSet = 'achievement-set.complete'; + public const BeatAchievementSet = 'achievement-set.beat'; public const NewLeaderboardEntry = 'leaderboard-entry.create'; @@ -34,7 +35,8 @@ public static function cases(): array self::StartedPlaying, self::UploadAchievement, self::EditAchievement, - self::CompleteGame, + self::CompleteAchievementSet, + self::BeatAchievementSet, self::NewLeaderboardEntry, self::ImprovedLeaderboardEntry, self::OpenedTicket, diff --git a/app/Community/EventServiceProvider.php b/app/Community/EventServiceProvider.php index ee04e21c4e..84acef0f42 100755 --- a/app/Community/EventServiceProvider.php +++ b/app/Community/EventServiceProvider.php @@ -5,16 +5,12 @@ namespace App\Community; use App\Community\Listeners\WriteUserActivity; -use App\Platform\Events\AchievementCreated; -use App\Platform\Events\AchievementPublished; +use App\Platform\Events\AchievementSetBeaten; use App\Platform\Events\AchievementSetCompleted; -use App\Platform\Events\AchievementUpdated; use App\Platform\Events\LeaderboardEntryCreated; use App\Platform\Events\LeaderboardEntryUpdated; use App\Platform\Events\PlayerAchievementUnlocked; use App\Platform\Events\PlayerGameAttached; -use App\Platform\Events\PlayerSessionResumed; -use App\Platform\Events\PlayerSessionStarted; use Illuminate\Auth\Events\Login; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; @@ -31,16 +27,10 @@ class EventServiceProvider extends ServiceProvider /* * Platform Events - Account Listeners */ - AchievementCreated::class => [ - WriteUserActivity::class, - ], - AchievementPublished::class => [ - WriteUserActivity::class, - ], AchievementSetCompleted::class => [ WriteUserActivity::class, ], - AchievementUpdated::class => [ + AchievementSetBeaten::class => [ WriteUserActivity::class, ], LeaderboardEntryCreated::class => [ @@ -55,10 +45,6 @@ class EventServiceProvider extends ServiceProvider PlayerGameAttached::class => [ WriteUserActivity::class, ], - PlayerSessionStarted::class => [ - ], - PlayerSessionResumed::class => [ - ], ]; public function boot(): void diff --git a/app/Community/Listeners/WriteUserActivity.php b/app/Community/Listeners/WriteUserActivity.php index 0180ad29f4..a4034c7c86 100644 --- a/app/Community/Listeners/WriteUserActivity.php +++ b/app/Community/Listeners/WriteUserActivity.php @@ -8,9 +8,8 @@ use App\Community\Enums\UserActivityType; use App\Community\Models\UserActivity; use App\Community\Models\UserActivityLegacy; -use App\Platform\Events\AchievementCreated; +use App\Platform\Events\AchievementSetBeaten; use App\Platform\Events\AchievementSetCompleted; -use App\Platform\Events\AchievementUpdated; use App\Platform\Events\LeaderboardEntryCreated; use App\Platform\Events\LeaderboardEntryUpdated; use App\Platform\Events\PlayerAchievementUnlocked; @@ -33,6 +32,7 @@ public function handle(object $event): void $subjectId = null; $context = null; + $storeLegacyActivity = true; $legacyActivityType = null; $data = null; $data2 = null; @@ -47,27 +47,15 @@ public function handle(object $event): void * ignore login activity within 6 hours after the last login activity */ $legacyActivityType = ActivityType::Login; - $storeActivity = $user->legacyActivities() + $storeLegacyActivity = $user->legacyActivities() ->where('activitytype', '=', $legacyActivityType) ->where('timestamp', '>', Carbon::now()->subHours(6)) ->doesntExist(); - // $userActivityType = UserActivityType::Login; - // $storeActivity = $user->activities() - // ->where('type', '=', $userActivityType) - // ->where('created_at', '>', Carbon::now()->subHours(6)) - // ->doesntExist(); - break; - case AchievementCreated::class: - $userActivityType = UserActivityType::UploadAchievement; - // TODO: subject_context = create - // TODO: subject_id - $subjectType = 'achievement'; - break; - case AchievementUpdated::class: - $userActivityType = UserActivityType::EditAchievement; - // TODO: subject_context = update - // TODO: subject_id - $subjectType = 'achievement'; + $userActivityType = UserActivityType::Login; + $storeActivity = $user->activities() + ->where('type', '=', $userActivityType) + ->where('created_at', '>', Carbon::now()->subHours(6)) + ->doesntExist(); break; case LeaderboardEntryCreated::class: $userActivityType = UserActivityType::NewLeaderboardEntry; @@ -89,10 +77,16 @@ public function handle(object $event): void $context = $event->hardcore ? 1 : null; break; case AchievementSetCompleted::class: - $userActivityType = UserActivityType::CompleteGame; + $userActivityType = UserActivityType::CompleteAchievementSet; // TODO: subject_context = complete // TODO: subject_id - $subjectType = 'game'; + // TODO $subjectType = 'achievement_set'; + break; + case AchievementSetBeaten::class: + $userActivityType = UserActivityType::BeatAchievementSet; + // TODO: subject_context = beat + // TODO: subject_id + // TODO $subjectType = 'achievement_set'; break; case PlayerGameAttached::class: $userActivityType = UserActivityType::StartedPlaying; @@ -103,7 +97,7 @@ public function handle(object $event): void default: } - if ($legacyActivityType && $storeActivity) { + if ($legacyActivityType && $storeLegacyActivity) { $user->legacyActivities()->save(new UserActivityLegacy([ 'activitytype' => $legacyActivityType, 'data' => $data, diff --git a/app/Connect/Concerns/AchievementRequests.php b/app/Connect/Concerns/AchievementRequests.php index 4de88bc5b2..27d623de51 100644 --- a/app/Connect/Concerns/AchievementRequests.php +++ b/app/Connect/Concerns/AchievementRequests.php @@ -4,13 +4,12 @@ namespace App\Connect\Concerns; -use App\Platform\Actions\ResumePlayerSessionAction; -use App\Platform\Actions\UnlockPlayerAchievementAction; +use App\Platform\Jobs\UnlockPlayerAchievementJob; use App\Platform\Models\Achievement; use App\Platform\Models\Game; use App\Platform\Models\PlayerAchievement; +use App\Platform\Models\PlayerGame; use App\Site\Models\User; -use Exception; use Illuminate\Http\Request; trait AchievementRequests @@ -115,24 +114,32 @@ protected function awardachievementMethod(Request $request): array $achievement = Achievement::find($achievementId); abort_if($achievement === null, 404, 'Achievement with ID "' . $achievementId . '" not found'); + // TODO "Unofficial achievements cannot be unlocked" + /** @var User $user */ $user = $request->user('connect-token'); // TODO: validate sent hash // $request->input('v') === $achievement->unlockValidationHash($user, (int) $hardcore); - // resume the player session before unlocking the achievement so they'll get associated together - try { - /** @var ResumePlayerSessionAction $resumePlayerSessionAction */ - $resumePlayerSessionAction = app()->make(ResumePlayerSessionAction::class); - $resumePlayerSessionAction->execute($request, $achievement->game); - } catch (Exception) { - // fail silently - might be an unauthenticated request (RetroArch) - } + // fail silently - might be an unauthenticated request (RetroArch) + dispatch(new UnlockPlayerAchievementJob($user->id, $achievement->id, (bool) $hardcore)) + ->onQueue('player-achievements'); - /** @var UnlockPlayerAchievementAction $unlockPlayerAchievementAction */ - $unlockPlayerAchievementAction = app()->make(UnlockPlayerAchievementAction::class); + $playerGame = PlayerGame::where('user_id', $user->id) + ->where('game_id', $achievement->game_id) + ->first(); + $remaining = 0; + if ($playerGame) { + $remaining = $playerGame->achievements_total - $playerGame->achievements_unlocked; + } - return $unlockPlayerAchievementAction->execute($user, $achievement, (bool) $hardcore); + // TODO respond with optimistically updated score values + return [ + 'Score' => $user->points, + 'SoftcoreScore' => (int) $user->points_softcore, + 'AchievementID' => (int) $achievementId, + 'AchievementsRemaining' => $remaining, + ]; } } diff --git a/app/Connect/Concerns/AuthRequests.php b/app/Connect/Concerns/AuthRequests.php index 1f704972a7..52c7b4457a 100644 --- a/app/Connect/Concerns/AuthRequests.php +++ b/app/Connect/Concerns/AuthRequests.php @@ -136,7 +136,7 @@ protected function loginMethod(Request $request): array $response = [ 'displayName' => $user->display_name, - 'pointsTotal' => $user->points_total, + 'pointsTotal' => $user->points, 'unreadMessagesCount' => $user->unread_messages_count, 'username' => $user->username, 'token' => $user->connect_token, diff --git a/app/Connect/Concerns/DevelopmentRequests.php b/app/Connect/Concerns/DevelopmentRequests.php index 4c22b0d125..2798145eb3 100644 --- a/app/Connect/Concerns/DevelopmentRequests.php +++ b/app/Connect/Concerns/DevelopmentRequests.php @@ -4,7 +4,7 @@ namespace App\Connect\Concerns; -use App\Platform\Actions\LinkHashToGameAction; +use App\Platform\Actions\LinkHashToGame; use App\Platform\Models\Achievement; use App\Platform\Models\Game; use App\Platform\Models\GameHash; @@ -91,8 +91,8 @@ protected function submitgametitleMethod(Request $request): array /** @var Game $game */ - /** @var LinkHashToGameAction $linkHashToGameAction */ - $linkHashToGameAction = app()->make(LinkHashToGameAction::class); + /** @var LinkHashToGame $linkHashToGameAction */ + $linkHashToGameAction = app()->make(LinkHashToGame::class); $linkHashToGameAction->execute($hash, $game, $gameHashTitle); return [ diff --git a/app/Connect/Concerns/GameRequests.php b/app/Connect/Concerns/GameRequests.php index 929e73df1f..eb5c7ec7ec 100644 --- a/app/Connect/Concerns/GameRequests.php +++ b/app/Connect/Concerns/GameRequests.php @@ -4,7 +4,7 @@ namespace App\Connect\Concerns; -use App\Platform\Actions\ResumePlayerSessionAction; +use App\Platform\Actions\ResumePlayerSession; use App\Platform\Models\Game; use App\Platform\Models\GameHash; use App\Platform\Models\GameHashSet; @@ -86,9 +86,9 @@ protected function gameidMethod(Request $request): array // NOTE: checking for a game id by hash might be done by tools as well // this endpoint is sometimes retried in quick succession, too try { - /** @var ResumePlayerSessionAction $resumePlayerSessionAction */ - $resumePlayerSessionAction = app()->make(ResumePlayerSessionAction::class); - $resumePlayerSessionAction->execute($request, $game, $gameHash); + /** @var ResumePlayerSession $resumePlayerSessionAction */ + $resumePlayerSessionAction = app()->make(ResumePlayerSession::class); + $resumePlayerSessionAction->execute($request->user('connect-token'), $game, $gameHash); } catch (Exception) { // fail silently - might be an unauthenticated request (RetroArch) } diff --git a/app/Connect/Concerns/HeartbeatRequests.php b/app/Connect/Concerns/HeartbeatRequests.php index 79f62ecdb0..15005b2e10 100644 --- a/app/Connect/Concerns/HeartbeatRequests.php +++ b/app/Connect/Concerns/HeartbeatRequests.php @@ -6,7 +6,7 @@ use App\Community\Enums\ActivityType; use App\Community\Models\UserActivity; -use App\Platform\Actions\ResumePlayerSessionAction; +use App\Platform\Events\PlayerSessionHeartbeat; use App\Platform\Models\Game; use Exception; use Illuminate\Http\Request; @@ -15,7 +15,6 @@ trait HeartbeatRequests { /** * Used by RAIntegration - * Sends out events * Called on * - game load -> StartedPlaying * @@ -42,17 +41,9 @@ protected function postactivityMethod(Request $request): array $activityType = $request->input('a'); $messagePayload = $request->input('m'); - /* - * any activity event will update the last_activity_at timestamp on the user - */ + // Behave like ping, ignore the rest if ($activityType === ActivityType::StartedPlaying) { - /** @var ?Game $game */ - $game = Game::find($messagePayload); - if ($game) { - /** @var ResumePlayerSessionAction $resumePlayerSessionAction */ - $resumePlayerSessionAction = app()->make(ResumePlayerSessionAction::class); - $resumePlayerSessionAction->execute($request, $game); - } + PlayerSessionHeartbeat::dispatch($request->user('connect-token'), Game::find($messagePayload)); } return []; @@ -70,10 +61,6 @@ protected function pingMethod(Request $request): array { $this->authorize('create', UserActivity::class); - // if (isset($activityMessage)) { - // UpdateUserRichPresence($user, $gameID, $activityMessage); - // } - $request->validate( [ 'g' => 'required|integer', @@ -87,24 +74,11 @@ protected function pingMethod(Request $request): array ); $gameId = $request->input('g'); - - // TODO: should be $request->user('connect-token')->games()->find($gameId) - /** @var ?Game $game */ - $game = Game::find($gameId); - abort_if($game === null, 404, 'Game with ID "' . $gameId . '" not found'); - /** - * TODO: abort if game has not yet been assigned to user - */ - // abort_unless($game !== null, 404, 'Game with ID "' . $gameId . '" is not attached to this user'); - $richPresence = $request->input('m'); - /** - * TODO: pass game hash here if set - */ - /** @var ResumePlayerSessionAction $resumePlayerSessionAction */ - $resumePlayerSessionAction = app()->make(ResumePlayerSessionAction::class); - $resumePlayerSessionAction->execute($request, $game, null, $richPresence); + // TODO: pass game hash here if set + + PlayerSessionHeartbeat::dispatch($request->user('connect-token'), $gameId, $richPresence); return []; } diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 1c3b6083e7..d2969d88f5 100755 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -42,8 +42,6 @@ protected function schedule(Schedule $schedule) // $this->call('ra:sync:game-hashes'); // $this->call('ra:sync:memory-notes'); - $this->call('ra:sync:player-achievements'); - $this->call('ra:sync:player-games'); // $this->call('ra:sync:user-awards'); // $this->call('ra:sync:game-relations'); // $this->call('ra:sync:leaderboards'); diff --git a/app/Helpers/database/achievement-creator.php b/app/Helpers/database/achievement-creator.php index 6b6f6bdb39..549a3554b0 100644 --- a/app/Helpers/database/achievement-creator.php +++ b/app/Helpers/database/achievement-creator.php @@ -1,10 +1,7 @@ first()['Author'] === $user; } - -function attributeDevelopmentAuthor(string $author, int $count, int $points): void -{ - $user = User::firstWhere('User', $author); - if ($user === null) { - return; - } - - $oldContribCount = $user->ContribCount; - $oldContribYield = $user->ContribYield; - - // use raw statement to perform atomic update - legacyDbStatement("UPDATE UserAccounts SET ContribCount = ContribCount + $count," . - " ContribYield = ContribYield + $points WHERE User=:user", ['user' => $author]); - - $newContribTier = PlayerBadge::getNewBadgeTier(AwardType::AchievementUnlocksYield, $oldContribCount, $oldContribCount + $count); - if ($newContribTier !== null) { - AddSiteAward($author, AwardType::AchievementUnlocksYield, $newContribTier); - } - - $newPointsTier = PlayerBadge::getNewBadgeTier(AwardType::AchievementPointsYield, $oldContribYield, $oldContribYield + $points); - if ($newPointsTier !== null) { - AddSiteAward($author, AwardType::AchievementPointsYield, $newPointsTier); - } -} - -function recalculateDeveloperContribution(string $author): void -{ - sanitize_sql_inputs($author); - - $query = "SELECT COUNT(*) AS ContribCount, SUM(Points) AS ContribYield - FROM (SELECT aw.User, ach.ID, MAX(aw.HardcoreMode) as HardcoreMode, ach.Points - FROM Achievements ach LEFT JOIN Awarded aw ON aw.AchievementID=ach.ID - WHERE ach.Author='$author' AND aw.User != '$author' - AND ach.Flags=" . AchievementFlag::OfficialCore . " - GROUP BY 1,2) AS UniqueUnlocks"; - - $dbResult = s_mysql_query($query); - if ($dbResult !== false) { - $contribCount = 0; - $contribYield = 0; - - if ($data = mysqli_fetch_assoc($dbResult)) { - $contribCount = $data['ContribCount'] ?? 0; - $contribYield = $data['ContribYield'] ?? 0; - } - - $query = "UPDATE UserAccounts - SET ContribCount = $contribCount, ContribYield = $contribYield - WHERE User = '$author'"; - - $dbResult = s_mysql_query($query); - if (!$dbResult) { - log_sql_fail(); - } - } -} diff --git a/app/Helpers/database/achievement.php b/app/Helpers/database/achievement.php index 5fbf36fcde..56082d5369 100644 --- a/app/Helpers/database/achievement.php +++ b/app/Helpers/database/achievement.php @@ -5,6 +5,11 @@ use App\Platform\Enums\AchievementFlag; use App\Platform\Enums\AchievementPoints; use App\Platform\Enums\AchievementType; +use App\Platform\Events\AchievementCreated; +use App\Platform\Events\AchievementPointsChanged; +use App\Platform\Events\AchievementPublished; +use App\Platform\Events\AchievementTypeChanged; +use App\Platform\Events\AchievementUnpublished; use App\Platform\Models\Achievement; use App\Site\Enums\Permissions; use Illuminate\Support\Carbon; @@ -255,6 +260,7 @@ function UploadNewAchievement( ); // uploaded new achievement + AchievementCreated::dispatch(Achievement::find($idInOut)); return true; } @@ -324,6 +330,8 @@ function UploadNewAchievement( postActivity($author, ActivityType::EditAchievement, $idInOut); + $achievement = Achievement::find($idInOut); + if ($changingAchSet) { if ($flag === AchievementFlag::OfficialCore) { addArticleComment( @@ -333,6 +341,7 @@ function UploadNewAchievement( "$author promoted this achievement to the Core set.", $author ); + AchievementPublished::dispatch($achievement); } elseif ($flag === AchievementFlag::Unofficial) { addArticleComment( "Server", @@ -341,6 +350,7 @@ function UploadNewAchievement( "$author demoted this achievement to Unofficial.", $author ); + AchievementUnpublished::dispatch($achievement); } expireGameTopAchievers($gameID); } else { @@ -376,22 +386,11 @@ function UploadNewAchievement( } } - if ($changingPoints || $changingAchSet) { - $numUnlocks = getAchievementUnlockCount($idInOut); - if ($numUnlocks > 0) { - if ($changingAchSet) { - if ($flag === AchievementFlag::OfficialCore) { - // promoted to core, restore point attribution - attributeDevelopmentAuthor($data['Author'], $numUnlocks, $numUnlocks * $points); - } else { - // demoted from core, remove point attribution - attributeDevelopmentAuthor($data['Author'], -$numUnlocks, -$numUnlocks * $points); - } - } else { - // points changed, adjust point attribution - attributeDevelopmentAuthor($data['Author'], 0, $numUnlocks * ($points - (int) $data['Points'])); - } - } + if ($changingPoints) { + AchievementPointsChanged::dispatch($achievement); + } + if ($changingType) { + AchievementTypeChanged::dispatch($achievement); } return true; @@ -537,13 +536,16 @@ function updateAchievementFlag(int|string|array $achID, int $newFlag): bool return false; } - foreach ($authorCount as $author => $count) { - $points = $authorPoints[$author]; - if ($newFlag != AchievementFlag::OfficialCore) { - $count = -$count; - $points = -$points; + foreach ($updatedAchIDs as $achievementId) { + $achievement = Achievement::find($achievementId); + + if ($newFlag === AchievementFlag::OfficialCore) { + AchievementPublished::dispatch($achievement); + } + + if ($newFlag === AchievementFlag::Unofficial) { + AchievementUnpublished::dispatch($achievement); } - attributeDevelopmentAuthor($author, $count, $points); } return true; diff --git a/app/Helpers/database/player-achievement.php b/app/Helpers/database/player-achievement.php index 48ab16fba1..f5e2279b29 100644 --- a/app/Helpers/database/player-achievement.php +++ b/app/Helpers/database/player-achievement.php @@ -10,6 +10,9 @@ use Carbon\Carbon; use Illuminate\Support\Collection; +/** + * @deprecated see PlayerAchievement model + */ function playerHasUnlock(?string $user, int $achievementId): array { $retVal = [ @@ -40,24 +43,9 @@ function playerHasUnlock(?string $user, int $achievementId): array return $retVal; } -function recalculatePlayerBeatenGames(string $username): bool -{ - // Get the list of games the user has played. - $gamesPlayedQuery = "SELECT DISTINCT Achievements.GameID - FROM Awarded - INNER JOIN Achievements ON Awarded.AchievementID = Achievements.ID - WHERE Awarded.User = :username - "; - - $gamesPlayed = legacyDbFetchAll($gamesPlayedQuery, ['username' => $username])->toArray(); - - foreach ($gamesPlayed as $game) { - testBeatenGame($game['GameID'], $username, true); - } - - return true; -} - +/** + * @deprecated see UnlockPlayerAchievementAction + */ function unlockAchievement(string $username, int $achievementId, bool $isHardcore): array { $retVal = [ @@ -84,6 +72,7 @@ function unlockAchievement(string $username, int $achievementId, bool $isHardcor return $retVal; } + // TODO config('feature.aggregate_queries') $hasAwardTypes = playerHasUnlock($user->User, $achievement->ID); $hasRegular = $hasAwardTypes['HasRegular']; $hasHardcore = $hasAwardTypes['HasHardcore']; @@ -107,18 +96,11 @@ function unlockAchievement(string $username, int $achievementId, bool $isHardcor ]); } - // TODO dispatch user unlock event to start/extend player session, upsert user game entry - if (!$alreadyAwarded) { - // testFullyCompletedGame could post a mastery notification. make sure to post - // the achievement unlock notification first postActivity($user, ActivityType::UnlockedAchievement, $achievement->ID, (int) $isHardcore); } - // TODO: Should the return value from this function be attaching anything to $retVal? - testBeatenGame($achievement->GameID, $user->User, !$alreadyAwarded); - - $completion = testFullyCompletedGame($achievement->GameID, $user->User, $isHardcore, !$alreadyAwarded); + $completion = getUnlockCounts($achievement->GameID, $user->username, $isHardcore); if (array_key_exists('NumAwarded', $completion)) { $retVal['AchievementsRemaining'] = $completion['NumAch'] - $completion['NumAwarded']; } @@ -137,30 +119,12 @@ function unlockAchievement(string $username, int $achievementId, bool $isHardcor return $retVal; } - // Use raw statement to ensure updates are atomic. Modifying the user model and - // committing via save() leaves a window where multiple simultaneous unlocks can - // increment the score separately and miss the merged result. For example: - // * unlock A => read points = 10 - // * unlock B => read points = 10 - // * unlock A => award 5 points, total = 15 - // * unlock B => award 10 points, total = 20 - // * unlock A => commit points (15) - // * unlock B => commit points (20) - // -- actual points is 20, expected points should be 25: 10 + 5 (A) + 10 (B) - $updateClause = ''; - if ($isHardcore) { - $updateClause = 'RAPoints = RAPoints + ' . $achievement->Points; - $updateClause .= ', TrueRAPoints = TrueRAPoints + ' . $achievement->TrueRatio; - if ($hasRegular) { - $updateClause .= ', RASoftcorePoints = RASoftcorePoints - ' . $achievement->Points; - } - } else { - $updateClause = 'RASoftcorePoints = RASoftcorePoints + ' . $achievement->Points; + // Optimistic update for async metrics updates + if (config('queue.default') !== 'sync') { + $retVal['SoftcoreScore'] = $isHardcore ? $user->points_softcore : $user->points_softcore + $achievement->points; + $retVal['Score'] = $isHardcore ? $user->points + $achievement->points : $user->points; } - legacyDbStatement("UPDATE UserAccounts SET $updateClause, Updated=:now WHERE User=:user", - ['user' => $user->User, 'now' => Carbon::now()]); - $retVal['Success'] = true; // Achievements all awarded. Now housekeeping (no error handling?) @@ -169,14 +133,6 @@ function unlockAchievement(string $username, int $achievementId, bool $isHardcor static_setlastearnedachievement($achievement->ID, $user->User, $achievement->Points); - if ($user->User != $achievement->Author) { - if ($isHardcore && $hasRegular) { - // developer received contribution points when the regular version was unlocked - } else { - attributeDevelopmentAuthor($achievement->Author, 1, $achievement->Points); - } - } - return $retVal; } @@ -241,7 +197,8 @@ function getAchievementUnlocksData( $data = legacyDbFetch($query, $bindings); $numWinners = $data['NumEarned']; - $numPossibleWinners = getTotalUniquePlayers((int) $data['GameID'], $parentGameId, requestedBy: $username, achievementFlag: AchievementFlag::OfficialCore); + // TODO use $game->players_total + $numPossibleWinners = getTotalUniquePlayers((int) $data['GameID'], $parentGameId, requestedBy: $username); // Get recent winners, and their most recent activity $bindings = [ @@ -307,7 +264,8 @@ function getRecentUnlocksPlayersData( // Fetch the total number of players for this game: $parentGameID = getParentGameIdFromGameTitle($game->Title, $game->ConsoleID); - $retVal['TotalPlayers'] = getTotalUniquePlayers($game->ID, $parentGameID, achievementFlag: AchievementFlag::OfficialCore); + // TODO use $game->players_total + $retVal['TotalPlayers'] = getTotalUniquePlayers($game->ID, $parentGameID); $extraWhere = ""; if ($friendsOnly && isset($user) && $user) { diff --git a/app/Helpers/database/player-game.php b/app/Helpers/database/player-game.php index 5a193bed01..910ec641b6 100644 --- a/app/Helpers/database/player-game.php +++ b/app/Helpers/database/player-game.php @@ -1,12 +1,10 @@ whereIn('type', [AchievementType::Progression, AchievementType::WinCondition]) ->where('Flags', AchievementFlag::OfficialCore) - ->select('type', DB::raw('count(*) as total')) + ->select(['type', DB::raw('count(*) as total')]) ->groupBy('type') ->get() ->keyBy('type') @@ -36,8 +37,7 @@ function testBeatenGame(int $gameId, string $user, bool $postBeaten): array // If the game has no beaten-tier achievements assigned, it is not considered beatable. // Bail. if ($totalProgressions === 0 && $totalWinConditions === 0) { - purgeAllPlayerBeatenGameAwardsForGame($user, $gameId); - + // TODO use $playerGame->achievements_beat for isBeatable, remove rest return [ 'isBeatenSoftcore' => false, 'isBeatenHardcore' => false, @@ -54,24 +54,14 @@ function testBeatenGame(int $gameId, string $user, bool $postBeaten): array $join->on('Achievements.ID', '=', 'Awarded.AchievementID') ->where('Awarded.User', '=', $user); }) - ->select('Achievements.type', 'Awarded.HardcoreMode', 'Awarded.AchievementID', 'Awarded.Date') + ->addSelect(['Achievements.type', 'Awarded.HardcoreMode', 'Awarded.AchievementID', 'Awarded.Date']) ->orderByDesc('Awarded.Date') - ->get(); - - // Create a Laravel collection and then group the collection items by generating a unique - // key for each item. The key is a combination of the achievement type and HardcoreMode - // status, separated by "|". For example, a Progression achievement accomplished in - // hardcore mode will have the key "Progression|1". After the groupBy, use the map - // method to apply the count function to each group. This approach allows us to - // both classify and count in a single loop with minimal conditional logic. - $achievements = collect($userAchievements)->groupBy(function ($item) { - return implode('|', [$item->type, $item->HardcoreMode]); - })->map->count(); - - $numUnlockedSoftcoreProgressions = $achievements[AchievementType::Progression . '|0'] ?? 0; - $numUnlockedHardcoreProgressions = $achievements[AchievementType::Progression . '|1'] ?? 0; - $numUnlockedSoftcoreWinConditions = $achievements[AchievementType::WinCondition . '|0'] ?? 0; - $numUnlockedHardcoreWinConditions = $achievements[AchievementType::WinCondition . '|1'] ?? 0; + ->get(['Achievements.type', 'Awarded.HardcoreMode', 'Awarded.AchievementID', 'Awarded.Date']); + + $numUnlockedSoftcoreProgressions = $userAchievements->where('type', AchievementType::Progression)->whereNotNull('Date')->where('HardcoreMode', UnlockMode::Softcore)->count(); + $numUnlockedHardcoreProgressions = $userAchievements->where('type', AchievementType::Progression)->whereNotNull('Date')->where('HardcoreMode', UnlockMode::Hardcore)->count(); + $numUnlockedSoftcoreWinConditions = $userAchievements->where('type', AchievementType::WinCondition)->whereNotNull('Date')->where('HardcoreMode', UnlockMode::Softcore)->count(); + $numUnlockedHardcoreWinConditions = $userAchievements->where('type', AchievementType::WinCondition)->whereNotNull('Date')->where('HardcoreMode', UnlockMode::Hardcore)->count(); // If there are no Win Condition achievements in the set, the game is considered beaten // if the user unlocks all the progression achievements. @@ -85,44 +75,7 @@ function testBeatenGame(int $gameId, string $user, bool $postBeaten): array $numUnlockedHardcoreProgressions === $totalProgressions && $numUnlockedHardcoreWinConditions >= $neededWinConditionAchievements; - $isBeaten = $isBeatenSoftcore || $isBeatenHardcore; - - // Revoke pre-existing awards that no longer satisfy the game's "beaten" criteria. - // If the platform changes the definition of beating a game and the user no - // longer satisfies the criteria, they should not have the award anymore. - $alreadyHasBeatenAwards = HasBeatenSiteAwards($user, $gameId); - if ($alreadyHasBeatenAwards && !$isBeaten) { - if (!$isBeatenSoftcore) { - purgePlayerBeatenGameAward($user, $gameId, UnlockMode::Softcore); - } - - if (!$isBeatenHardcore) { - purgePlayerBeatenGameAward($user, $gameId, UnlockMode::Hardcore); - } - } - - // The user has beaten the game, give them an award. - if ($postBeaten && $isBeaten) { - $awardMode = $isBeatenHardcore ? UnlockMode::Hardcore : UnlockMode::Softcore; - - if (!HasSiteAward($user, AwardType::GameBeaten, $gameId, $awardMode)) { - $awardDate = Carbon::parse(calculateBeatenGameTimestamp($userAchievements)); - - AddSiteAward( - $user, - AwardType::GameBeaten, - $gameId, - $awardMode, - $awardDate, - displayOrder: 0 - ); - - if ($isBeatenHardcore && $awardDate->gte(Carbon::now()->subMinutes(10))) { - static_addnewhardcoregamebeaten($gameId, $user); - } - } - } - + // TODO use $playerGame->beaten_at for isBeatenSoftcore, $playerGame->beaten_hardcore_at for isBeatenHardcore, remove rest return [ 'isBeatenSoftcore' => $isBeatenSoftcore, 'isBeatenHardcore' => $isBeatenHardcore, @@ -130,104 +83,28 @@ function testBeatenGame(int $gameId, string $user, bool $postBeaten): array ]; } -function purgePlayerBeatenGameAward(string $username, int $gameId, int $unlockMode = UnlockMode::Softcore): void -{ - PlayerBadge::where('User', $username) - ->where('AwardType', AwardType::GameBeaten) - ->where('AwardData', $gameId) - ->where('AwardDataExtra', $unlockMode) - ->delete(); -} - -function purgeAllPlayerBeatenGameAwardsForGame(string $username, int $gameId): void -{ - PlayerBadge::where('User', $username) - ->where('AwardType', AwardType::GameBeaten) - ->where('AwardData', $gameId) - ->delete(); -} - /** - * Beaten game awards are stored with an AwardDate that corresponds to when they - * unlocked the precise achievement that granted them the beaten status. This has - * to be calculated by on the rules that Progression and Win Condition achievements follow. + * @deprecated TODO read from PlayerGame model */ -function calculateBeatenGameTimestamp(mixed $userAchievements): string -{ - $progressionAchievementsUnlocked = 0; - $latestProgressionDate = null; - $earliestWinConditionDate = null; - - foreach ($userAchievements as $achievement) { - if ($achievement->type == AchievementType::Progression && $achievement->AchievementID) { - $progressionAchievementsUnlocked++; - // Keep track of the latest progression achievement date. - $latestProgressionDate = $latestProgressionDate === null || $achievement->Date > $latestProgressionDate - ? $achievement->Date - : $latestProgressionDate; - } elseif ($achievement->type == AchievementType::WinCondition && $achievement->AchievementID) { - // Keep track of the earliest win condition date. - $earliestWinConditionDate = $earliestWinConditionDate === null || $achievement->Date < $earliestWinConditionDate - ? $achievement->Date - : $earliestWinConditionDate; - } - } - - // Return the latest date between the progression and win condition achievements. - return $progressionAchievementsUnlocked > 0 - ? ($latestProgressionDate ? max($latestProgressionDate, $earliestWinConditionDate) : $earliestWinConditionDate) - : $earliestWinConditionDate; -} - -function testFullyCompletedGame(int $gameID, string $user, bool $isHardcore, bool $postMastery): array +function getUnlockCounts(int $gameID, string $username, bool $hardcore = false): ?array { - // TODO remove, implement in UpdatePlayerGameMetricsAction instead - - $query = "SELECT COUNT(DISTINCT ach.ID) AS NumAch, + $data = legacyDbFetch( + "SELECT COUNT(DISTINCT ach.ID) AS NumAch, COUNT(CASE WHEN aw.HardcoreMode=1 THEN 1 ELSE NULL END) AS NumAwardedHC, COUNT(CASE WHEN aw.HardcoreMode=0 THEN 1 ELSE NULL END) AS NumAwardedSC FROM Achievements AS ach - LEFT JOIN Awarded AS aw ON aw.AchievementID = ach.ID AND aw.User = :user - WHERE ach.GameID = $gameID AND ach.Flags = " . AchievementFlag::OfficialCore; - - $data = legacyDbFetch($query, ['user' => $user]); - - $minToCompleteGame = 6; - if ($postMastery && $data['NumAch'] >= $minToCompleteGame) { - $awardBadge = null; - if ($isHardcore && $data['NumAwardedHC'] === $data['NumAch']) { - // all hardcore achievements unlocked, award mastery - $awardBadge = UnlockMode::Hardcore; - } elseif ($data['NumAwardedSC'] === $data['NumAch']) { - if ($isHardcore && HasSiteAward($user, AwardType::Mastery, $gameID, UnlockMode::Softcore)) { - // when unlocking a hardcore achievement, don't update the completion - // date if the user already has a completion badge - } else { - $awardBadge = UnlockMode::Softcore; - } - } - - if ($awardBadge !== null) { - if (!HasSiteAward($user, AwardType::Mastery, $gameID, $awardBadge)) { - AddSiteAward($user, AwardType::Mastery, $gameID, $awardBadge); - - if ($awardBadge === UnlockMode::Hardcore) { - static_addnewhardcoremastery($gameID, $user); - } - } - - if (!RecentlyPostedProgressionActivity($user, $gameID, $awardBadge, ActivityType::CompleteGame)) { - postActivity($user, ActivityType::CompleteGame, $gameID, $awardBadge); - } + LEFT JOIN Awarded AS aw ON aw.AchievementID = ach.ID AND aw.User = :username + WHERE ach.GameID = $gameID AND ach.Flags = " . AchievementFlag::OfficialCore, + ['username' => $username] + ); - expireGameTopAchievers($gameID); - } + if ($data === null) { + return $data; } - return [ - 'NumAch' => $data['NumAch'], - 'NumAwarded' => $isHardcore ? $data['NumAwardedHC'] : $data['NumAwardedSC'], - ]; + $data['NumAwarded'] = $hardcore ? $data['NumAwardedHC'] : $data['NumAwardedSC']; + + return $data; } function getGameRankAndScore(int $gameID, string $username): array @@ -421,23 +298,48 @@ function getUserProgress(string $user, array $gameIDs, int $numRecentAchievement return $libraryOut; } +/** + * @deprecated not used anymore after denormalization + */ function expireUserAchievementUnlocksForGame(string $user, int $gameID): void { Cache::forget(CacheKey::buildUserGameUnlocksCacheKey($user, $gameID, true)); Cache::forget(CacheKey::buildUserGameUnlocksCacheKey($user, $gameID, false)); } -function getUserAchievementUnlocksForGame(string $user, int $gameID, int $flag = AchievementFlag::OfficialCore): array +function getUserAchievementUnlocksForGame(string $username, int $gameID, int $flag = AchievementFlag::OfficialCore): array { + if (config('feature.aggregate_queries')) { + $user = User::firstWhere('User', $username); + $achievementIds = Achievement::where('GameID', $gameID) + ->where('Flags', $flag) + ->pluck('ID'); + $playerAchievements = PlayerAchievement::where('user_id', $user->id) + ->whereIn('achievement_id', $achievementIds) + ->get([ + 'achievement_id', + 'unlocked_at', + 'unlocked_hardcore_at', + ]) + ->mapWithKeys(function (PlayerAchievement $unlock, int $key) { + return [$unlock->achievement_id => [ + // TODO move this transformation to where it's needed (web api) and use models everywhere else + 'DateEarned' => $unlock->unlocked_at?->format('Y-m-d H:i:s'), + 'DateEarnedHardcore' => $unlock->unlocked_hardcore_at?->format('Y-m-d H:i:s'), + ]]; + }); + + return $playerAchievements->toArray(); + } $cacheKey = CacheKey::buildUserGameUnlocksCacheKey( - $user, + $username, $gameID, isOfficial: $flag === AchievementFlag::OfficialCore ); return Cache::remember($cacheKey, Carbon::now()->addDays(7), - function () use ($user, $gameID, $flag) { + function () use ($username, $gameID, $flag) { $query = "SELECT ach.ID, aw.Date, aw.HardcoreMode FROM Awarded AS aw LEFT JOIN Achievements AS ach ON ach.ID = aw.AchievementID @@ -446,7 +348,7 @@ function () use ($user, $gameID, $flag) { $userUnlocks = legacyDbFetchAll($query, [ 'gameId' => $gameID, 'achievementFlag' => $flag, - 'username' => $user, + 'username' => $username, ]); $achievementUnlocks = []; @@ -567,13 +469,15 @@ function getUsersGameList(string $user, ?array &$dataOut): int return $i; } -// TODO: Remove when denormalized data is ready. See comments in getUsersCompletedGamesAndMax(). +/** + * @deprecated TODO: Remove when denormalized data is ready. See comments in getUsersCompletedGamesAndMax(). + */ function getLightweightUsersCompletedGamesAndMax(string $user, string $cachedAwardedValues): array { // Parse the cached value. $awardedCache = []; foreach (explode(',', $cachedAwardedValues) as $row) { - list($gameId, $maxPossible, $numAwarded, $numAwardedHC) = explode('|', $row); + [$gameId, $maxPossible, $numAwarded, $numAwardedHC] = explode('|', $row); $awardedCache[$gameId] = [ 'MaxPossible' => $maxPossible, @@ -649,7 +553,9 @@ function getLightweightUsersCompletedGamesAndMax(string $user, string $cachedAwa return $lightResults; } -// TODO: Remove when denormalized data is ready. See comments in getUsersCompletedGamesAndMax(). +/** + * @deprecated TODO Remove when denormalized data is ready. See comments in getUsersCompletedGamesAndMax(). + */ function prepareUserCompletedGamesCacheValue(array $allFetchedResults): string { // Extract awarded data @@ -669,7 +575,9 @@ function prepareUserCompletedGamesCacheValue(array $allFetchedResults): string return $awardedCacheString; } -// TODO: Remove when denormalized data is ready. See comments in getUsersCompletedGamesAndMax(). +/** + * @deprecated TODO Remove when denormalized data is ready. See comments in getUsersCompletedGamesAndMax(). + */ function expireUserCompletedGamesCacheValue(string $user): void { Cache::delete(CacheKey::buildUserCompletedGamesCacheKey($user)); @@ -685,7 +593,7 @@ function getUsersCompletedGamesAndMax(string $user): array $minAchievementsForCompletion = 5; if (config('feature.aggregate_queries')) { - $query = "SELECT gd.ID AS GameID, c.Name AS ConsoleName, c.ID AS ConsoleID, + $query = "SELECT gd.ID AS GameID, c.Name AS ConsoleName, c.ID AS ConsoleID, gd.ImageIcon, gd.Title, gd.achievements_published as MaxPossible, pg.achievements_unlocked AS NumAwarded, pg.achievements_unlocked_hardcore AS NumAwardedHC, " . floatDivisionStatement('pg.achievements_unlocked', 'gd.achievements_published') . " AS PctWon, " . @@ -731,12 +639,14 @@ function getUsersCompletedGamesAndMax(string $user): array return $fullResults; } +/** + * @deprecated TODO use denormalized game metrics players_total and players_hardcore + */ function getTotalUniquePlayers( int $gameID, ?int $parentGameID = null, ?string $requestedBy = null, bool $hardcoreOnly = false, - ?int $achievementFlag = null, ): int { $bindings = [ 'gameId' => $gameID, @@ -748,11 +658,7 @@ function getTotalUniquePlayers( $unlockModeStatement = ' AND aw.HardcoreMode = :unlockMode'; } - $achievementFlagStatement = ''; - if ($achievementFlag !== null) { - $bindings['achievementFlag'] = $achievementFlag; - $achievementFlagStatement = 'AND ach.Flags = :achievementFlag'; - } + $bindings['achievementFlag'] = AchievementFlag::OfficialCore; $requestedByStatement = ''; if ($requestedBy) { @@ -772,7 +678,7 @@ function getTotalUniquePlayers( LEFT JOIN Achievements AS ach ON ach.ID = aw.AchievementID LEFT JOIN UserAccounts AS ua ON ua.User = aw.User WHERE $gameIdStatement - $unlockModeStatement $achievementFlagStatement + $unlockModeStatement AND ach.Flags = :achievementFlag AND (NOT ua.Untracked $requestedByStatement) "; @@ -804,6 +710,9 @@ function getGameRecentPlayers(int $gameID, int $maximum_results = 0): array return $retval; } +/** + * @deprecated use denormalized data from player_games + */ function expireGameTopAchievers(int $gameID): void { $cacheKey = "game:$gameID:topachievers"; @@ -812,6 +721,8 @@ function expireGameTopAchievers(int $gameID): void /** * Gets a game's high scorers or latest masters. + * + * @deprecated use denormalized data from player_games */ function getGameTopAchievers(int $gameID): array { @@ -833,7 +744,7 @@ function getGameTopAchievers(int $gameID): array $numAchievementsInSet = $data['NumAchievementsInSet']; } - // TODO slow query (17) + // TODO config('feature.aggregate_queries') slow query (17) $query = "SELECT aw.User, COUNT(*) AS NumAchievements, SUM(ach.points) AS TotalScore, MAX(aw.Date) AS LastAward FROM Awarded AS aw LEFT JOIN Achievements AS ach ON ach.ID = aw.AchievementID diff --git a/app/Helpers/database/player-rank.php b/app/Helpers/database/player-rank.php index 9b88a2ee19..3f65b4f7c4 100644 --- a/app/Helpers/database/player-rank.php +++ b/app/Helpers/database/player-rank.php @@ -2,8 +2,7 @@ use App\Community\Enums\Rank; use App\Community\Enums\RankType; -use App\Platform\Enums\AchievementFlag; -use App\Platform\Enums\UnlockMode; +use App\Platform\Events\PlayerRankedStatusChanged; use App\Site\Models\User; use App\Support\Cache\CacheKey; use Illuminate\Support\Carbon; @@ -11,10 +10,16 @@ function SetUserUntrackedStatus(string $usernameIn, int $isUntracked): void { - $query = "UPDATE UserAccounts SET Untracked = $isUntracked, Updated=NOW() WHERE User = '$usernameIn'"; - s_mysql_query($query); + legacyDbStatement("UPDATE UserAccounts SET Untracked = $isUntracked, Updated=NOW() WHERE User = '$usernameIn'"); + + PlayerRankedStatusChanged::dispatch(User::firstWhere('User', $usernameIn), (bool) $isUntracked); + + // TODO update games that are affected by this user's library } +/** + * @deprecated take from authenticated user directly + */ function getPlayerPoints(?string $user, ?array &$dataOut): bool { if (empty($user) || !isValidUsername($user)) { @@ -36,34 +41,10 @@ function getPlayerPoints(?string $user, ?array &$dataOut): bool return false; } -function recalculatePlayerPoints(string $user): bool -{ - sanitize_sql_inputs($user); - - $query = "UPDATE UserAccounts ua - LEFT JOIN ( - SELECT aw.User AS UserAwarded, - SUM(IF(aw.HardcoreMode = " . UnlockMode::Hardcore . ", ach.Points, 0)) AS HardcorePoints, - SUM(IF(aw.HardcoreMode = " . UnlockMode::Hardcore . ", ach.TrueRatio, 0)) AS TruePoints, - SUM(IF(aw.HardcoreMode = " . UnlockMode::Softcore . ", ach.Points, 0)) AS TotalPoints - FROM Awarded AS aw - LEFT JOIN Achievements AS ach ON ach.ID = aw.AchievementID - WHERE aw.User = '$user' AND ach.Flags = " . AchievementFlag::OfficialCore . " - ) hc ON ua.User = hc.UserAwarded - SET RAPoints = COALESCE(hc.HardcorePoints, 0), - TrueRAPoints = COALESCE(hc.TruePoints, 0), - RASoftcorePoints = COALESCE(hc.TotalPoints - hc.HardcorePoints, 0) - WHERE User = '$user'"; - - $dbResult = s_mysql_query($query); - - return (bool) $dbResult; -} - function countRankedUsers(int $type = RankType::Hardcore): int { return Cache::remember("rankedUserCount:$type", - 60, // expire once a minute + Carbon::now()->addMinute(), function () use ($type) { $query = "SELECT COUNT(*) AS count FROM UserAccounts "; switch ($type) { diff --git a/app/Helpers/database/site-award.php b/app/Helpers/database/site-award.php index 25d4de7cbb..d8a3858ee9 100644 --- a/app/Helpers/database/site-award.php +++ b/app/Helpers/database/site-award.php @@ -2,9 +2,13 @@ use App\Community\Enums\AwardType; use App\Platform\Enums\UnlockMode; +use App\Platform\Events\SiteBadgeAwarded; use App\Platform\Models\PlayerBadge; use Carbon\Carbon; +/** + * @deprecated use PlayerBadge model + */ function AddSiteAward( string $user, int $awardType, @@ -12,7 +16,7 @@ function AddSiteAward( int $dataExtra = 0, ?Carbon $awardDate = null, ?int $displayOrder = null, -): void { +): PlayerBadge { if (!isset($displayOrder)) { $displayOrder = 0; $query = "SELECT MAX(DisplayOrder) AS MaxDisplayOrder FROM SiteAwards WHERE User = :user"; @@ -34,26 +38,12 @@ function AddSiteAward( 'DisplayOrder' => $displayOrder, ] ); -} - -function HasBeatenSiteAwards(string $username, int $gameId): bool -{ - return PlayerBadge::where('User', $username) - ->where('AwardType', AwardType::GameBeaten) - ->where('AwardData', $gameId) - ->count() > 0; -} - -function HasSiteAward(string $user, int $awardType, int $data, ?int $dataExtra = null): bool -{ - $query = "SELECT AwardDate FROM SiteAwards WHERE User=:user AND AwardType=$awardType AND AwardData=$data"; - if ($dataExtra !== null) { - $query .= " AND AwardDataExtra=$dataExtra"; - } - - $dbData = legacyDbFetch($query, ['user' => $user]); - return isset($dbData['AwardDate']); + return PlayerBadge::where('User', $user) + ->where('AwardType', $awardType) + ->where('AwardData', $data) + ->where('AwardDataExtra', $dataExtra) + ->first(); } function getUsersWithAward(int $awardType, int $data, ?int $dataExtra = null): array @@ -172,10 +162,13 @@ function SetPatreonSupporter(string $username, bool $enable): void sanitize_sql_inputs($username); if ($enable) { - AddSiteAward($username, AwardType::PatreonSupporter, 0, 0); + $badge = AddSiteAward($username, AwardType::PatreonSupporter, 0, 0); + SiteBadgeAwarded::dispatch($badge); + // TODO PatreonSupporterAdded::dispatch($user); } else { $query = "DELETE FROM SiteAwards WHERE User = '$username' AND AwardType = " . AwardType::PatreonSupporter; s_mysql_query($query); + // TODO PatreonSupporterRemoved::dispatch($user); } } @@ -196,7 +189,8 @@ function SetCertifiedLegend(string $usernameIn, bool $enable): void sanitize_sql_inputs($usernameIn); if ($enable) { - AddSiteAward($usernameIn, AwardType::CertifiedLegend, 0, 0); + $badge = AddSiteAward($usernameIn, AwardType::CertifiedLegend, 0, 0); + SiteBadgeAwarded::dispatch($badge); } else { $query = "DELETE FROM SiteAwards WHERE User = '$usernameIn' AND AwardType = " . AwardType::CertifiedLegend; s_mysql_query($query); @@ -250,10 +244,10 @@ function getUserEventAwardCount(string $user): int 'event' => 101, ]; - $query = "SELECT COUNT(DISTINCT AwardData) AS TotalAwards + $query = "SELECT COUNT(DISTINCT AwardData) AS TotalAwards FROM SiteAwards sa INNER JOIN GameData gd ON gd.ID = sa.AwardData - WHERE User = :user + WHERE User = :user AND AwardType = :type AND gd.ConsoleID = :event"; diff --git a/app/Helpers/database/static.php b/app/Helpers/database/static.php index 4d84923551..53e10dd1ce 100644 --- a/app/Helpers/database/static.php +++ b/app/Helpers/database/static.php @@ -3,6 +3,9 @@ use App\Site\Models\User; use Carbon\Carbon; +/** + * @deprecated + */ function static_addnewachievement(int $id): void { $query = "UPDATE StaticData AS sd "; @@ -13,6 +16,9 @@ function static_addnewachievement(int $id): void } } +/** + * @deprecated + */ function static_addnewgame(int $id): void { // Subquery to get # of games that have achievements @@ -28,6 +34,9 @@ function static_addnewgame(int $id): void } } +/** + * @deprecated + */ function static_addnewregistereduser(string $user): void { sanitize_sql_inputs($user); @@ -40,6 +49,9 @@ function static_addnewregistereduser(string $user): void } } +/** + * @deprecated + */ function static_addnewhardcoremastery(int $gameId, string $username): void { $foundUser = User::firstWhere('User', $username); @@ -58,6 +70,9 @@ function static_addnewhardcoremastery(int $gameId, string $username): void legacyDbStatement($query, ['gameId' => $gameId, 'userId' => $foundUser->ID, 'now' => Carbon::now()]); } +/** + * @deprecated + */ function static_addnewhardcoregamebeaten(int $gameId, string $username): void { $foundUser = User::firstWhere('User', $username); @@ -76,6 +91,9 @@ function static_addnewhardcoregamebeaten(int $gameId, string $username): void legacyDbStatement($query, ['gameId' => $gameId, 'userId' => $foundUser->ID, 'now' => Carbon::now()]); } +/** + * @deprecated + */ function static_setlastearnedachievement(int $id, string $user, int $points): void { $query = "UPDATE StaticData @@ -90,6 +108,9 @@ function static_setlastearnedachievement(int $id, string $user, int $points): vo } } +/** + * @deprecated + */ function static_setlastupdatedgame(int $id): void { $query = "UPDATE StaticData AS sd "; @@ -100,6 +121,9 @@ function static_setlastupdatedgame(int $id): void } } +/** + * @deprecated + */ function static_setlastupdatedachievement(int $id): void { $query = "UPDATE StaticData AS sd "; diff --git a/app/Helpers/database/user-activity.php b/app/Helpers/database/user-activity.php index 8d95300988..54e243e8c1 100644 --- a/app/Helpers/database/user-activity.php +++ b/app/Helpers/database/user-activity.php @@ -8,9 +8,12 @@ use App\Site\Enums\Permissions; use App\Site\Models\User; use App\Support\Cache\CacheKey; -use Illuminate\Support\Carbon; +use Carbon\Carbon; use Illuminate\Support\Facades\Cache; +/** + * @deprecated see UserActivity model + */ function getMostRecentActivity(string $user, ?int $type = null, ?int $data = null): ?array { $innerClause = "Activity.user = :user"; @@ -28,6 +31,9 @@ function getMostRecentActivity(string $user, ?int $type = null, ?int $data = nul return legacyDbFetch($query, ['user' => $user]); } +/** + * @deprecated see WriteUserActivity listener + */ function updateActivity(int $activityID): void { // Update the last update value of given activity @@ -38,18 +44,9 @@ function updateActivity(int $activityID): void legacyDbStatement($query); } -function RecentlyPostedProgressionActivity(string $user, int $gameId, int $isHardcore, int $activityType): bool -{ - $activity = UserActivityLegacy::where('User', $user) - ->where('activitytype', $activityType) - ->where('data', $gameId) - ->where('data2', $isHardcore) - ->where('lastupdate', '>=', Carbon::now()->subHours(1)) - ->first(); - - return $activity != null; -} - +/** + * @deprecated see WriteUserActivity listener + */ function postActivity(string|User $userIn, int $type, ?int $data = null, ?int $data2 = null): bool { if (!ActivityType::isValid($type)) { @@ -198,6 +195,9 @@ function postActivity(string|User $userIn, int $type, ?int $data = null, ?int $d return true; } +/** + * @deprecated see ResumePlayerSessionAction + */ function UpdateUserRichPresence(User $user, int $gameID, string $presenceMsg): void { $user->RichPresenceMsg = utf8_sanitize($presenceMsg); @@ -205,6 +205,9 @@ function UpdateUserRichPresence(User $user, int $gameID, string $presenceMsg): v $user->RichPresenceMsgDate = Carbon::now(); } +/** + * @deprecated see UserActivity model + */ function getActivityMetadata(int $activityID): ?array { $query = "SELECT * FROM Activity diff --git a/app/Platform/Actions/AddImageToGameAction.php b/app/Platform/Actions/AddImageToGame.php similarity index 98% rename from app/Platform/Actions/AddImageToGameAction.php rename to app/Platform/Actions/AddImageToGame.php index 2cc945265e..db0c5708bf 100644 --- a/app/Platform/Actions/AddImageToGameAction.php +++ b/app/Platform/Actions/AddImageToGame.php @@ -9,7 +9,7 @@ use Illuminate\Filesystem\Filesystem; use Illuminate\Http\Request; -class AddImageToGameAction +class AddImageToGame { public function __construct(private AddMediaAction $addMediaAction, private Filesystem $filesystem) { diff --git a/app/Platform/Actions/AddTriggerAction.php b/app/Platform/Actions/AddTrigger.php similarity index 91% rename from app/Platform/Actions/AddTriggerAction.php rename to app/Platform/Actions/AddTrigger.php index cc8484368d..55367b30e7 100644 --- a/app/Platform/Actions/AddTriggerAction.php +++ b/app/Platform/Actions/AddTrigger.php @@ -7,7 +7,7 @@ use App\Platform\Models\Achievement; use Illuminate\Http\Request; -class AddTriggerAction +class AddTrigger { public function execute(Request $request, Achievement $achievement, string $trigger): void { diff --git a/app/Platform/Actions/AttachPlayerGame.php b/app/Platform/Actions/AttachPlayerGame.php index 180e098b59..032d7b3256 100644 --- a/app/Platform/Actions/AttachPlayerGame.php +++ b/app/Platform/Actions/AttachPlayerGame.php @@ -13,19 +13,21 @@ class AttachPlayerGame public function execute(User $user, Game $game): Game { // upsert game attachment without running into unique constraints - /** @var ?Game $playerGame */ - $playerGame = $user->games()->find($game); + /** @var ?Game $gameWithPivot */ + $gameWithPivot = $user->games()->find($game); - if ($playerGame) { - return $playerGame; + if ($gameWithPivot) { + return $gameWithPivot; } $user->games()->attach($game); - /** @var Game $playerGame */ - $playerGame = $user->games()->find($game); + + /** @var Game $gameWithPivot */ + $gameWithPivot = $user->games()->find($game); + // let everyone know that this user started this game for first time - PlayerGameAttached::dispatch($user, $playerGame); + PlayerGameAttached::dispatch($user, $gameWithPivot); - return $playerGame; + return $gameWithPivot; } } diff --git a/app/Platform/Actions/LinkHashToGameAction.php b/app/Platform/Actions/LinkHashToGame.php similarity index 92% rename from app/Platform/Actions/LinkHashToGameAction.php rename to app/Platform/Actions/LinkHashToGame.php index 208366432b..6ccb1036c8 100644 --- a/app/Platform/Actions/LinkHashToGameAction.php +++ b/app/Platform/Actions/LinkHashToGame.php @@ -9,7 +9,7 @@ use App\Platform\Models\GameHashSet; use Illuminate\Database\Eloquent\Relations\BelongsToMany; -class LinkHashToGameAction +class LinkHashToGame { public function execute(string $hash, Game $game, ?string $gameHashTitle = null): GameHash { @@ -25,7 +25,7 @@ public function execute(string $hash, Game $game, ?string $gameHashTitle = null) */ $game->load(['gameHashSets.hashes' => function (BelongsToMany $query) use ($hash) { - $query->where('hash', $hash); + $query->where('Hash', $hash); }]); $linkedHashes = $game->gameHashSets->pluck('hashes')->collapse()->unique(); /** @var ?GameHash $linkedHash */ @@ -51,9 +51,8 @@ public function execute(string $hash, Game $game, ?string $gameHashTitle = null) $gameHashSet = $game->gameHashSets()->compatible()->first(); } - /** @var GameHash $gameHash */ $gameHash = GameHash::firstOrCreate(['hash' => $hash, 'system_id' => $game->system_id], [ - 'name' => $gameHashTitle, + 'Name' => $gameHashTitle, 'description' => $gameHashTitle, ]); $gameHashSet->hashes()->save($gameHash); diff --git a/app/Platform/Actions/LinkLatestEmulatorReleaseAction.php b/app/Platform/Actions/LinkLatestEmulatorRelease.php similarity index 91% rename from app/Platform/Actions/LinkLatestEmulatorReleaseAction.php rename to app/Platform/Actions/LinkLatestEmulatorRelease.php index ee0e027d4a..a792aa25ce 100644 --- a/app/Platform/Actions/LinkLatestEmulatorReleaseAction.php +++ b/app/Platform/Actions/LinkLatestEmulatorRelease.php @@ -6,7 +6,7 @@ use App\Platform\Models\Emulator; -class LinkLatestEmulatorReleaseAction +class LinkLatestEmulatorRelease { public function execute(Emulator $emulator): void { diff --git a/app/Platform/Actions/LinkLatestIntegrationReleaseAction.php b/app/Platform/Actions/LinkLatestIntegrationRelease.php similarity index 98% rename from app/Platform/Actions/LinkLatestIntegrationReleaseAction.php rename to app/Platform/Actions/LinkLatestIntegrationRelease.php index 1f05047553..a6c4dfbe07 100644 --- a/app/Platform/Actions/LinkLatestIntegrationReleaseAction.php +++ b/app/Platform/Actions/LinkLatestIntegrationRelease.php @@ -8,7 +8,7 @@ use Illuminate\Filesystem\Filesystem; use Spatie\MediaLibrary\MediaCollections\Models\Media; -class LinkLatestIntegrationReleaseAction +class LinkLatestIntegrationRelease { public function __construct(private Filesystem $filesystem) { diff --git a/app/Platform/Actions/ResetPlayerProgress.php b/app/Platform/Actions/ResetPlayerProgress.php new file mode 100644 index 0000000000..9434d4716b --- /dev/null +++ b/app/Platform/Actions/ResetPlayerProgress.php @@ -0,0 +1,103 @@ + $user->User]); + + $affectedGames = collect(); + $authorUsernames = collect(); + foreach ($affectedAchievements as $achievementData) { + if ($achievementData['Author'] !== $user->User) { + $authorUsernames->push($achievementData['Author']); + } + $affectedGames->push($achievementData['GameID']); + } + + if ($achievementID !== null) { + $playerAchievement = $user->playerAchievements()->where('achievement_id', $achievementID)->first(); + $achievement = $playerAchievement->achievement; + if ($playerAchievement->unlocked_hardcore_at && $achievement->isPublished) { + // resetting a hardcore unlock removes hardcore mastery badges + $user->playerBadges() + ->where('AwardType', AwardType::Mastery) + ->where('AwardData', $achievement->game_id) + ->where('AwardDataExtra', UnlockMode::Hardcore) + ->delete(); + } + $playerAchievement->delete(); + + $user->playerAchievementsLegacy()->where('AchievementID', $achievementID)->delete(); + } elseif ($gameID !== null) { + $achievementIds = Achievement::where('GameID', $gameID)->pluck('ID'); + + $user->playerAchievements() + ->whereIn('achievement_id', $achievementIds) + ->delete(); + + $user->playerAchievementsLegacy() + ->whereIn('AchievementID', $achievementIds) + ->delete(); + } else { + $user->playerAchievements()->delete(); + $user->playerAchievementsLegacy()->delete(); + } + + $authors = User::whereIn('User', $authorUsernames->unique())->get('ID'); + foreach ($authors as $author) { + dispatch(new UpdateDeveloperContributionYieldJob($author->id)); + } + + $affectedGames = $affectedGames->unique(); + foreach ($affectedGames as $affectedGameID) { + dispatch(new UpdatePlayerGameMetricsJob($user->id, $affectedGameID)); + + // force the top achievers for the game to be recalculated + expireGameTopAchievers($affectedGameID); + + // expire the cached unlocks for the game for the user + expireUserAchievementUnlocksForGame($user->User, $affectedGameID); + + // expire the cached awarded data for the user's profile + // TODO: Remove when denormalized data is ready. + expireUserCompletedGamesCacheValue($user->User); + } + + $user->save(); + } +} diff --git a/app/Platform/Actions/ResetPlayerProgressAction.php b/app/Platform/Actions/ResetPlayerProgressAction.php deleted file mode 100644 index c76e9f7507..0000000000 --- a/app/Platform/Actions/ResetPlayerProgressAction.php +++ /dev/null @@ -1,95 +0,0 @@ - $user->User]) as $row) { - if ($row['HardcoreMode']) { - $user->RAPoints -= $row['Points']; - $user->TrueRAPoints -= $row['TruePoints']; - } else { - $user->RASoftcorePoints -= $row['Points']; - } - - if ($row['Author'] !== $user->User) { - attributeDevelopmentAuthor($row['Author'], -$row['Count'], -$row['Points']); - } - - if (!in_array($row['GameID'], $affectedGames)) { - $affectedGames[] = $row['GameID']; - } - } - - $clause = ''; - if ($achievementID !== null) { - $user->playerAchievements()->where('achievement_id', $achievementID)->delete(); - // TODO one achievement reset PlayerAchievementLocked::dispatch($user, $achievementID); - - $clause = "AND AchievementID=$achievementID"; - } elseif ($gameID !== null) { - $user->playerAchievements() - ->whereIn('achievement_id', Achievement::where('GameID', $gameID)->pluck('ID')) - ->delete(); - // TODO one game reset PlayerGameProgressReset::dispatch($user, $gameID); - - $clause = "AND AchievementID IN (SELECT ID FROM Achievements WHERE GameID=$gameID)"; - } else { - $user->playerAchievements()->delete(); - // TODO multiple games reset PlayerGameProgressReset::dispatch($user, $gameIDs); - } - - legacyDbStatement("DELETE FROM Awarded WHERE User = :username $clause", ['username' => $user->User]); - - // TODO everything below should be queued based on the events dispatched above - foreach ($affectedGames as $affectedGameID) { - // delete the mastery badge (if the player had it) - $user->playerBadges() - ->where('AwardType', AwardType::Mastery) - ->where('AwardData', $affectedGameID) - ->delete(); - - // force the top achievers for the game to be recalculated - expireGameTopAchievers($affectedGameID); - - // expire the cached unlocks for the game for the user - expireUserAchievementUnlocksForGame($user->User, $affectedGameID); - - // expire the cached awarded data for the user's profile - // TODO: Remove when denormalized data is ready. - expireUserCompletedGamesCacheValue($user->User); - - // revoke beaten game awards if necessary - testBeatenGame($affectedGameID, $user->User, false); - } - - $user->save(); - } -} diff --git a/app/Platform/Actions/ResumePlayerSession.php b/app/Platform/Actions/ResumePlayerSession.php new file mode 100644 index 0000000000..3f361cd7a2 --- /dev/null +++ b/app/Platform/Actions/ResumePlayerSession.php @@ -0,0 +1,81 @@ +make(AttachPlayerGame::class); + $game = $attachPlayerGameAction->execute($user, $game); + $playerGame = $game->pivot; + $playerGame->last_played_at = $timestamp; + $playerGame->save(); + + $timestamp ??= Carbon::now(); + + // look for an active session + /** @var ?PlayerSession $playerSession */ + $playerSession = $user->playerSessions() + ->where('game_id', $game->id) + ->orderByDesc('id') + ->first(); + + if ($playerSession) { + // if the session hasn't been updated in the last 10 minutes, start a new session + if ($timestamp->diffInMinutes($playerSession->rich_presence_updated_at) < 10) { + $playerSession->duration = max(1, $timestamp->diffInMinutes($playerSession->created_at)); + if ($presence) { + $playerSession->rich_presence = $presence; + } + $playerSession->rich_presence_updated_at = $timestamp > $playerSession->rich_presence_updated_at ? $timestamp : $playerSession->rich_presence_updated_at; + $playerSession->save(['touch' => true]); + + PlayerSessionResumed::dispatch($user, $game, $presence); + + return $playerSession; + } + } + + // provide a default presence for the new session if none was provided + if (!$presence) { + $presence = 'Playing ' . $game->title; + } + + // create new session + $playerSession = new PlayerSession([ + 'user_id' => $user->id, + 'game_id' => $game->id, + // TODO add game hash set reference as soon as they are in place + // 'game_hash_id' => $game->gameHashSets()->first()->hashes()->first()->id, + // 'game_hash_set_id' => $game->gameHashSets()->first()->id, // TODO + 'rich_presence' => $presence, + 'rich_presence_updated_at' => $timestamp, + 'duration' => 1, // 1 minute is minimum duration + ]); + + $user->playerSessions()->save($playerSession); + + // TODO: store user agent + + PlayerSessionStarted::dispatch($user, $game, $presence); + + return $playerSession; + } +} diff --git a/app/Platform/Actions/ResumePlayerSessionAction.php b/app/Platform/Actions/ResumePlayerSessionAction.php deleted file mode 100644 index 356a9d629e..0000000000 --- a/app/Platform/Actions/ResumePlayerSessionAction.php +++ /dev/null @@ -1,73 +0,0 @@ -user(); - - abort_unless($user !== null, 401); - - $attachPlayerGameAction = app()->make(AttachPlayerGame::class); - $attachPlayerGameAction->execute($user, $game); - - $now = Carbon::now(); - - // look for an active session - /** @var ?PlayerSession $playerSession */ - $playerSession = $user->playerSessions()->where('game_id', $game->id)->first(); - if ($playerSession) { - // if the session hasn't been updated in the last 10 minutes, start a new session - if ($now->diffInMinutes($playerSession->updated_at) < 10) { - $playerSession->duration = $now->diffInMinutes($playerSession->created_at); - - if ($presence) { - $playerSession->rich_presence = $presence; - $playerSession->rich_presence_updated_at = $now; - } - - $playerSession->save(); - - PlayerSessionResumed::dispatch($user, $game, $presence); - - return; - } - } - - // provide a default presence for the new session if one was not provided - if (!$presence) { - $presence = 'Playing ' . $game->title; - } - - // create new session - $user->playerSessions()->save( - new PlayerSession([ - 'user_id' => $user->id, - 'game_id' => $game->id, - 'game_hash_id' => $game->gameHashSets()->first()->hashes()->first()->id, // TODO - 'game_hash_set_id' => $game->gameHashSets()->first()->id, // TODO - 'rich_presence' => $presence, - 'rich_presence_updated_at' => $now, - 'duration' => 0, - ]), - ); - - // TODO: store user agent - - PlayerSessionStarted::dispatch($user, $game, $presence); - } -} diff --git a/app/Platform/Actions/RevalidateAchievementSetBadgeEligibility.php b/app/Platform/Actions/RevalidateAchievementSetBadgeEligibility.php new file mode 100644 index 0000000000..6efd60cded --- /dev/null +++ b/app/Platform/Actions/RevalidateAchievementSetBadgeEligibility.php @@ -0,0 +1,180 @@ +user) { + return; + } + + $this->revalidateBeatenBadgeEligibility($playerGame); + $this->revalidateCompletionBadgeEligibility($playerGame); + } + + private function revalidateBeatenBadgeEligibility(PlayerGame $playerGame): void + { + $badge = $playerGame->user->playerBadges() + ->where('AwardType', AwardType::GameBeaten) + ->where('AwardData', $playerGame->game->id); + $softcoreBadge = (clone $badge)->where('AwardDataExtra', UnlockMode::Softcore); + $hardcoreBadge = (clone $badge)->where('AwardDataExtra', UnlockMode::Hardcore); + + if ($playerGame->beaten_at === null && $softcoreBadge->exists()) { + PlayerBadgeLost::dispatch($softcoreBadge->first()); + $softcoreBadge->delete(); + } + + if ($playerGame->beaten_hardcore_at === null && $hardcoreBadge->exists()) { + PlayerBadgeLost::dispatch($hardcoreBadge->first()); + $hardcoreBadge->delete(); + } + + if ($playerGame->beaten_hardcore_at === null && $playerGame->beaten_at !== null && !$softcoreBadge->exists()) { + $badge = AddSiteAward( + $playerGame->user->username, + AwardType::GameBeaten, + $playerGame->game->id, + UnlockMode::Softcore, + $playerGame->beaten_at, + displayOrder: 0 + ); + PlayerBadgeAwarded::dispatch($badge); + PlayerGameBeaten::dispatch($playerGame->user, $playerGame->game); + } + + if ($playerGame->beaten_hardcore_at !== null && !$hardcoreBadge->exists()) { + $softcoreBadge->delete(); + + $badge = AddSiteAward( + $playerGame->user->username, + AwardType::GameBeaten, + $playerGame->game->id, + UnlockMode::Hardcore, + $playerGame->beaten_hardcore_at, + displayOrder: 0 + ); + PlayerBadgeAwarded::dispatch($badge); + PlayerGameBeaten::dispatch($playerGame->user, $playerGame->game, true); + + if ($playerGame->beaten_hardcore_at->gte(Carbon::now()->subMinutes(10))) { + static_addnewhardcoregamebeaten($playerGame->game->id, $playerGame->user->username); + } + } + } + + private function revalidateCompletionBadgeEligibility(PlayerGame $playerGame): void + { + $badge = $playerGame->user->playerBadges() + ->where('AwardType', AwardType::Mastery) + ->where('AwardData', $playerGame->game->id); + $softcoreBadge = (clone $badge)->where('AwardDataExtra', UnlockMode::Softcore); + $hardcoreBadge = (clone $badge)->where('AwardDataExtra', UnlockMode::Hardcore); + + if ($playerGame->completed_at === null && $softcoreBadge->exists()) { + // if the user has at least one unlock for the set, assume there was + // a revision and do nothing. if they want to get rid of the badge, + // they can reset one or more of the achievements they have. + if (!$playerGame->achievements_unlocked && $playerGame->achievements_total) { + PlayerBadgeLost::dispatch($softcoreBadge->first()); + $softcoreBadge->delete(); + } + } + + if ($playerGame->completed_hardcore_at === null && $hardcoreBadge->exists()) { + // user has no achievements for the set. if the set is empty, assume it + // was demoted and keep the badge, otherwise assume they did a full reset + // and destroy the badge. + if (!$playerGame->achievements_unlocked && !$playerGame->achievements_unlocked_hardcore && $playerGame->achievements_total) { + PlayerBadgeLost::dispatch($hardcoreBadge->first()); + $hardcoreBadge->delete(); + } + } + + if ($playerGame->achievements_total < PlayerBadge::MINIMUM_ACHIEVEMENTS_COUNT_FOR_MASTERY) { + if ($softcoreBadge->exists()) { + PlayerBadgeLost::dispatch($softcoreBadge->first()); + $softcoreBadge->delete(); + } + if ($hardcoreBadge->exists()) { + PlayerBadgeLost::dispatch($hardcoreBadge->first()); + $hardcoreBadge->delete(); + } + + return; + } + + if ($playerGame->completed_hardcore_at === null && $playerGame->completed_at !== null && !$softcoreBadge->exists()) { + $badge = AddSiteAward( + $playerGame->user->username, + AwardType::Mastery, + $playerGame->game->id, + UnlockMode::Softcore, + $playerGame->completed_at, + displayOrder: 0 + ); + PlayerBadgeAwarded::dispatch($badge); + PlayerGameCompleted::dispatch($playerGame->user, $playerGame->game); + + // TODO WriteUserActivity + $recentActivity = $playerGame->user->legacyActivities() + ->where('activitytype', ActivityType::CompleteGame) + ->where('data', $playerGame->game->id) + ->where('data2', UnlockMode::Softcore) + ->where('lastupdate', '>=', Carbon::now()->subHour()) + ->first(); + if ($recentActivity === null) { + postActivity($playerGame->user->username, ActivityType::CompleteGame, $playerGame->game->id, UnlockMode::Softcore); + } + } + + if ($playerGame->completed_hardcore_at !== null && !$hardcoreBadge->exists()) { + $softcoreBadge->delete(); + + $badge = AddSiteAward( + $playerGame->user->username, + AwardType::Mastery, + $playerGame->game->id, + UnlockMode::Hardcore, + $playerGame->completed_hardcore_at, + displayOrder: 0 + ); + PlayerBadgeAwarded::dispatch($badge); + PlayerGameCompleted::dispatch($playerGame->user, $playerGame->game, true); + + // TODO WriteUserActivity + $recentActivity = $playerGame->user->legacyActivities() + ->where('activitytype', ActivityType::CompleteGame) + ->where('data', $playerGame->game->id) + ->where('data2', UnlockMode::Hardcore) + ->where('lastupdate', '>=', Carbon::now()->subHour()) + ->first(); + if ($recentActivity === null) { + postActivity($playerGame->user->username, ActivityType::CompleteGame, $playerGame->game->id, UnlockMode::Hardcore); + } + + if ($playerGame->completed_hardcore_at->gte(Carbon::now()->subMinutes(10))) { + static_addnewhardcoremastery($playerGame->game->id, $playerGame->user->username); + } + + expireGameTopAchievers($playerGame->game->id); + } + } +} diff --git a/app/Platform/Actions/UnlockPlayerAchievement.php b/app/Platform/Actions/UnlockPlayerAchievement.php new file mode 100644 index 0000000000..860aa42408 --- /dev/null +++ b/app/Platform/Actions/UnlockPlayerAchievement.php @@ -0,0 +1,85 @@ +loadMissing('game'); + if (!$achievement->game) { + throw new Exception('Achievement does not belong to any game'); + } + + if ($unlockedBy) { + // only attach the game if it's a manual unlock + $attachPlayerGameAction = app()->make(AttachPlayerGame::class); + $attachPlayerGameAction->execute($user, $achievement->game); + } else { + // make sure to resume the player session which will attach the game to the player, too + $playerSession = app()->make(ResumePlayerSession::class) + ->execute($user, $achievement->game, timestamp: $timestamp); + } + + $unlock = $user->playerAchievements()->firstOrCreate([ + 'achievement_id' => $achievement->id, + // TODO 'trigger_id' => assume trigger_id from most recent version of the achievement trigger + ]); + + // determine if the unlock needs to occur + $alreadyUnlockedInThisMode = false; + if ($hardcore) { + if ($unlock->unlocked_hardcore_at !== null) { + $alreadyUnlockedInThisMode = true; + } else { + $unlock->unlocked_hardcore_at = $timestamp; + + if ($unlock->wasRecentlyCreated) { + $unlock->unlocked_at = $unlock->unlocked_hardcore_at; + } + } + } else { + if (!$unlock->wasRecentlyCreated) { + $alreadyUnlockedInThisMode = true; + } else { + $unlock->unlocked_at = $timestamp; + } + } + + if ($alreadyUnlockedInThisMode) { + return; + } + + // set the unlocked_by, reset if unlocked by player + $unlock->unlocker_id = $unlockedBy?->id; + + // attach latest player session if it was not a manual unlock + if (!$unlockedBy) { + $unlock->player_session_id = $playerSession->id; + + $playerSession->hardcore = $playerSession->hardcore ?: (bool) $unlock->unlocked_hardcore_at; + $playerSession->save(); + } + + // commit the unlock + $unlock->save(); + + // post the unlock notification + PlayerAchievementUnlocked::dispatch($user, $achievement, $hardcore); + } +} diff --git a/app/Platform/Actions/UnlockPlayerAchievementAction.php b/app/Platform/Actions/UnlockPlayerAchievementAction.php deleted file mode 100644 index e2af4fea79..0000000000 --- a/app/Platform/Actions/UnlockPlayerAchievementAction.php +++ /dev/null @@ -1,129 +0,0 @@ - $achievement->id]; - - $unlock = $user->playerAchievements()->where('achievement_id', $achievement->id)->first(); - if ($unlock == null) { - $unlock = new PlayerAchievement([ - 'user_id' => $user->id, - 'achievement_id' => $achievement->id, - // TODO assume trigger_id from most recent version of the achievement trigger - // TODO 'trigger_id' => ??? - ]); - $user->playerAchievements()->save($unlock); - } - - // determine if the unlock needs to occur and update the player's score accordingly - if ($hardcore) { - if ($unlock->unlocked_hardcore_at != null) { - $alreadyUnlocked = true; - } else { - $user->points_total += $achievement->points; - $unlock->unlocked_hardcore_at = Carbon::now(); - - if ($unlock->unlocked_at == null) { - $user->points_total += $achievement->points; - $unlock->unlocked_at = $unlock->unlocked_hardcore_at; - } - } - } else { - if ($unlock->unlocked_at != null) { - $alreadyUnlocked = true; - } else { - $user->points_total += $achievement->points; - $unlock->unlocked_at = Carbon::now(); - } - } - - if (!$alreadyUnlocked) { - // set the unlocked_by or associate to the player's session - if ($unlockedBy) { - $unlock->unlocker_id = $unlockedBy->id; - } else { - $playerSession = $user->playerSessions()->where('game_id', $achievement->game_id)->first(); - if ($playerSession) { - $unlock->player_session_id = $playerSession->id; - } - } - - // commit the unlock - $unlock->save(); - - // post the unlock notification - PlayerAchievementUnlocked::dispatch($user, $achievement, $hardcore); - - /* - * TODO: adjust retro ratio for user -> queue job via event - */ - - // commit the changes to the user's score - $user->save(); - } - - $response['pointsTotal'] = $user->points_total; - - /** @var ?Game $game */ - $game = Game::find($achievement->game_id); - if ($game) { - // determine how many achievements are still needed for the user to complete/master the set - $achievementIds = []; - foreach ($game->achievements()->get() as $game_achievement) { - /* - * TODO: only capture Core achievements - */ - $achievementIds[] = $game_achievement->id; - } - $coreCount = count($achievementIds); - - $userUnlocks = $user->playerAchievements()->whereIn('achievement_id', $achievementIds); - - if ($hardcore) { - $response['achievementsRemaining'] = $coreCount - $userUnlocks->whereNotNull('unlocked_hardcore_at')->count(); - } else { - $response['achievementsRemaining'] = $coreCount - $userUnlocks->whereNotNull('unlocked_at')->count(); - } - } - - if ($alreadyUnlocked) { - if ($hardcore) { - $response['error'] = 'User already has hardcore and regular achievements awarded.'; - } else { - $response['error'] = 'User already has this achievement awarded.'; - } - - $response['success'] = false; - - return $response; - } - - // if the set has been completed, post the mastery notification - if ($game && $response['achievementsRemaining'] == 0) { - AchievementSetCompleted::dispatch($user, $game, $hardcore); - } - - /* - * TODO: count unlock for achievement author -> queue job via event - */ - - return $response; - } -} diff --git a/app/Platform/Actions/UpdateDeveloperContributionYield.php b/app/Platform/Actions/UpdateDeveloperContributionYield.php new file mode 100644 index 0000000000..a795241bf7 --- /dev/null +++ b/app/Platform/Actions/UpdateDeveloperContributionYield.php @@ -0,0 +1,171 @@ +username; + + $points = 0; + $pointLevel = 0; + $nextPointThreshold = PlayerBadge::getBadgeThreshold(AwardType::AchievementPointsYield, $pointLevel); + + $count = 0; + $countLevel = 0; + $nextCountThreshold = PlayerBadge::getBadgeThreshold(AwardType::AchievementUnlocksYield, $countLevel); + + // get all unlocks for achievements created by the user ordered by date + $unlocks = PlayerAchievementLegacy::select('Awarded.Date', DB::raw('MAX(Awarded.HardcoreMode)'), 'Achievements.Points') + ->leftJoin('Achievements', 'Achievements.ID', '=', 'Awarded.AchievementID') + ->where('Achievements.Author', '=', $username) + ->where('Awarded.User', '!=', $username) + ->where('Achievements.Flags', '=', AchievementFlag::OfficialCore) + ->groupBy(['Awarded.User', 'Awarded.AchievementID']) + ->orderBy('Awarded.Date') + ->get(); + + /** @var PlayerAchievementLegacy $unlock */ + foreach ($unlocks as $unlock) { + // when a threshold is crossed, award a badge + $count++; + if ($count === $nextCountThreshold) { + PlayerBadge::upsert( + [ + [ + 'User' => $username, + 'AwardType' => AwardType::AchievementUnlocksYield, + 'AwardData' => $countLevel, + 'AwardDate' => $unlock->Date, + ], + ], + ['User', 'AwardType', 'AwardData'], + ['AwardDate'] + ); + + // TODO SiteBadgeAwarded::dispatch($badge); + + $countLevel++; + + $nextCountThreshold = PlayerBadge::getBadgeThreshold(AwardType::AchievementUnlocksYield, $countLevel); + } + + $points += $unlock['Points']; + if ($points >= $nextPointThreshold) { + PlayerBadge::upsert( + [ + [ + 'User' => $username, + 'AwardType' => AwardType::AchievementPointsYield, + 'AwardData' => $pointLevel, + 'AwardDate' => $unlock->Date, + ], + ], + ['User', 'AwardType', 'AwardData'], + ['AwardDate'] + ); + + // TODO SiteBadgeAwarded::dispatch($badge); + + $pointLevel++; + + $nextPointThreshold = PlayerBadge::getBadgeThreshold(AwardType::AchievementPointsYield, $pointLevel); + if ($nextPointThreshold == 0) { + // if we run out of tiers, getBadgeThreshold returns 0, so everything will be >=. set to MAXINT + $nextPointThreshold = PHP_INT_MAX; + } + } + } + + // remove any extra badge tiers + PlayerBadge::where('User', '=', $username) + ->where('AwardType', '=', AwardType::AchievementUnlocksYield) + ->where('AwardData', '>=', $countLevel) + ->delete(); + + PlayerBadge::where('User', '=', $username) + ->where('AwardType', '=', AwardType::AchievementPointsYield) + ->where('AwardData', '>=', $pointLevel) + ->delete(); + + // update the denormalized data + User::where('User', '=', $username) + ->update(['ContribCount' => $count, 'ContribYield' => $points]); + } + + /** + * @deprecated TODO still needed? + */ + public function attributeDevelopmentAuthor(string $author, int $count, int $points): void + { + $user = User::firstWhere('User', $author); + if ($user === null) { + return; + } + + $oldContribCount = $user->ContribCount; + $oldContribYield = $user->ContribYield; + + // use raw statement to perform atomic update + legacyDbStatement("UPDATE UserAccounts SET ContribCount = ContribCount + $count," . + " ContribYield = ContribYield + $points WHERE User=:user", ['user' => $author]); + + $newContribTier = PlayerBadge::getNewBadgeTier(AwardType::AchievementUnlocksYield, $oldContribCount, $oldContribCount + $count); + if ($newContribTier !== null) { + $badge = AddSiteAward($author, AwardType::AchievementUnlocksYield, $newContribTier); + } + + $newPointsTier = PlayerBadge::getNewBadgeTier(AwardType::AchievementPointsYield, $oldContribYield, $oldContribYield + $points); + if ($newPointsTier !== null) { + $badge = AddSiteAward($author, AwardType::AchievementPointsYield, $newPointsTier); + } + } + + /** + * @deprecated TODO still needed? + */ + public function recalculateDeveloperContribution(string $author): void + { + sanitize_sql_inputs($author); + + $query = "SELECT COUNT(*) AS ContribCount, SUM(Points) AS ContribYield + FROM (SELECT aw.User, ach.ID, MAX(aw.HardcoreMode) as HardcoreMode, ach.Points + FROM Achievements ach LEFT JOIN Awarded aw ON aw.AchievementID=ach.ID + WHERE ach.Author='$author' AND aw.User != '$author' + AND ach.Flags=" . AchievementFlag::OfficialCore . " + GROUP BY 1,2) AS UniqueUnlocks"; + + $dbResult = s_mysql_query($query); + if ($dbResult !== false) { + $contribCount = 0; + $contribYield = 0; + + if ($data = mysqli_fetch_assoc($dbResult)) { + $contribCount = $data['ContribCount'] ?? 0; + $contribYield = $data['ContribYield'] ?? 0; + } + + $query = "UPDATE UserAccounts + SET ContribCount = $contribCount, ContribYield = $contribYield + WHERE User = '$author'"; + + $dbResult = s_mysql_query($query); + if (!$dbResult) { + log_sql_fail(); + } + } + } +} diff --git a/app/Platform/Actions/UpdateGameAchievementsMetrics.php b/app/Platform/Actions/UpdateGameAchievementsMetrics.php new file mode 100644 index 0000000000..6222e827dc --- /dev/null +++ b/app/Platform/Actions/UpdateGameAchievementsMetrics.php @@ -0,0 +1,60 @@ +id); + if (config('feature.aggregate_queries')) { + $parentGame = $parentGameId ? Game::find($parentGameId) : null; + $playersTotal = $parentGame ? $parentGame->players_total : $game->players_total; + $playersHardcore = $parentGame ? $parentGame->players_hardcore : $game->players_hardcore; + } else { + $playersTotal = getTotalUniquePlayers($game->id, $parentGameId); + $playersHardcore = getTotalUniquePlayers($game->id, $parentGameId, null, true); + } + + // force all unachieved to be 1 + $playersHardcoreCalc = $playersHardcore ?: 1; + $pointsWeightedTotal = 0; + $achievements = $game->achievements()->published()->get(); + foreach ($achievements as $achievement) { + $unlocksCount = $achievement->playerAchievements() + ->leftJoin('UserAccounts as user', 'user.ID', '=', 'player_achievements.user_id') + ->where('user.Untracked', false) + ->count(); + $unlocksHardcoreCount = $achievement->playerAchievements() + ->leftJoin('UserAccounts as user', 'user.ID', '=', 'player_achievements.user_id') + ->where('user.Untracked', false) + ->whereNotNull('unlocked_hardcore_at') + ->count(); + + // force all unachieved to be 1 + $unlocksHardcoreCalc = $unlocksHardcoreCount ?: 1; + $weight = 0.4; + $pointsWeighted = (int) ($achievement->points * (1 - $weight)) + ($achievement->points * (($playersHardcoreCalc / $unlocksHardcoreCalc) * $weight)); + $pointsWeightedTotal += $pointsWeighted; + + $achievement->unlocks_total = $unlocksCount; + $achievement->unlocks_hardcore_total = $unlocksHardcoreCount; + $achievement->unlock_percentage = $playersTotal ? $unlocksCount / $playersTotal : 0; + $achievement->unlock_hardcore_percentage = $playersHardcore ? $unlocksHardcoreCount / $playersHardcore : 0; + $achievement->TrueRatio = $pointsWeighted; + $achievement->save(); + } + + $game->TotalTruePoints = $pointsWeightedTotal; + $game->save(); + + // TODO GameAchievementSetMetricsUpdated::dispatch($game); + } +} diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php new file mode 100644 index 0000000000..302abb4e14 --- /dev/null +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -0,0 +1,114 @@ +achievements_published = $game->achievements()->published()->count(); + $game->achievements_unpublished = $game->achievements()->unpublished()->count(); + + // update achievements version by changed hash + // if ($game->attributes['achievements_total']) { + // $game->attributes['achievements_version_hash'] = md5($publishedAchievements->implode('trigger')); + // if ($game->isDirty('achievements_version_hash')) { + // $game->attributes['achievements_version'] = $game->achievements_version + 1; + // } + // } + + $game->points_total = $game->achievements()->published()->sum('points'); + // NOTE $game->TotalTruePoints are updated separately + + // TODO switch as soon as all player_games have been populated + // $game->players_total = $game->playerGames() + // ->leftJoin('UserAccounts as user', 'user.ID', '=', 'player_games.user_id') + // ->where('player_games.achievements_unlocked', '>', 0) + // ->where('user.Untracked', false) + // ->count(); + // $game->players_hardcore = $game->playerGames() + // ->leftJoin('UserAccounts as user', 'user.ID', '=', 'player_games.user_id') + // ->where('player_games.achievements_unlocked_hardcore', '>', 0) + // ->where('user.Untracked', false) + // ->count(); + $parentGameId = getParentGameIdFromGameId($game->id); + $game->players_total = getTotalUniquePlayers($game->id, $parentGameId); + $game->players_hardcore = getTotalUniquePlayers($game->id, $parentGameId, null, true); + + $achievementSetVersionChanged = false; + $achievementsPublishedChange = 0; + $pointsTotalChange = 0; + $playersTotalChange = 0; + $playersHardcoreChange = 0; + if ($game->achievements_published || $game->achievements_unpublished) { + $versionHashFields = ['ID', 'MemAddr', 'type', 'Points']; + $achievementSetVersionHashPayload = $game->achievements()->published() + ->orderBy('ID') + ->get($versionHashFields) + ->map(fn ($achievement) => implode('-', $achievement->only($versionHashFields))) + ->implode('-'); + $game->achievement_set_version_hash = hash('sha256', $achievementSetVersionHashPayload); + + $achievementSetVersionChanged = $game->isDirty('achievement_set_version_hash'); + $achievementsPublishedChange = $game->achievements_published - $game->getOriginal('achievements_published'); + $pointsTotalChange = $game->points_total - $game->getOriginal('points_total'); + $playersTotalChange = $game->players_total - $game->getOriginal('players_total'); + $playersHardcoreChange = $game->players_hardcore - $game->getOriginal('players_hardcore'); + } + $pointsWeightedBeforeUpdate = $game->TotalTruePoints; + + $game->save(); + + // dispatch(new UpdateGameAchievementsMetricsJob($game->id))->onQueue('game-metrics'); + app()->make(UpdateGameAchievementsMetrics::class) + ->execute($game); + $game->refresh(); + $pointsWeightedChange = $game->TotalTruePoints - $pointsWeightedBeforeUpdate; + + GameMetricsUpdated::dispatch($game); + + // TODO dispatch events for achievement set and game metrics changes + $tmp = $achievementsPublishedChange; + $tmp = $pointsTotalChange; + $tmp = $pointsWeightedChange; + $tmp = $playersTotalChange; + $tmp = $playersHardcoreChange; + + // ad-hoc updates for player games, so they can be updated the next time a player updates their profile + // Note that those might be multiple thousand entries depending on a game's players count + + $updateReason = null; + if ($pointsWeightedChange) { + $updateReason = 'weighted_points_outdated'; + } + if ($achievementSetVersionChanged) { + $updateReason = 'version_mismatch'; + } + if ($updateReason) { + $game->playerGames() + ->where(function ($query) use ($game, $updateReason) { + if ($updateReason === 'weighted_points_outdated') { + $query->whereNot('points_weighted_total', '=', $game->TotalTruePoints) + ->orWhereNull('points_weighted_total'); + } + if ($updateReason === 'version_mismatch') { + $query->whereNot('achievement_set_version_hash', '=', $game->achievement_set_version_hash) + ->orWhereNull('achievement_set_version_hash'); + } + }) + ->update([ + 'update_status' => $updateReason, + 'achievements_total' => $game->achievements_published, + 'points_total' => $game->points_total, + 'points_weighted_total' => $game->TotalTruePoints, + ]); + } + } +} diff --git a/app/Platform/Actions/UpdateGameMetricsAction.php b/app/Platform/Actions/UpdateGameMetricsAction.php deleted file mode 100644 index b8ec0bce32..0000000000 --- a/app/Platform/Actions/UpdateGameMetricsAction.php +++ /dev/null @@ -1,50 +0,0 @@ -achievements_total = $this->achievements()->count(); - - /* - * update published achievements metrics - */ - // $publishedAchievements = $this->achievements()->published(); - // $this->attributes['achievements_published'] = $publishedAchievements->count(); - - /* - * fetch achievements data - */ - // $publishedAchievements = $publishedAchievements->get(['trigger', 'points', 'points_ratio']); - - /* - * update achievements version by changed hash - */ - // if ($this->attributes['achievements_total']) { - // $this->attributes['achievements_version_hash'] = md5($publishedAchievements->implode('trigger')); - // if ($this->isDirty('achievements_version_hash')) { - // $this->attributes['achievements_version'] = $this->achievements_version + 1; - // } - // } - - // $this->attributes['points_total'] = $publishedAchievements->sum('points'); - // $this->attributes['points_weighted'] = $publishedAchievements->sum('points_ratio'); - - /* - * update unpublished achievements metrics - */ - // $this->attributes['achievements_unpublished'] = $this->achievements()->unpublished()->count(); - - // $this->attributes['players_total'] = $this->players()->count(); - - // $game->save(); - } -} diff --git a/app/Platform/Actions/UpdateGameWeightedPoints.php b/app/Platform/Actions/UpdateGameWeightedPoints.php deleted file mode 100644 index a7a2f9a730..0000000000 --- a/app/Platform/Actions/UpdateGameWeightedPoints.php +++ /dev/null @@ -1,65 +0,0 @@ -game; + $user = $playerGame->user; + + if (!$user) { + return; + } + + // TODO use pre-aggregated values instead of fetching models + // TODO $game->achievements_published + // TODO $game->points_total + // TODO $game->TotalTruePoints + $achievements = $game->achievements()->published()->get(); + $achievementsTotal = $achievements->count(); + $pointsTotal = $achievements->sum('Points'); + $pointsWeightedTotal = $achievements->sum('TrueRatio'); + + $achievementsUnlocked = $user->achievements()->where('GameID', $game->id) + ->published() + ->withPivot([ + 'unlocked_at', + 'unlocked_hardcore_at', + ]) + ->get(); + $achievementsUnlockedHardcore = $achievementsUnlocked->filter(fn (Achievement $achievement) => $achievement->pivot->unlocked_hardcore_at !== null); + + $points = $achievementsUnlocked->sum('Points'); + $pointsHardcore = $achievementsUnlockedHardcore->sum('Points'); + $pointsWeighted = $achievementsUnlocked->sum('TrueRatio'); + + $playerAchievements = $achievementsUnlocked->pluck('pivot'); + $playerAchievementsHardcore = $playerAchievements->whereNotNull('unlocked_hardcore_at'); + $achievementsUnlockedCount = $playerAchievements->count(); + $achievementsUnlockedHardcoreCount = $playerAchievementsHardcore->count(); + + $firstUnlockAt = $playerAchievements->min('unlocked_at'); + $lastUnlockAt = $playerAchievements->max('unlocked_at'); + + $firstUnlockHardcoreAt = $playerAchievementsHardcore->min('unlocked_hardcore_at'); + $lastUnlockHardcoreAt = $playerAchievementsHardcore->max('unlocked_hardcore_at'); + + // Coalesce dates to existing values or unlock dates + + /** @var ?Carbon $startedAt */ + $startedAt = $playerGame->created_at !== null + ? min($firstUnlockAt, $playerGame->created_at) + : $firstUnlockAt; + + $createdAt = $playerGame->created_at !== null + ? $playerGame->created_at + : $startedAt; + + $lastPlayedAt = $playerAchievements->pluck('unlocked_at') + ->merge($playerAchievementsHardcore->pluck('unlocked_hardcore_at')) + ->add($playerGame->last_played_at) + ->filter() + ->max(); + + $timeTaken = $startedAt ? $startedAt->diffInSeconds($lastPlayedAt) : $playerGame->time_taken; + $timeTakenHardcore = $startedAt ? $startedAt->diffInSeconds($lastPlayedAt) : $playerGame->time_taken_hardcore; + + $playerGame->fill([ + 'update_status' => null, // reset previously added update reason + 'achievement_set_version_hash' => $game->achievement_set_version_hash, + 'achievements_total' => $achievementsTotal, + 'achievements_unlocked' => $achievementsUnlockedCount, + 'achievements_unlocked_hardcore' => $achievementsUnlockedHardcoreCount, + 'last_played_at' => $lastPlayedAt, + // 'playtime_total' => $playtimeTotal, + 'time_taken' => $timeTaken, + 'time_taken_hardcore' => $timeTakenHardcore, + 'last_unlock_at' => $lastUnlockAt, + 'last_unlock_hardcore_at' => $lastUnlockHardcoreAt, + 'first_unlock_at' => $firstUnlockAt, + 'first_unlock_hardcore_at' => $firstUnlockHardcoreAt, + 'points_total' => $pointsTotal, + 'points' => $points, + 'points_hardcore' => $pointsHardcore, + 'points_weighted_total' => $pointsWeightedTotal, + 'points_weighted' => $pointsWeighted, + 'created_at' => $createdAt, + ]); + + $playerGame->fill($this->beatProgressMetrics($playerGame, $achievementsUnlocked, $achievements)); + $playerGame->fill($this->completionProgressMetrics($playerGame)); + + $playerGame->save(); + + PlayerGameMetricsUpdated::dispatch($user, $game); + + app()->make(RevalidateAchievementSetBadgeEligibility::class)->execute($playerGame); + + expireGameTopAchievers($playerGame->game->id); + } + + /** + * @param Collection $achievementsUnlocked + * @param Collection $achievements + */ + public function beatProgressMetrics(PlayerGame $playerGame, Collection $achievementsUnlocked, Collection $achievements): array + { + $totalProgressions = $achievements->where('type', AchievementType::Progression)->count(); + $totalWinConditions = $achievements->where('type', AchievementType::WinCondition)->count(); + + // If the game has no beaten-tier achievements assigned, it is not considered beatable. + // Bail. + if (!$totalProgressions && !$totalWinConditions) { + return [ + 'achievements_beat' => null, + 'achievements_beat_unlocked' => null, + 'achievements_beat_unlocked_hardcore' => null, + 'beaten_percentage' => null, + 'beaten_percentage_hardcore' => null, + 'beaten_at' => null, + 'beaten_hardcore_at' => null, + ]; + } + + $progressionUnlocks = $achievementsUnlocked->where('type', AchievementType::Progression)->pluck('pivot'); + $progressionUnlocksHardcore = $progressionUnlocks->filter(fn (PlayerAchievement $playerAchievement) => $playerAchievement->unlocked_hardcore_at !== null); + $winConditionUnlocks = $achievementsUnlocked->where('type', AchievementType::WinCondition)->pluck('pivot'); + $winConditionUnlocksHardcore = $winConditionUnlocks->filter(fn (PlayerAchievement $playerAchievement) => $playerAchievement->unlocked_hardcore_at !== null); + $progressionUnlocksSoftcoreCount = $progressionUnlocks->count(); + $progressionUnlocksHardcoreCount = $progressionUnlocksHardcore->count(); + $winConditionUnlocksSoftcoreCount = $winConditionUnlocks->count(); + $winConditionUnlocksHardcoreCount = $winConditionUnlocksHardcore->count(); + + // If there are no Win Condition achievements in the set, the game is considered beaten + // if the user unlocks all the progression achievements. + $neededWinConditionAchievements = $totalWinConditions >= 1 ? 1 : 0; + + $isBeatenSoftcore = + $progressionUnlocksSoftcoreCount === $totalProgressions + && $winConditionUnlocksSoftcoreCount >= $neededWinConditionAchievements; + + $isBeatenHardcore = + $progressionUnlocksHardcoreCount === $totalProgressions + && $winConditionUnlocksHardcoreCount >= $neededWinConditionAchievements; + + $beatAchievements = $totalProgressions + $neededWinConditionAchievements; + $beatAchievementsUnlockedCount = $progressionUnlocksSoftcoreCount + min($neededWinConditionAchievements, $winConditionUnlocksSoftcoreCount); + $beatAchievementsUnlockedHardcoreCount = $progressionUnlocksHardcoreCount + min($neededWinConditionAchievements, $winConditionUnlocksHardcoreCount); + + $beatenAt = $isBeatenSoftcore ? $playerGame->beaten_at : null; + $beatenHardcoreAt = $isBeatenHardcore ? $playerGame->beaten_hardcore_at : null; + $beatenDates = $playerGame->beaten_dates; + $beatenDatesHardcore = $playerGame->beaten_dates_hardcore; + if (!$beatenAt && $isBeatenSoftcore) { + $beatenAt = collect([ + $progressionUnlocks->max('unlocked_at'), + $winConditionUnlocks->min('unlocked_at'), + ]) + ->filter() + ->max(); + $beatenDates = (new Collection($beatenDates))->push($beatenAt); + } + if (!$beatenHardcoreAt && $isBeatenHardcore) { + $beatenHardcoreAt = collect([ + $progressionUnlocksHardcore->max('unlocked_hardcore_at'), + $winConditionUnlocksHardcore->min('unlocked_hardcore_at'), + ]) + ->filter() + ->max(); + $beatenDatesHardcore = (new Collection($beatenDates))->push($beatenHardcoreAt); + } + + return [ + 'achievements_beat' => $beatAchievements, + 'achievements_beat_unlocked' => $beatAchievementsUnlockedCount, + 'achievements_beat_unlocked_hardcore' => $beatAchievementsUnlockedHardcoreCount, + 'beaten_percentage' => $beatAchievements ? $beatAchievementsUnlockedCount / $beatAchievements : null, + 'beaten_percentage_hardcore' => $beatAchievements ? $beatAchievementsUnlockedHardcoreCount / $beatAchievements : null, + 'beaten_dates' => $beatenDates, + 'beaten_dates_hardcore' => $beatenDatesHardcore, + 'beaten_at' => $beatenAt, + 'beaten_hardcore_at' => $beatenHardcoreAt, + ]; + } + + public function completionProgressMetrics(PlayerGame $playerGame): array + { + if (!$playerGame->achievements_total) { + return [ + 'completed_at' => null, + 'completed_hardcore_at' => null, + 'completion_percentage' => null, + 'completion_percentage_hardcore' => null, + ]; + } + + $isCompleted = $playerGame->achievements_unlocked === $playerGame->achievements_total; + $isCompletedHardcore = $playerGame->achievements_unlocked_hardcore === $playerGame->achievements_total; + + $completedAt = $isCompleted ? $playerGame->completed_at : null; + $completedHardcoreAt = $isCompletedHardcore ? $playerGame->completed_hardcore_at : null; + $completionDates = $playerGame->completion_dates; + $completionDatesHardcore = $playerGame->completion_dates_hardcore; + if ($isCompleted && !$completedAt) { + $completedAt = $playerGame->last_unlock_at; + $completionDates = (new Collection($completionDates)) + ->push($completedAt); + } + if ($isCompletedHardcore && !$completedHardcoreAt) { + $completedHardcoreAt = $playerGame->last_unlock_hardcore_at; + $completionDatesHardcore = (new Collection($completionDates)) + ->push($completedHardcoreAt); + } + + return [ + 'completion_dates' => $completionDates, + 'completion_dates_hardcore' => $completionDatesHardcore, + 'completed_at' => $completedAt, + 'completed_hardcore_at' => $completedHardcoreAt, + 'completion_percentage' => $playerGame->achievements_total ? $playerGame->achievements_unlocked / $playerGame->achievements_total : null, + 'completion_percentage_hardcore' => $playerGame->achievements_total ? $playerGame->achievements_unlocked_hardcore / $playerGame->achievements_total : null, + ]; + + // TODO check progress and dispatch completion events if applicable + // $justCompleted = false; + // if ($justCompleted) { + // $completionDates = new Collection($playerGame->completion_dates); + // } + // if the set has been completed, post the mastery notification + // if ($game && $response['achievementsRemaining'] == 0) { + // AchievementSetCompleted::dispatch($user, $game, $hardcore); + // } + } +} diff --git a/app/Platform/Actions/UpdatePlayerGameMetricsAction.php b/app/Platform/Actions/UpdatePlayerGameMetricsAction.php deleted file mode 100644 index f6a51c4397..0000000000 --- a/app/Platform/Actions/UpdatePlayerGameMetricsAction.php +++ /dev/null @@ -1,114 +0,0 @@ -game; - - /** @var User $game */ - $user = $playerGame->user; - - $unlockedAchievements = $user->achievements()->where('GameID', $game->ID) - ->withPivot([ - 'unlocked_at', - 'unlocked_hardcore_at', - ]) - ->get(); - - $playerAchievements = $unlockedAchievements->pluck('pivot'); - $playerAchievementsHardcore = $playerAchievements->whereNotNull('unlocked_hardcore_at'); - - // TODO use pre-aggregated values instead of fetching models - $achievements = $game->achievements()->published()->get(); - // TODO $game->achievements_published - $achievementsTotal = $achievements->count(); - // TODO $game->points_total - $pointsTotal = $achievements->sum('Points'); - // TODO $game->TotalTruePoints - $pointsWeightedTotal = $achievements->sum('TrueRatio'); - - $points = $unlockedAchievements->sum('Points'); - $pointsWeighted = $unlockedAchievements->sum('TrueRatio'); - - $achievementsUnlockedCount = $playerAchievements->count(); - $achievementsUnlockedHardcoreCount = $playerAchievementsHardcore->count(); - - $firstUnlockAt = $playerAchievements->min('unlocked_at'); - $lastUnlockAt = $playerAchievements->max('unlocked_at'); - - $firstUnlockHardcoreAt = $playerAchievements->min('unlocked_hardcore_at'); - $lastUnlockHardcoreAt = $playerAchievements->max('unlocked_hardcore_at'); - - // TODO check completion state here and dispatch completion events if applicable - - $completedAt = $playerGame->completed_at; - $completedHardcoreAt = $playerGame->completed_hardcore_at; - $completionDates = $playerGame->completion_dates; - $completionDatesHardcore = $playerGame->completion_dates_hardcore; - // $justCompleted = false; - // if ($justCompleted) { - // $completionDates = new Collection($playerGame->completion_dates); - // } - - // Coalesce dates to existing values or unlock dates - - /** @var Carbon $startedAt */ - $startedAt = $playerGame->created_at !== null - ? min($firstUnlockAt, $playerGame->created_at) - : $firstUnlockAt; - - $createdAt = $playerGame->created_at !== null - ? $playerGame->created_at - : $startedAt; - - $lastPlayedAt = $playerAchievements->pluck('unlocked_at') - ->merge($playerAchievementsHardcore->pluck('unlocked_hardcore_at')) - ->add($playerGame->last_played_at) - ->filter() - ->max(); - - $timeTaken = $startedAt->diffInSeconds($completedAt ?? $lastPlayedAt); - $timeTakenHardcore = $startedAt->diffInSeconds($completedHardcoreAt ?? $lastPlayedAt); - - $metrics = [ - 'achievements_total' => $achievementsTotal, - 'achievements_unlocked' => $achievementsUnlockedCount, - 'achievements_unlocked_hardcore' => $achievementsUnlockedHardcoreCount, - 'completion_percentage' => $achievementsUnlockedCount / $achievementsTotal * 100, - 'completion_percentage_hardcore' => $achievementsUnlockedHardcoreCount / $achievementsTotal * 100, - 'last_played_at' => $lastPlayedAt, - // 'playtime_total' => $playtimeTotal, - 'time_taken' => $timeTaken, - 'time_taken_hardcore' => $timeTakenHardcore, - 'completion_dates' => $completionDates, - 'completion_dates_hardcore' => $completionDatesHardcore, - 'completed_at' => $completedAt, - 'completed_hardcore_at' => $completedHardcoreAt, - 'last_unlock_at' => $lastUnlockAt, - 'last_unlock_hardcore_at' => $lastUnlockHardcoreAt, - 'first_unlock_at' => $firstUnlockAt, - 'first_unlock_hardcore_at' => $firstUnlockHardcoreAt, - 'points_total' => $pointsTotal, - 'points' => $points, - 'points_weighted_total' => $pointsWeightedTotal, - 'points_weighted' => $pointsWeighted, - 'created_at' => $createdAt, - ]; - - $playerGame->update($metrics); - } -} diff --git a/app/Platform/Actions/UpdatePlayerMetrics.php b/app/Platform/Actions/UpdatePlayerMetrics.php new file mode 100644 index 0000000000..10e7eeac5f --- /dev/null +++ b/app/Platform/Actions/UpdatePlayerMetrics.php @@ -0,0 +1,26 @@ +playerGames()->where('achievements_unlocked', '>', 0); + $user->achievements_unlocked = $playerGames->sum('achievements_unlocked'); + $user->achievements_unlocked_hardcore = $playerGames->sum('achievements_unlocked_hardcore'); + $user->completion_percentage_average = $playerGames->average('completion_percentage'); + $user->completion_percentage_average_hardcore = $playerGames->average('completion_percentage_hardcore'); + + // TODO refactor to use aggregated player_games metrics + $user->RAPoints = $user->achievements()->published()->wherePivotNotNull('unlocked_hardcore_at')->sum('Points'); + $user->RASoftcorePoints = $user->achievements()->published()->wherePivotNull('unlocked_hardcore_at')->sum('Points'); + $user->TrueRAPoints = $user->achievements()->published()->wherePivotNotNull('unlocked_hardcore_at')->sum('TrueRatio'); + + $user->save(); + } +} diff --git a/app/Platform/Actions/UpsertTriggerVersionAction.php b/app/Platform/Actions/UpsertTriggerVersion.php similarity index 98% rename from app/Platform/Actions/UpsertTriggerVersionAction.php rename to app/Platform/Actions/UpsertTriggerVersion.php index 9e79fee132..3f3ac1a491 100644 --- a/app/Platform/Actions/UpsertTriggerVersionAction.php +++ b/app/Platform/Actions/UpsertTriggerVersion.php @@ -8,7 +8,7 @@ use App\Platform\Models\Trigger; use Illuminate\Database\Eloquent\Model; -class UpsertTriggerVersionAction +class UpsertTriggerVersion { public function execute(Model $triggerable, string $conditions, bool $versioned = true): ?Trigger { diff --git a/app/Platform/AppServiceProvider.php b/app/Platform/AppServiceProvider.php index 1ae396ff98..931269ad43 100644 --- a/app/Platform/AppServiceProvider.php +++ b/app/Platform/AppServiceProvider.php @@ -19,18 +19,13 @@ use App\Platform\Commands\SyncPlayerRichPresence; use App\Platform\Commands\SyncPlayerSession; use App\Platform\Commands\UnlockPlayerAchievement; -use App\Platform\Commands\UpdateAllAchievementsMetrics; -use App\Platform\Commands\UpdateAllGamesMetrics; -use App\Platform\Commands\UpdateAllPlayerGamesMetrics; use App\Platform\Commands\UpdateAwardsStaticData; use App\Platform\Commands\UpdateDeveloperContributionYield; +use App\Platform\Commands\UpdateGameAchievementsMetrics; use App\Platform\Commands\UpdateGameMetrics; -use App\Platform\Commands\UpdateGameWeightedPoints; use App\Platform\Commands\UpdateLeaderboardMetrics; use App\Platform\Commands\UpdatePlayerGameMetrics; -use App\Platform\Commands\UpdatePlayerMasteries; use App\Platform\Commands\UpdatePlayerMetrics; -use App\Platform\Commands\UpdatePlayerPoints; use App\Platform\Commands\UpdatePlayerRanks; use App\Platform\Components\GameCard; use App\Platform\Models\Achievement; @@ -64,40 +59,30 @@ public function boot(): void { if ($this->app->runningInConsole()) { $this->commands([ - DeleteOrphanedLeaderboardEntries::class, - UpdateDeveloperContributionYield::class, - UpdateGameWeightedPoints::class, - UpdatePlayerPoints::class, - UpdatePlayerMasteries::class, + // Games + UpdateGameMetrics::class, + UpdateGameAchievementsMetrics::class, - /* - * no-intro - */ + // Game Hashes NoIntroImport::class, - /* - * Platform - */ - UnlockPlayerAchievement::class, - - UpdateAllAchievementsMetrics::class, - - UpdateGameMetrics::class, - UpdateAllGamesMetrics::class, - + // Leaderboards UpdateLeaderboardMetrics::class, + DeleteOrphanedLeaderboardEntries::class, + // Players + UnlockPlayerAchievement::class, + UpdatePlayerGameMetrics::class, UpdatePlayerMetrics::class, UpdatePlayerRanks::class, - UpdatePlayerGameMetrics::class, - UpdateAllPlayerGamesMetrics::class, - + // Awards & Badges UpdateAwardsStaticData::class, - /* - * Sync - */ + // Developer + UpdateDeveloperContributionYield::class, + + // Sync SyncAchievements::class, SyncGameSets::class, SyncGames::class, @@ -117,10 +102,6 @@ public function boot(): void /** @var Schedule $schedule */ $schedule = $this->app->make(Schedule::class); - // TODO replace with queued jobs - $schedule->command(UpdateGameWeightedPoints::class)->everyMinute(); - $schedule->command(UpdatePlayerPoints::class)->everyMinute(); - $schedule->command(DeleteOrphanedLeaderboardEntries::class)->daily(); }); diff --git a/app/Platform/Commands/DeleteOrphanedLeaderboardEntries.php b/app/Platform/Commands/DeleteOrphanedLeaderboardEntries.php index a1336ea668..9d24616863 100644 --- a/app/Platform/Commands/DeleteOrphanedLeaderboardEntries.php +++ b/app/Platform/Commands/DeleteOrphanedLeaderboardEntries.php @@ -8,7 +8,7 @@ class DeleteOrphanedLeaderboardEntries extends Command { - protected $signature = 'ra:platform:delete-orphaned-leaderboard-entries'; + protected $signature = 'ra:platform:leaderboard:delete-orphaned-entries'; protected $description = 'Delete orphaned leaderboard entries'; public function handle(): void diff --git a/app/Platform/Commands/NoIntroImport.php b/app/Platform/Commands/NoIntroImport.php index 297747e74c..5cd6e49df2 100644 --- a/app/Platform/Commands/NoIntroImport.php +++ b/app/Platform/Commands/NoIntroImport.php @@ -13,7 +13,7 @@ class NoIntroImport extends Command { - protected $signature = 'ra:platform:rom:no-intro:import {jsonDatFile} {systemId?} {--seed} {--ignore-system-mismatch}'; + protected $signature = 'ra:platform:game-hash:no-intro:import {jsonDatFile} {systemId?} {--seed} {--ignore-system-mismatch}'; protected $description = 'Imports JSON converted no-intro.org DAT files'; public function __construct() diff --git a/app/Platform/Commands/SyncGameHashes.php b/app/Platform/Commands/SyncGameHashes.php index 8343775c51..7f9436240d 100644 --- a/app/Platform/Commands/SyncGameHashes.php +++ b/app/Platform/Commands/SyncGameHashes.php @@ -4,7 +4,7 @@ namespace App\Platform\Commands; -use App\Platform\Actions\LinkHashToGameAction; +use App\Platform\Actions\LinkHashToGame; use App\Platform\Models\Game; use App\Support\Sync\SyncTrait; use Exception; @@ -17,8 +17,9 @@ class SyncGameHashes extends Command protected $signature = 'ra:sync:game-hashes {id?} {--f|full} {--p|no-post}'; protected $description = 'Sync game hashes'; - public function __construct(private LinkHashToGameAction $linkHashToGameAction) - { + public function __construct( + private readonly LinkHashToGame $linkHashToGameAction + ) { parent::__construct(); } diff --git a/app/Platform/Commands/SyncGames.php b/app/Platform/Commands/SyncGames.php index 103893f6a0..ae30790074 100644 --- a/app/Platform/Commands/SyncGames.php +++ b/app/Platform/Commands/SyncGames.php @@ -4,8 +4,8 @@ namespace App\Platform\Commands; -use App\Platform\Actions\AddImageToGameAction; -use App\Platform\Actions\UpsertTriggerVersionAction; +use App\Platform\Actions\AddImageToGame; +use App\Platform\Actions\UpsertTriggerVersion; use App\Platform\Models\Game; use App\Platform\Models\GameHashSet; use App\Support\Sync\SyncTrait; @@ -22,8 +22,8 @@ class SyncGames extends Command protected $description = 'Sync games'; public function __construct( - private AddImageToGameAction $addImageToGameAction, - private UpsertTriggerVersionAction $upsertTriggerVersionAction + private readonly AddImageToGame $addImageToGameAction, + private readonly UpsertTriggerVersion $upsertTriggerVersionAction ) { parent::__construct(); } diff --git a/app/Platform/Commands/SyncPlayerGames.php b/app/Platform/Commands/SyncPlayerGames.php index cba06a33fd..e74071d58d 100644 --- a/app/Platform/Commands/SyncPlayerGames.php +++ b/app/Platform/Commands/SyncPlayerGames.php @@ -4,7 +4,6 @@ namespace App\Platform\Commands; -use App\Platform\Actions\UpdatePlayerGameMetricsAction; use App\Platform\Models\PlayerGame; use App\Support\Sync\SyncTrait; use Exception; @@ -19,11 +18,6 @@ class SyncPlayerGames extends Command protected $signature = 'ra:sync:player-games {username?} {--f|full} {--p|no-post}'; protected $description = 'Sync player games'; - public function __construct(private UpdatePlayerGameMetricsAction $updatePlayerGameMetricsAction) - { - parent::__construct(); - } - /** * @throws Exception */ diff --git a/app/Platform/Commands/UnlockPlayerAchievement.php b/app/Platform/Commands/UnlockPlayerAchievement.php index 41d555cdc6..08db5cfe60 100644 --- a/app/Platform/Commands/UnlockPlayerAchievement.php +++ b/app/Platform/Commands/UnlockPlayerAchievement.php @@ -4,16 +4,22 @@ namespace App\Platform\Commands; +use App\Platform\Actions\UnlockPlayerAchievement as UnlockPlayerAchievementAction; +use App\Platform\Models\Achievement; +use App\Site\Models\User; use Exception; use Illuminate\Console\Command; class UnlockPlayerAchievement extends Command { - protected $signature = 'ra:platform:player:unlock-achievement'; - protected $description = ''; + protected $signature = 'ra:platform:player:unlock-achievement + {username} + {achievementIds : Comma-separated list of achievement IDs} + {--hardcore}'; + protected $description = 'Unlock achievement(s) for user'; public function __construct( - // private UnlockPlayerAchievementAction $unlockPlayerAchievementAction + private readonly UnlockPlayerAchievementAction $unlockPlayerAchievement, ) { parent::__construct(); } @@ -23,6 +29,29 @@ public function __construct( */ public function handle(): void { - // $this->unlockPlayerAchievementAction->execute($user, $achievement, $hardcore, $unlockedBy = null); + $username = $this->argument('username'); + $achievementIds = collect(explode(',', $this->argument('achievementIds'))) + ->map(fn ($id) => (int) $id); + $hardcore = (bool) $this->option('hardcore'); + + $user = User::where('User', $this->argument('username'))->firstOrFail(); + + $achievements = Achievement::whereIn('id', $achievementIds)->get(); + + $this->info('Unlocking ' . $achievements->count() . ' [' . ($hardcore ? 'hardcore' : 'softcore') . '] ' . __res('achievement', $achievements->count()) . ' for user [' . $username . '] [' . $user->id . ']'); + + $progressBar = $this->output->createProgressBar($achievements->count()); + $progressBar->start(); + + foreach ($achievements as $achievement) { + $this->unlockPlayerAchievement->execute( + $user, + $achievement, + $hardcore, + ); + $progressBar->advance(); + } + + $progressBar->finish(); } } diff --git a/app/Platform/Commands/UpdateAllAchievementsMetrics.php b/app/Platform/Commands/UpdateAllAchievementsMetrics.php deleted file mode 100644 index 1ddbc5b831..0000000000 --- a/app/Platform/Commands/UpdateAllAchievementsMetrics.php +++ /dev/null @@ -1,22 +0,0 @@ -having('achievements_count', '>', '0') - // ->chunk(100, function ($games, $index) { - // $this->info('chunk ' . ($index * 100) . ' ' . memory_get_usage()); - // foreach ($games as $game) { - // $game->updateMetrics(); - // } - // }); - } -} diff --git a/app/Platform/Commands/UpdateAllPlayerGamesMetrics.php b/app/Platform/Commands/UpdateAllPlayerGamesMetrics.php deleted file mode 100644 index 12a066586b..0000000000 --- a/app/Platform/Commands/UpdateAllPlayerGamesMetrics.php +++ /dev/null @@ -1,22 +0,0 @@ -update(['num_hardcore_game_beaten_awards' => $hardcoreGameBeatenAwardsCount]); } + /** + * @deprecated use a query in the component instead - most "last of something" queries are not expensive + */ private function updateLastGameHardcoreMastered(): void { $foundAward = PlayerBadge::with('user') @@ -75,6 +78,9 @@ private function updateLastGameHardcoreMastered(): void } } + /** + * @deprecated use a query in the component instead - most "last of something" queries are not expensive + */ private function updateLastGameHardcoreBeaten(): void { $foundAward = PlayerBadge::with('user') diff --git a/app/Platform/Commands/UpdateDeveloperContributionYield.php b/app/Platform/Commands/UpdateDeveloperContributionYield.php index 2ddafd4e18..ecd8726a54 100644 --- a/app/Platform/Commands/UpdateDeveloperContributionYield.php +++ b/app/Platform/Commands/UpdateDeveloperContributionYield.php @@ -4,123 +4,44 @@ namespace App\Platform\Commands; -use App\Community\Enums\AwardType; -use App\Platform\Enums\AchievementFlag; -use App\Platform\Models\PlayerAchievementLegacy; -use App\Platform\Models\PlayerBadge; +use App\Platform\Actions\UpdateDeveloperContributionYield as UpdateDeveloperContributionYieldAction; use App\Site\Models\User; use Illuminate\Console\Command; -use Illuminate\Support\Facades\DB; class UpdateDeveloperContributionYield extends Command { - protected $signature = 'ra:platform:update-developer-contribution-yield {username?}'; + protected $signature = 'ra:platform:developer:update-contribution-yield {username?}'; protected $description = 'Calculate developer contributions and badge tiers'; + public function __construct( + private readonly UpdateDeveloperContributionYieldAction $updateDeveloperContributionYield + ) { + parent::__construct(); + } + public function handle(): void { $username = $this->argument('username'); - if (!empty($username)) { - $this->calculate($username); - return; - } + $users = collect(); - $users = User::select('User') - ->where('ContribCount', '>', 0) - ->get(); + if (!empty($username)) { + $users->push(User::where('User', $username)->firstOrFail()); + } else { + $users = User::select('User') + ->where('ContribCount', '>', 0) + ->get(); + } $progressBar = $this->output->createProgressBar($users->count()); $progressBar->start(); /** @var User $user */ foreach ($users as $user) { - $this->calculate($user->User); + $this->updateDeveloperContributionYield->execute($user); $progressBar->advance(); } $progressBar->finish(); } - - private function calculate(string $username): void - { - $points = 0; - $pointLevel = 0; - $nextPointThreshold = PlayerBadge::getBadgeThreshold(AwardType::AchievementPointsYield, $pointLevel); - - $count = 0; - $countLevel = 0; - $nextCountThreshold = PlayerBadge::getBadgeThreshold(AwardType::AchievementUnlocksYield, $countLevel); - - // get all unlocks for achievements created by the user ordered by date - $unlocks = PlayerAchievementLegacy::select('Awarded.Date', DB::raw('MAX(Awarded.HardcoreMode)'), 'Achievements.Points') - ->leftJoin('Achievements', 'Achievements.ID', '=', 'Awarded.AchievementID') - ->where('Achievements.Author', '=', $username) - ->where('Awarded.User', '!=', $username) - ->where('Achievements.Flags', '=', AchievementFlag::OfficialCore) - ->groupBy(['Awarded.User', 'Awarded.AchievementID']) - ->orderBy('Awarded.Date') - ->get(); - - /** @var PlayerAchievementLegacy $unlock */ - foreach ($unlocks as $unlock) { - // when a threshold is crossed, award a badge - $count++; - if ($count === $nextCountThreshold) { - PlayerBadge::upsert( - [ - [ - 'User' => $username, - 'AwardType' => AwardType::AchievementUnlocksYield, - 'AwardData' => $countLevel, - 'AwardDate' => $unlock->Date, - ], - ], - ['User', 'AwardType', 'AwardData'], - ['AwardDate'] - ); - $countLevel++; - - $nextCountThreshold = PlayerBadge::getBadgeThreshold(AwardType::AchievementUnlocksYield, $countLevel); - } - - $points += $unlock['Points']; - if ($points >= $nextPointThreshold) { - PlayerBadge::upsert( - [ - [ - 'User' => $username, - 'AwardType' => AwardType::AchievementPointsYield, - 'AwardData' => $pointLevel, - 'AwardDate' => $unlock->Date, - ], - ], - ['User', 'AwardType', 'AwardData'], - ['AwardDate'] - ); - $pointLevel++; - - $nextPointThreshold = PlayerBadge::getBadgeThreshold(AwardType::AchievementPointsYield, $pointLevel); - if ($nextPointThreshold == 0) { - // if we run out of tiers, getBadgeThreshold returns 0, so everything will be >=. set to MAXINT - $nextPointThreshold = 0xFFFFFFFF; - } - } - } - - // remove any extra badge tiers - PlayerBadge::where('User', '=', $username) - ->where('AwardType', '=', AwardType::AchievementUnlocksYield) - ->where('AwardData', '>=', $countLevel) - ->delete(); - - PlayerBadge::where('User', '=', $username) - ->where('AwardType', '=', AwardType::AchievementPointsYield) - ->where('AwardData', '>=', $pointLevel) - ->delete(); - - // update the denormalized data - User::where('User', '=', $username) - ->update(['ContribCount' => $count, 'ContribYield' => $points]); - } } diff --git a/app/Platform/Commands/UpdateGameAchievementsMetrics.php b/app/Platform/Commands/UpdateGameAchievementsMetrics.php new file mode 100644 index 0000000000..44be564b1c --- /dev/null +++ b/app/Platform/Commands/UpdateGameAchievementsMetrics.php @@ -0,0 +1,40 @@ +argument('gameIds'))) + ->map(fn ($id) => (int) $id); + + $games = Game::whereIn('id', $gameIds)->get(); + + $progressBar = $this->output->createProgressBar($games->count()); + $progressBar->start(); + + foreach ($games as $game) { + $this->updateGameAchievementsMetrics->execute($game); + $progressBar->advance(); + } + + $progressBar->finish(); + } +} diff --git a/app/Platform/Commands/UpdateGameMetrics.php b/app/Platform/Commands/UpdateGameMetrics.php index 09198b20a8..de68f6b12d 100644 --- a/app/Platform/Commands/UpdateGameMetrics.php +++ b/app/Platform/Commands/UpdateGameMetrics.php @@ -4,25 +4,37 @@ namespace App\Platform\Commands; -use App\Platform\Actions\UpdateGameMetricsAction; +use App\Platform\Actions\UpdateGameMetrics as UpdateGameMetricsAction; use App\Platform\Models\Game; use Illuminate\Console\Command; class UpdateGameMetrics extends Command { - protected $signature = 'ra:platform:game:update-metrics {game}'; - protected $description = "Update a game's metrics"; + protected $signature = 'ra:platform:game:update-metrics + {gameIds : Comma-separated list of game IDs}'; + protected $description = "Update game(s) metrics"; - public function __construct(private UpdateGameMetricsAction $updateGameMetricsAction) - { + public function __construct( + private readonly UpdateGameMetricsAction $updateGameMetrics + ) { parent::__construct(); } public function handle(): void { - /** @var Game $game */ - $game = Game::findOrFail($this->argument('game')); + $gameIds = collect(explode(',', $this->argument('gameIds'))) + ->map(fn ($id) => (int) $id); + + $games = Game::whereIn('id', $gameIds)->get(); + + $progressBar = $this->output->createProgressBar($games->count()); + $progressBar->start(); + + foreach ($games as $game) { + $this->updateGameMetrics->execute($game); + $progressBar->advance(); + } - $this->updateGameMetricsAction->execute($game); + $progressBar->finish(); } } diff --git a/app/Platform/Commands/UpdateGameWeightedPoints.php b/app/Platform/Commands/UpdateGameWeightedPoints.php deleted file mode 100644 index bde54de209..0000000000 --- a/app/Platform/Commands/UpdateGameWeightedPoints.php +++ /dev/null @@ -1,45 +0,0 @@ -argument('gameId'); - if (!empty($gameId)) { - $this->updateGameWeightedPointsAction->run($gameId); - - return; - } - - $staticData = StaticData::first(); - - $gameId = $staticData['NextGameToScan']; - for ($i = 0; $i < 3; $i++) { - $this->updateGameWeightedPointsAction->run($gameId); - // get next highest game ID - $gameId = Game::where('ID', '>', $gameId)->min('ID') ?? 1; - } - - StaticData::first()->update([ - 'NextGameToScan' => $gameId, - ]); - } -} diff --git a/app/Platform/Commands/UpdatePlayerGameMetrics.php b/app/Platform/Commands/UpdatePlayerGameMetrics.php index 4c65f90f72..2c218f3000 100644 --- a/app/Platform/Commands/UpdatePlayerGameMetrics.php +++ b/app/Platform/Commands/UpdatePlayerGameMetrics.php @@ -4,52 +4,56 @@ namespace App\Platform\Commands; -use App\Platform\Actions\UpdatePlayerGameMetricsAction; -use App\Platform\Models\PlayerGame; +use App\Platform\Actions\UpdatePlayerGameMetrics as UpdatePlayerGameMetricsAction; +use App\Platform\Models\Game; use App\Site\Models\User; use Illuminate\Console\Command; -use Illuminate\Support\Collection; class UpdatePlayerGameMetrics extends Command { - protected $signature = 'ra:platform:player:update-game-metrics {username} {gameId?}'; - protected $description = 'Update player games and achievement-set metrics'; - - public function __construct(private UpdatePlayerGameMetricsAction $updatePlayerGameMetricsAction) - { + protected $signature = 'ra:platform:player:update-game-metrics + {username} + {gameIds? : Comma-separated list of game IDs. Leave empty to update all games in player library} + {--outdated}'; + protected $description = 'Update player game(s) metrics'; + + public function __construct( + private readonly UpdatePlayerGameMetricsAction $updatePlayerGameMetrics + ) { parent::__construct(); } public function handle(): void { - $username = $this->argument('username'); - $gameId = $this->argument('gameId'); + $outdated = $this->option('outdated'); - /** ?User $user */ - $user = User::firstWhere('User', $username); - if (!$user) { - $this->error('User not found'); + $gameIds = collect(explode(',', $this->argument('gameIds') ?? '')) + ->filter() + ->map(fn ($id) => (int) $id); - return; - } + $user = User::where('User', $this->argument('username'))->firstOrFail(); - $playerGames = new Collection(); - if ($gameId) { - /** @var ?PlayerGame $playerGame */ - $playerGame = $user->playerGames()->with(['user', 'game'])->firstWhere('game_id', $gameId); - if (!$playerGame) { - $this->error('Player game not found'); - - return; - } - $playerGames->add($playerGame); - } else { - $playerGames = $user->playerGames()->with(['user', 'game'])->get(); + $query = $user->playerGames() + ->with(['user', 'game']); + if ($gameIds->isNotEmpty()) { + $query->whereIn( + 'game_id', + Game::whereIn('id', $gameIds)->get()->pluck('id') + ); } + if ($outdated) { + $query->whereNotNull('update_status'); + } + $playerGames = $query->get(); + + $progressBar = $this->output->createProgressBar($playerGames->count()); + $progressBar->start(); foreach ($playerGames as $playerGame) { - $this->info('Updating player [' . $playerGame->user_id . '] game [' . $playerGame->game_id . ']'); - $this->updatePlayerGameMetricsAction->execute($playerGame); + $this->updatePlayerGameMetrics->execute($playerGame); + $progressBar->advance(); } + + $progressBar->finish(); } } diff --git a/app/Platform/Commands/UpdatePlayerMasteries.php b/app/Platform/Commands/UpdatePlayerMasteries.php deleted file mode 100644 index 6b64d94980..0000000000 --- a/app/Platform/Commands/UpdatePlayerMasteries.php +++ /dev/null @@ -1,137 +0,0 @@ -argument('username'); - if (!empty($username)) { - $this->recalculate($username); - - return; - } - - // TODO use PlayerBadge model - $users = DB::table('SiteAwards') - ->where('AwardType', '=', AwardType::Mastery) - ->distinct() - ->get(['User']); - - $progressBar = $this->output->createProgressBar($users->count()); - $progressBar->start(); - - foreach ($users as $user) { - $this->recalculate($user->User); - $progressBar->advance(); - } - - $progressBar->finish(); - } - - private function recalculate(string $username): void - { - // get all mastery awards for the user - // TODO use PlayerBadge model - $awards = DB::table('SiteAwards') - ->where('AwardType', '=', AwardType::Mastery) - ->where('User', '=', $username) - ->get(); - - $masteredGames = []; - foreach ($awards as $award) { - $masteredGames[$award->AwardData][$award->AwardDataExtra] = true; - } - - foreach ($masteredGames as $gameID => $masteryData) { - if (array_key_exists($gameID, $this->gameAchievements)) { - $coreAchievementCount = $this->gameAchievements[$gameID]; - } else { - // TODO use Achievement model - $coreAchievementCount = DB::table('Achievements') - ->where('GameID', '=', $gameID) - ->where('Flags', '=', AchievementFlag::OfficialCore) - ->count(); - $this->gameAchievements[$gameID] = $coreAchievementCount; - } - - // TODO use PlayerAchievement model - $userUnlocks = DB::table('Awarded') - ->select(['Awarded.HardcoreMode', DB::raw('COUNT(Awarded.AchievementID) AS Num')]) - ->leftJoin('Achievements', 'Achievements.ID', '=', 'Awarded.AchievementID') - ->where('Achievements.GameID', '=', $gameID) - ->where('Awarded.User', '=', $username) - ->where('Achievements.Flags', '=', AchievementFlag::OfficialCore) - ->groupBy(['Awarded.HardcoreMode']) - ->pluck('Num', 'Awarded.HardcoreMode') - ->toArray(); - - $hardcoreCount = $userUnlocks[UnlockMode::Hardcore] ?? 0; - $softcoreCount = $userUnlocks[UnlockMode::Softcore] ?? 0; - - $deleteAward = false; - $demoteAward = false; - if ($hardcoreCount === 0 && $softcoreCount === 0) { - // user has no achievements for the set. if the set is empty, assume it - // was demoted and keep the badge, otherwise assume they did a full reset - // and destroy the badge. - $deleteAward = ($coreAchievementCount !== 0); - } elseif ($hardcoreCount < $coreAchievementCount) { - if ($softcoreCount < $coreAchievementCount) { - // if the user has at least one unlock for the set, assume there was - // a revision and do nothing. if they want to get rid of the badge, - // they can reset one or more of the achievements they have. - } elseif ($masteryData[UnlockMode::Hardcore] ?? false) { - // user has a hardcore badge, but only the softcore achievements, demote it - $demoteAward = true; - } - } - - if ($deleteAward) { - // user no longer has all achievements for the set, revoke their badge - // TODO use PlayerBadge model - DB::table('SiteAwards') - ->where('AwardType', '=', AwardType::Mastery) - ->where('User', '=', $username) - ->where('AwardData', '=', $gameID) - ->delete(); - } elseif ($demoteAward) { - // user has all softcore achievements for the set, but no longer has - // all hardcore achievements for the set - if ($masteryData[UnlockMode::Softcore] ?? false) { - // user already has a separate softcore badge, delete the hardcore one - // TODO use PlayerBadge model - DB::table('SiteAwards') - ->where('AwardType', '=', AwardType::Mastery) - ->where('User', '=', $username) - ->where('AwardData', '=', $gameID) - ->where('AwardDataExtra', '=', UnlockMode::Hardcore) - ->delete(); - } else { - // user only has a hardcore badge, demote it to softcore - DB::connection('mysql') - ->table('SiteAwards') - ->where('AwardType', '=', AwardType::Mastery) - ->where('User', '=', $username) - ->where('AwardData', '=', $gameID) - ->update(['AwardDataExtra' => UnlockMode::Softcore]); - } - } - } - } -} diff --git a/app/Platform/Commands/UpdatePlayerMetrics.php b/app/Platform/Commands/UpdatePlayerMetrics.php index fee5c1952b..82d5fa0060 100644 --- a/app/Platform/Commands/UpdatePlayerMetrics.php +++ b/app/Platform/Commands/UpdatePlayerMetrics.php @@ -4,19 +4,26 @@ namespace App\Platform\Commands; +use App\Platform\Actions\UpdatePlayerMetrics as UpdatePlayerMetricsAction; +use App\Site\Models\User; use Illuminate\Console\Command; class UpdatePlayerMetrics extends Command { - protected $signature = 'ra:platform:player:update-metrics'; - protected $description = ''; + protected $signature = 'ra:platform:player:update-metrics {username}'; + protected $description = 'Update player metrics'; - public function __construct() - { + public function __construct( + private readonly UpdatePlayerMetricsAction $updatePlayerMetrics + ) { parent::__construct(); } public function handle(): void { + $user = User::where('User', $this->argument('username'))->firstOrFail(); + + $this->info('Update metrics for player ' . $user->username . ' [' . $user->id . ']'); + $this->updatePlayerMetrics->execute($user); } } diff --git a/app/Platform/Commands/UpdatePlayerPoints.php b/app/Platform/Commands/UpdatePlayerPoints.php deleted file mode 100644 index 36d6986299..0000000000 --- a/app/Platform/Commands/UpdatePlayerPoints.php +++ /dev/null @@ -1,53 +0,0 @@ -argument('username'); - if (!empty($username)) { - $this->calculate($username); - - return; - } - - $staticData = StaticData::first(); - - $userId = $staticData['NextUserIDToScan']; - for ($i = 0; $i < 3; $i++) { - /** @var ?User $user */ - $user = User::find($userId); - if ($user) { - $this->calculate($user->User); - } - // get next highest user ID - $userId = User::where('ID', '>', $userId) - ->hasAnyPoints()->min('ID') ?? 1; - } - - StaticData::first()->update([ - 'NextUserIDToScan' => $userId, - ]); - } - - private function calculate(string $username): void - { - // TODO aggregate player_games instead - recalculatePlayerPoints($username); - // TODO queue UpdateDeveloperContributionYield command instead for a more detailed contribution yield update - recalculateDeveloperContribution($username); - // TODO (?) - recalculatePlayerBeatenGames($username); - } -} diff --git a/app/Platform/Concerns/ActsAsPlayer.php b/app/Platform/Concerns/ActsAsPlayer.php index 525b816024..16ea186bce 100644 --- a/app/Platform/Concerns/ActsAsPlayer.php +++ b/app/Platform/Concerns/ActsAsPlayer.php @@ -44,17 +44,7 @@ public function rollConnectToken(): void public function getPointsRatioAttribute(): float|string { - return $this->points_total ? ($this->points_weighted / $this->points_total) : 0; - } - - public function getPointsTotalAttribute(): int - { - return (int) ($this->attributes['RAPoints'] ?? 0); - } - - public function getPointsWeightedTotalAttribute(): int - { - return (int) ($this->attributes['TrueRAPoints'] ?? 0); + return $this->points ? ($this->points_weighted / $this->points) : 0; } // == relations @@ -119,7 +109,7 @@ public function playerSessions(): HasMany */ public function lastGame(): BelongsTo { - return $this->belongsTo(Game::class, 'LastGameID', 'user_id'); + return $this->belongsTo(Game::class, 'LastGameID', 'ID'); } /** diff --git a/app/Platform/Controllers/AchievementPlayerController.php b/app/Platform/Controllers/AchievementPlayerController.php index aa1b410e5f..c0b5e94dff 100644 --- a/app/Platform/Controllers/AchievementPlayerController.php +++ b/app/Platform/Controllers/AchievementPlayerController.php @@ -6,7 +6,6 @@ use App\Http\Controller; use App\Platform\Models\Achievement; -use App\Platform\Models\PlayerAchievement; use Illuminate\Contracts\View\View; class AchievementPlayerController extends Controller @@ -24,15 +23,7 @@ public function index(Achievement $achievement): View 'game', ]); - $unlocks = $achievement->hasMany(PlayerAchievement::class); - $numWinners = $unlocks->count(); - $numPossibleWinners = $achievement->game()->first()->players()->count(); - - return view('achievement.player.index', [ - 'numWinners' => $numWinners, - 'numPossibleWinners' => $numPossibleWinners, - 'winnerPercent' => round($numWinners * 100 / $numPossibleWinners, 2), - ]) + return view('achievement.player.index') ->with('achievement', $achievement); } } diff --git a/app/Platform/Controllers/EmulatorReleaseController.php b/app/Platform/Controllers/EmulatorReleaseController.php index 4a03e59ecb..a665a44348 100644 --- a/app/Platform/Controllers/EmulatorReleaseController.php +++ b/app/Platform/Controllers/EmulatorReleaseController.php @@ -5,7 +5,7 @@ namespace App\Platform\Controllers; use App\Http\Controller; -use App\Platform\Actions\LinkLatestEmulatorReleaseAction; +use App\Platform\Actions\LinkLatestEmulatorRelease; use App\Platform\Models\Emulator; use App\Platform\Models\EmulatorRelease; use App\Platform\Requests\EmulatorReleaseRequest; @@ -57,7 +57,7 @@ public function store( EmulatorReleaseRequest $request, Emulator $emulator, AddMediaAction $addFileToCollectionAction, - LinkLatestEmulatorReleaseAction $linkLatestReleaseAction + LinkLatestEmulatorRelease $linkLatestReleaseAction ): RedirectResponse { $this->authorize('create', $this->resourceClass()); @@ -95,7 +95,7 @@ public function update( EmulatorReleaseRequest $request, EmulatorRelease $release, AddMediaAction $addFileToCollectionAction, - LinkLatestEmulatorReleaseAction $linkLatestReleaseAction + LinkLatestEmulatorRelease $linkLatestReleaseAction ): RedirectResponse { $this->authorize('update', $release); @@ -115,7 +115,7 @@ public function update( public function destroy( EmulatorRelease $release, - LinkLatestEmulatorReleaseAction $linkLatestReleaseAction + LinkLatestEmulatorRelease $linkLatestReleaseAction ): RedirectResponse { $this->authorize('delete', $release); @@ -151,7 +151,7 @@ public function forceDestroy(int $release): RedirectResponse ); } - public function restore(int $release, LinkLatestEmulatorReleaseAction $linkLatestReleaseAction): RedirectResponse + public function restore(int $release, LinkLatestEmulatorRelease $linkLatestReleaseAction): RedirectResponse { $release = EmulatorRelease::withTrashed()->find($release); diff --git a/app/Platform/Controllers/GameController.php b/app/Platform/Controllers/GameController.php index f346052834..70bc351278 100644 --- a/app/Platform/Controllers/GameController.php +++ b/app/Platform/Controllers/GameController.php @@ -80,8 +80,6 @@ public function show(Request $request, Game $game, ?string $slug = null): View|R 'leaderboards', 'forumTopic', ]); - $game->loadSum('achievements', 'points'); - $game->loadCount(['players', 'achievements']); // $game->achievements->each->setRelation('game', $game); diff --git a/app/Platform/Controllers/IntegrationReleaseController.php b/app/Platform/Controllers/IntegrationReleaseController.php index e7ff6fe5a8..04f9791a20 100644 --- a/app/Platform/Controllers/IntegrationReleaseController.php +++ b/app/Platform/Controllers/IntegrationReleaseController.php @@ -5,7 +5,7 @@ namespace App\Platform\Controllers; use App\Http\Controller; -use App\Platform\Actions\LinkLatestIntegrationReleaseAction; +use App\Platform\Actions\LinkLatestIntegrationRelease; use App\Platform\Models\IntegrationRelease; use App\Platform\Requests\IntegrationReleaseRequest; use App\Support\MediaLibrary\Actions\AddMediaAction; @@ -47,7 +47,7 @@ public function create(): View public function store( IntegrationReleaseRequest $request, AddMediaAction $addMediaAction, - LinkLatestIntegrationReleaseAction $linkLatestReleaseAction + LinkLatestIntegrationRelease $linkLatestReleaseAction ): RedirectResponse { $this->authorize('create', $this->resourceClass()); @@ -82,7 +82,7 @@ public function update( IntegrationReleaseRequest $request, IntegrationRelease $release, AddMediaAction $addMediaAction, - LinkLatestIntegrationReleaseAction $linkLatestReleaseAction + LinkLatestIntegrationRelease $linkLatestReleaseAction ): RedirectResponse { $this->authorize('update', $release); @@ -100,7 +100,7 @@ public function update( public function destroy( IntegrationRelease $release, - LinkLatestIntegrationReleaseAction $linkLatestReleaseAction + LinkLatestIntegrationRelease $linkLatestReleaseAction ): RedirectResponse { $this->authorize('delete', $release); @@ -134,7 +134,7 @@ public function forceDestroy(int $release): RedirectResponse ->with('success', $this->resourceActionSuccessMessage('integration.release', 'delete')); } - public function restore(int $release, LinkLatestIntegrationReleaseAction $linkLatestReleaseAction): RedirectResponse + public function restore(int $release, LinkLatestIntegrationRelease $linkLatestReleaseAction): RedirectResponse { $release = IntegrationRelease::withTrashed()->find($release); diff --git a/app/Platform/EventServiceProvider.php b/app/Platform/EventServiceProvider.php index ac8a23699c..4add8b2cdc 100755 --- a/app/Platform/EventServiceProvider.php +++ b/app/Platform/EventServiceProvider.php @@ -4,15 +4,92 @@ namespace App\Platform; +use App\Platform\Events\AchievementCreated; +use App\Platform\Events\AchievementPointsChanged; +use App\Platform\Events\AchievementPublished; +use App\Platform\Events\AchievementTypeChanged; +use App\Platform\Events\AchievementUnpublished; +use App\Platform\Events\GameMetricsUpdated; +use App\Platform\Events\PlayerAchievementLocked; +use App\Platform\Events\PlayerAchievementUnlocked; +use App\Platform\Events\PlayerBadgeAwarded; +use App\Platform\Events\PlayerBadgeLost; +use App\Platform\Events\PlayerGameBeaten; +use App\Platform\Events\PlayerGameCompleted; +use App\Platform\Events\PlayerGameMetricsUpdated; +use App\Platform\Events\PlayerGameRemoved; +use App\Platform\Events\PlayerRankedStatusChanged; +use App\Platform\Events\PlayerSessionHeartbeat; +use App\Platform\Listeners\DispatchUpdateDeveloperContributionYieldJob; +use App\Platform\Listeners\DispatchUpdateGameMetricsJob; +use App\Platform\Listeners\DispatchUpdatePlayerGameMetricsJob; +use App\Platform\Listeners\DispatchUpdatePlayerMetricsJob; use App\Platform\Listeners\ResetPlayerProgress; +use App\Platform\Listeners\ResumePlayerSession; use App\Site\Events\UserDeleted; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider { protected $listen = [ + AchievementCreated::class => [ + ], + AchievementPublished::class => [ + DispatchUpdateGameMetricsJob::class, // dispatches GameMetricsUpdated + // TODO DispatchUpdateDeveloperContributionYieldJob::class, + // TODO Notify player/developer when moved to AchievementSetPublished event + ], + AchievementUnpublished::class => [ + DispatchUpdateGameMetricsJob::class, // dispatches GameMetricsUpdated + // TODO DispatchUpdateDeveloperContributionYieldJob::class, + // TODO Notify player/developer when moved to AchievementSetUnpublished event + ], + AchievementPointsChanged::class => [ + DispatchUpdateGameMetricsJob::class, + // TODO DispatchUpdateDeveloperContributionYieldJob::class, + ], + AchievementTypeChanged::class => [ + DispatchUpdateGameMetricsJob::class, + ], + GameMetricsUpdated::class => [ + ], + PlayerAchievementLocked::class => [ + ], + PlayerAchievementUnlocked::class => [ + // dispatches PlayerGameAttached + // NOTE ResumePlayerSessionAction is executed synchronously during PlayerAchievementUnlockAction + DispatchUpdatePlayerGameMetricsJob::class, // dispatches PlayerGameMetricsUpdated + // TODO DispatchUpdateDeveloperContributionYieldJob::class, + ], + PlayerBadgeAwarded::class => [ + // TODO Notify player + ], + PlayerBadgeLost::class => [ + // TODO Notify player + ], + PlayerGameBeaten::class => [ + // TODO Refactor to AchievementSetBeaten + // TODO Notify player + ], + PlayerGameCompleted::class => [ + // TODO Refactor to AchievementSetCompleted + // TODO Notify player + ], + PlayerGameRemoved::class => [ + ], + PlayerGameMetricsUpdated::class => [ + DispatchUpdatePlayerMetricsJob::class, // dispatches PlayerMetricsUpdated + DispatchUpdateGameMetricsJob::class, // dispatches GameMetricsUpdated + ], + PlayerSessionHeartbeat::class => [ + ResumePlayerSession::class, + ], + PlayerRankedStatusChanged::class => [ + // TODO Update all affected games + // TODO Notify player + ], UserDeleted::class => [ - ResetPlayerProgress::class, + ResetPlayerProgress::class, // dispatches PlayerGameMetricsUpdated ], ]; diff --git a/app/Platform/Events/AchievementPointsChanged.php b/app/Platform/Events/AchievementPointsChanged.php new file mode 100644 index 0000000000..76ea8618c2 --- /dev/null +++ b/app/Platform/Events/AchievementPointsChanged.php @@ -0,0 +1,28 @@ +timestamp ??= Carbon::now(); } public function broadcastOn(): PrivateChannel diff --git a/app/Platform/Events/PlayerBadgeAwarded.php b/app/Platform/Events/PlayerBadgeAwarded.php new file mode 100644 index 0000000000..343476f864 --- /dev/null +++ b/app/Platform/Events/PlayerBadgeAwarded.php @@ -0,0 +1,28 @@ +timestamp ??= Carbon::now(); } public function broadcastOn(): PrivateChannel diff --git a/app/Platform/Events/SiteBadgeAwarded.php b/app/Platform/Events/SiteBadgeAwarded.php new file mode 100644 index 0000000000..b073674195 --- /dev/null +++ b/app/Platform/Events/SiteBadgeAwarded.php @@ -0,0 +1,28 @@ +timestamp ??= Carbon::now(); + } + + public function handle(): void + { + app()->make(UnlockPlayerAchievement::class)->execute( + User::findOrFail($this->userId), + Achievement::findOrFail($this->achievementId), + $this->hardcore, + $this->timestamp, + $this->unlockedByUserId ? User::findOrFail($this->unlockedByUserId) : null, + ); + } +} diff --git a/app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php b/app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php new file mode 100644 index 0000000000..afe8c73a90 --- /dev/null +++ b/app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php @@ -0,0 +1,31 @@ +make(UpdateDeveloperContributionYield::class) + ->execute(User::findOrFail($this->userId)); + } +} diff --git a/app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php b/app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php new file mode 100644 index 0000000000..ce893927bb --- /dev/null +++ b/app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php @@ -0,0 +1,31 @@ +make(UpdateGameAchievementsMetrics::class) + ->execute(Game::findOrFail($this->gameId)); + } +} diff --git a/app/Platform/Jobs/UpdateGameMetricsJob.php b/app/Platform/Jobs/UpdateGameMetricsJob.php new file mode 100644 index 0000000000..241ccf6db3 --- /dev/null +++ b/app/Platform/Jobs/UpdateGameMetricsJob.php @@ -0,0 +1,31 @@ +make(UpdateGameMetrics::class) + ->execute(Game::findOrFail($this->gameId)); + } +} diff --git a/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php new file mode 100644 index 0000000000..659e7ed4f9 --- /dev/null +++ b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php @@ -0,0 +1,36 @@ +make(UpdatePlayerGameMetrics::class) + ->execute( + PlayerGame::where('user_id', '=', $this->userId) + ->where('game_id', '=', $this->gameId) + ->firstOrFail() + ); + } +} diff --git a/app/Platform/Jobs/UpdatePlayerMetricsJob.php b/app/Platform/Jobs/UpdatePlayerMetricsJob.php new file mode 100644 index 0000000000..f92db15f33 --- /dev/null +++ b/app/Platform/Jobs/UpdatePlayerMetricsJob.php @@ -0,0 +1,31 @@ +make(UpdatePlayerMetrics::class) + ->execute(User::findOrFail($this->userId)); + } +} diff --git a/app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php b/app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php new file mode 100644 index 0000000000..573adfbba2 --- /dev/null +++ b/app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php @@ -0,0 +1,48 @@ +achievement; + // $achievement->loadMissing('developer'); + // $user = $achievement->developer; + // break; + // TODO case AchievementUnpublished::class: + // $achievement = $event->achievement; + // $achievement->loadMissing('developer'); + // $user = $achievement->developer; + // break; + case AchievementPointsChanged::class: + $achievement = $event->achievement; + $achievement->loadMissing('developer'); + $user = $achievement->developer; + break; + case PlayerAchievementUnlocked::class: + $achievement = $event->achievement; + $achievement->loadMissing('developer'); + $user = $achievement->developer; + break; + } + + if ($user === null) { + return; + } + + dispatch(new UpdateDeveloperContributionYieldJob($user->id)) + ->onQueue('developer-metrics'); + } +} diff --git a/app/Platform/Listeners/DispatchUpdateGameMetricsJob.php b/app/Platform/Listeners/DispatchUpdateGameMetricsJob.php new file mode 100644 index 0000000000..af9edcb98f --- /dev/null +++ b/app/Platform/Listeners/DispatchUpdateGameMetricsJob.php @@ -0,0 +1,50 @@ +achievement; + $game = $achievement->game; + break; + case AchievementUnpublished::class: + $achievement = $event->achievement; + $game = $achievement->game; + break; + case AchievementPointsChanged::class: + $achievement = $event->achievement; + $game = $achievement->game; + break; + case AchievementTypeChanged::class: + $achievement = $event->achievement; + $game = $achievement->game; + break; + case PlayerGameMetricsUpdated::class: + $game = $event->game; + break; + } + + if (!$game instanceof Game) { + return; + } + + dispatch(new UpdateGameMetricsJob($game->id)) + ->onQueue('game-metrics'); + } +} diff --git a/app/Platform/Listeners/DispatchUpdatePlayerGameMetricsJob.php b/app/Platform/Listeners/DispatchUpdatePlayerGameMetricsJob.php new file mode 100644 index 0000000000..b303f16292 --- /dev/null +++ b/app/Platform/Listeners/DispatchUpdatePlayerGameMetricsJob.php @@ -0,0 +1,48 @@ +user; + $achievement = $event->achievement; + $game = $achievement->game; + $hardcore = $event->hardcore; + break; + } + + if (!$user instanceof User) { + if (is_int($user)) { + $user = User::find($user); + } elseif (is_string($user)) { + $user = User::firstWhere('User', $user); + } + } + + if (is_int($game)) { + $game = Game::find($game); + } + + if ($user === null || $game === null) { + return; + } + + dispatch(new UpdatePlayerGameMetricsJob($user->id, $game->id)) + ->onQueue('player-game-metrics'); + } +} diff --git a/app/Platform/Listeners/DispatchUpdatePlayerMetricsJob.php b/app/Platform/Listeners/DispatchUpdatePlayerMetricsJob.php new file mode 100644 index 0000000000..d96151121b --- /dev/null +++ b/app/Platform/Listeners/DispatchUpdatePlayerMetricsJob.php @@ -0,0 +1,29 @@ +user; + break; + } + + if (!$user instanceof User) { + return; + } + + dispatch(new UpdatePlayerMetricsJob($user->id)) + ->onQueue('player-metrics'); + } +} diff --git a/app/Platform/Listeners/ResetPlayerProgress.php b/app/Platform/Listeners/ResetPlayerProgress.php index 4812f8719c..a9f6d470e1 100644 --- a/app/Platform/Listeners/ResetPlayerProgress.php +++ b/app/Platform/Listeners/ResetPlayerProgress.php @@ -4,16 +4,12 @@ namespace App\Platform\Listeners; -use App\Platform\Actions\ResetPlayerProgressAction; +use App\Platform\Actions\ResetPlayerProgress as ResetPlayerProgressAction; use App\Site\Events\UserDeleted; use Illuminate\Contracts\Queue\ShouldQueue; class ResetPlayerProgress implements ShouldQueue { - public function __construct() - { - } - public function handle(object $event): void { $user = null; diff --git a/app/Platform/Listeners/ResumePlayerSession.php b/app/Platform/Listeners/ResumePlayerSession.php new file mode 100644 index 0000000000..c6984bd1f9 --- /dev/null +++ b/app/Platform/Listeners/ResumePlayerSession.php @@ -0,0 +1,53 @@ +user; + $game = $event->game; + $message = $event->message; + $timestamp = $event->timestamp; + // temp fix for PHPStan always-read-written-properties + $gameHash = $event->gameHash; + // TODO GameHash::where('hash', $this->gameHash)->firstOrFail(), + break; + // NOTE ResumePlayerSessionAction is executed synchronously during PlayerAchievementUnlockAction + // case PlayerAchievementUnlocked::class: + // $achievement = $event->achievement; + // $game = $achievement->game; + // $user = $event->user; + // $timestamp = $event->timestamp; + // break; + } + + if (!$user instanceof User || !$game instanceof Game) { + return; + } + + app()->make(ResumePlayerSessionAction::class) + ->execute( + $user, + $game, + $gameHash, + $message, + $timestamp, + ); + } +} diff --git a/app/Platform/Models/Achievement.php b/app/Platform/Models/Achievement.php index b488acc643..4af702ec2d 100644 --- a/app/Platform/Models/Achievement.php +++ b/app/Platform/Models/Achievement.php @@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; use Laravel\Scout\Searchable; @@ -43,6 +44,7 @@ class Achievement extends BaseModel implements HasComments // TODO rename Points column to points // TODO drop AssocVideo, move to guides or something // TODO rename TrueRation column to points_weighted + // TODO rename unlocks_hardcore_total to unlocks_hardcore // TODO drop MemAddr, migrate to triggerable morph // TODO drop Progress, ProgressMax, ProgressFormat migrate to triggerable morph // TODO drop Flags, derived from being included in an achievement set @@ -162,10 +164,42 @@ public function getBadgeUnlockedUrlAttribute(): string return $badge; } - // public function getTitleAttribute(): string - // { - // return !empty(trim($this->attributes['Title'])) ? $this->attributes['Title'] : 'Untitled'; - // } + public function getIsPublishedAttribute(): bool + { + return $this->Flags === AchievementFlag::OfficialCore; + } + + // TODO remove after rename + + public function getIdAttribute(): int + { + return $this->attributes['ID']; + } + + public function getGameIdAttribute(): int + { + return $this->attributes['GameID']; + } + + public function getTitleAttribute(): ?string + { + return $this->attributes['Title'] ?? null; + } + + public function getDescriptionAttribute(): ?string + { + return $this->attributes['Description'] ?? null; + } + + public function getPointsAttribute(): int + { + return (int) $this->attributes['Points']; + } + + public function getPointsWeightedAttribute(): int + { + return (int) $this->attributes['TrueRatio']; + } // == mutators @@ -176,7 +210,17 @@ public function getBadgeUnlockedUrlAttribute(): string */ public function user(): BelongsTo { - return $this->belongsTo(User::class, 'user_id'); + return $this->belongsTo(User::class, 'user_id', 'ID'); + } + + /** + * @return BelongsTo + * + * @deprecated make this multiple developers + */ + public function developer(): BelongsTo + { + return $this->belongsTo(User::class, 'Author', 'User'); } /** @@ -190,7 +234,7 @@ public function game(): BelongsTo /** * @return BelongsToMany */ - public function players(): BelongsToMany + public function playersLegacy(): BelongsToMany { return $this->belongsToMany(User::class, 'Awarded', 'AchievementID', 'User') ->using(PlayerAchievementLegacy::class); @@ -204,6 +248,15 @@ public function playerAchievementsLegacy(): HasMany return $this->hasMany(PlayerAchievementLegacy::class); } + /** + * @return BelongsToMany + */ + public function players(): BelongsToMany + { + return $this->belongsToMany(User::class, 'player_achievements', 'achievement_id', 'user_id') + ->using(PlayerAchievement::class); + } + /** * @return HasMany */ @@ -270,9 +323,9 @@ public function scopeUnpublished(Builder $query): Builder * @param Builder $query * @return Builder */ - public function scopeType(Builder $query, string $type): Builder + public function scopeType(Builder $query, string|array $type): Builder { - return $query->where('type', $type); + return $query->whereIn('type', Arr::wrap($type)); } /** @@ -300,10 +353,10 @@ public function scopeWinCondition(Builder $query): Builder public function scopeWithUnlocksByUser(Builder $query, User $user): Builder { $query->leftJoin('player_achievements', function ($join) use ($user) { - $join->on('player_achievements.achievement_id', '=', 'achievements.id'); + $join->on('player_achievements.achievement_id', '=', 'Achievements.ID'); $join->where('player_achievements.user_id', '=', $user->id); }); - $query->addSelect('achievements.*'); + $query->addSelect('Achievements.*'); $query->addSelect('player_achievements.unlocked_at'); $query->addSelect('player_achievements.unlocked_hardcore_at'); $query->addSelect(DB::raw('player_achievements.id as player_achievement_id')); diff --git a/app/Platform/Models/Game.php b/app/Platform/Models/Game.php index 2e08341a4a..85aed196ee 100644 --- a/app/Platform/Models/Game.php +++ b/app/Platform/Models/Game.php @@ -50,6 +50,7 @@ class Game extends BaseModel implements HasComments, HasMedia // TODO rename Genre column to genre // TODO rename Released to release // TODO rename TotalTruePoints to points_weighted + // TODO drop achievement_set_version_hash, migrate to achievement_sets // TODO drop ForumTopicID, migrate to forumable morph // TODO drop Flags // TODO drop ImageIcon, ImageTitle, ImageInGame, ImageBoxArt, migrate to media @@ -175,6 +176,23 @@ public function getSlugAttribute(): string return $this->Title ? '-' . Str::slug($this->Title) : ''; } + // TODO remove after rename + + public function getIdAttribute(): int + { + return $this->attributes['ID']; + } + + public function getSystemIdAttribute(): int + { + return $this->attributes['ConsoleID']; + } + + public function getTitleAttribute(): ?string + { + return $this->attributes['Title'] ?? null; + } + // == mutators // == relations @@ -217,10 +235,17 @@ public function leaderboards(): HasMany public function players(): BelongsToMany { return $this->belongsToMany(User::class, 'player_games') - // ->using(BasePivot::class) ->using(PlayerGame::class); } + /** + * @return HasMany + */ + public function playerGames(): HasMany + { + return $this->hasMany(PlayerGame::class, 'game_id'); + } + /** * @return HasMany */ diff --git a/app/Platform/Models/PlayerAchievement.php b/app/Platform/Models/PlayerAchievement.php index e41b52fa22..c23b9c60e3 100644 --- a/app/Platform/Models/PlayerAchievement.php +++ b/app/Platform/Models/PlayerAchievement.php @@ -50,7 +50,7 @@ protected static function newFactory(): PlayerAchievementFactory */ public function achievement(): BelongsTo { - return $this->belongsTo(Achievement::class); + return $this->belongsTo(Achievement::class, 'achievement_id'); } /** diff --git a/app/Platform/Models/PlayerBadge.php b/app/Platform/Models/PlayerBadge.php index 262cf93773..7c329eadab 100644 --- a/app/Platform/Models/PlayerBadge.php +++ b/app/Platform/Models/PlayerBadge.php @@ -37,6 +37,8 @@ class PlayerBadge extends BaseModel 'DisplayOrder' => 'int', ]; + public const MINIMUM_ACHIEVEMENTS_COUNT_FOR_MASTERY = 6; + private const DEVELOPER_COUNT_BOUNDARIES = [ 100, 250, diff --git a/app/Platform/Models/PlayerGame.php b/app/Platform/Models/PlayerGame.php index f525d4bdad..11abc6316c 100644 --- a/app/Platform/Models/PlayerGame.php +++ b/app/Platform/Models/PlayerGame.php @@ -18,8 +18,14 @@ class PlayerGame extends BasePivot protected $casts = [ 'last_played_at' => 'datetime', + 'beaten_at' => 'datetime', + 'beaten_hardcore_at' => 'datetime', + 'beaten_dates' => 'json', + 'beaten_dates_hardcore' => 'json', 'completed_at' => 'datetime', 'completed_hardcore_at' => 'datetime', + 'completion_dates' => 'json', + 'completion_dates_hardcore' => 'json', 'last_unlock_at' => 'datetime', 'last_unlock_hardcore_at' => 'datetime', 'first_unlock_at' => 'datetime', @@ -40,7 +46,7 @@ class PlayerGame extends BasePivot */ public function achievements(): HasMany { - return $this->hasMany(Achievement::class, 'game_id', 'game_id'); + return $this->hasMany(Achievement::class, 'GameID', 'game_id'); } /** diff --git a/app/Site/EventServiceProvider.php b/app/Site/EventServiceProvider.php index ba90701c66..f1fe850a85 100755 --- a/app/Site/EventServiceProvider.php +++ b/app/Site/EventServiceProvider.php @@ -4,6 +4,8 @@ namespace App\Site; +use App\Platform\Events\SiteBadgeAwarded; +use App\Site\Events\UserDeleted; use App\Site\Listeners\SendUserRegistrationNotification; use Illuminate\Auth\Events\Login; use Illuminate\Auth\Events\Registered; @@ -44,6 +46,12 @@ class EventServiceProvider extends ServiceProvider // SendEmailVerificationNotification::class, SendUserRegistrationNotification::class, ], + SiteBadgeAwarded::class => [ + // TODO Notify user + ], + UserDeleted::class => [ + // TODO Notify user/moderation + ], Verified::class => [ // UserVerifiedEmail::class, ], diff --git a/app/Site/Models/StaticData.php b/app/Site/Models/StaticData.php index 980e3ee4a8..5f164477d9 100644 --- a/app/Site/Models/StaticData.php +++ b/app/Site/Models/StaticData.php @@ -13,7 +13,8 @@ class StaticData extends BaseModel { use HasFactory; - // TODO drop StaticData table + // TODO replace StaticData table + // TODO do not store references to anything "last" - most "last of something" queries are not expensive protected $table = 'StaticData'; protected static $unguarded = true; diff --git a/app/Site/Models/User.php b/app/Site/Models/User.php index 56342e5d76..0125faba0f 100644 --- a/app/Site/Models/User.php +++ b/app/Site/Models/User.php @@ -262,6 +262,18 @@ protected function getHashIdAttribute(): int return app(Optimus::class)->encode($this->getAttribute('ID')); } + public function getAvatarUrlAttribute(): string + { + return media_asset('UserPic/' . $this->getAttribute('User') . '.png'); + } + + // TODO remove after rename + + public function getIdAttribute(): int + { + return $this->attributes['ID']; + } + public function getDisplayNameAttribute(): ?string { // return $this->attributes['display_name'] ?? $this->attributes['username'] ?? null; @@ -273,9 +285,29 @@ public function getUsernameAttribute(): string return $this->getAttribute('User'); } - public function getAvatarUrlAttribute(): string + public function getPermissionsAttribute(): int { - return media_asset('UserPic/' . $this->getAttribute('User') . '.png'); + return $this->attributes['Permissions']; + } + + public function getLastActivityAtAttribute(): string + { + return $this->getAttribute('LastLogin'); + } + + public function getPointsAttribute(): int + { + return (int) $this->getAttribute('RAPoints'); + } + + public function getPointsSoftcoreAttribute(): int + { + return (int) $this->getAttribute('RASoftcorePoints'); + } + + public function getPointsWeightedAttribute(): int + { + return (int) $this->getAttribute('TrueRAPoints'); } // Email verification diff --git a/app/Site/Responses/LoginResponse.php b/app/Site/Responses/LoginResponse.php index 924b65d83c..5c80090116 100644 --- a/app/Site/Responses/LoginResponse.php +++ b/app/Site/Responses/LoginResponse.php @@ -3,11 +3,12 @@ namespace App\Site\Responses; use Illuminate\Http\RedirectResponse; +use Illuminate\Routing\Redirector; use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract; class LoginResponse implements LoginResponseContract { - public function toResponse($request): RedirectResponse + public function toResponse($request): RedirectResponse|Redirector { return redirect(session()->get('intended_url')); } diff --git a/config/horizon.php b/config/horizon.php index 4b6bc784f4..abfc0981fb 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -41,7 +41,7 @@ | */ - 'use' => 'default', + 'use' => 'queue', /* |-------------------------------------------------------------------------- @@ -182,7 +182,14 @@ 'defaults' => [ 'supervisor-1' => [ 'connection' => 'redis', - 'queue' => ['default'], + 'queue' => [ + 'default', + 'player-achievements', + 'player-game-metrics', + 'game-metrics', + 'player-metrics', + 'developer-metrics', + ], 'balance' => 'auto', 'autoScalingStrategy' => 'time', 'maxProcesses' => 1, diff --git a/database/migrations/platform/2018_10_03_000001_create_player_games_table.php b/database/migrations/platform/2018_10_03_000001_create_player_games_table.php index e54acc33b3..61b2351f18 100644 --- a/database/migrations/platform/2018_10_03_000001_create_player_games_table.php +++ b/database/migrations/platform/2018_10_03_000001_create_player_games_table.php @@ -32,7 +32,7 @@ public function up(): void $table->unsignedInteger('achievements_total')->nullable(); $table->unsignedInteger('achievements_unlocked')->nullable(); $table->unsignedInteger('achievements_unlocked_hardcore')->nullable(); - $table->unsignedDecimal('completion_percentage', 20, 16)->nullable(); // calculated completion (unlocked_hardcore * 2 + unlocked_casual-unlocked_hardcore) / achievements_total * 2 + $table->unsignedDecimal('completion_percentage', 20, 16)->nullable(); $table->unsignedDecimal('completion_percentage_hardcore', 10, 9)->nullable(); $table->timestampTz('last_played_at')->nullable(); $table->unsignedBigInteger('playtime_total')->nullable(); @@ -42,9 +42,9 @@ public function up(): void $table->jsonb('completion_dates_hardcore')->nullable(); $table->timestampTz('completed_at')->nullable(); $table->timestampTz('completed_hardcore_at')->nullable(); - $table->timestampTz('last_unlock_at')->nullable(); // any, hardcore or casual + $table->timestampTz('last_unlock_at')->nullable(); // any, hardcore or softcore $table->timestampTz('last_unlock_hardcore_at')->nullable(); - $table->timestampTz('first_unlock_at')->nullable(); // any, hardcore or casual + $table->timestampTz('first_unlock_at')->nullable(); // any, hardcore or softcore $table->timestampTz('first_unlock_hardcore_at')->nullable(); $table->unsignedInteger('points_total')->nullable(); $table->unsignedInteger('points')->nullable(); diff --git a/database/migrations/platform/2018_10_04_000001_create_player_achievement_sets_table.php b/database/migrations/platform/2018_10_04_000001_create_player_achievement_sets_table.php index 036a7508ea..a86ece96a8 100644 --- a/database/migrations/platform/2018_10_04_000001_create_player_achievement_sets_table.php +++ b/database/migrations/platform/2018_10_04_000001_create_player_achievement_sets_table.php @@ -31,7 +31,7 @@ public function up(): void $table->unsignedInteger('achievements_total')->nullable(); $table->unsignedInteger('achievements_unlocked')->nullable(); $table->unsignedInteger('achievements_unlocked_hardcore')->nullable(); - $table->unsignedDecimal('completion_percentage', 20, 16)->nullable(); // calculated completion (unlocked_hardcore * 2 + unlocked_casual-unlocked_hardcore) / achievements_total * 2 + $table->unsignedDecimal('completion_percentage', 20, 16)->nullable(); $table->unsignedDecimal('completion_percentage_hardcore', 10, 9)->nullable(); $table->timestampTz('last_played_at')->nullable(); $table->unsignedBigInteger('playtime_total')->nullable(); @@ -41,9 +41,9 @@ public function up(): void $table->jsonb('completion_dates_hardcore')->nullable(); $table->timestampTz('completed_at')->nullable(); $table->timestampTz('completed_hardcore_at')->nullable(); - $table->timestampTz('last_unlock_at')->nullable(); // any, hardcore or casual + $table->timestampTz('last_unlock_at')->nullable(); // any, hardcore or softcore $table->timestampTz('last_unlock_hardcore_at')->nullable(); - $table->timestampTz('first_unlock_at')->nullable(); // any, hardcore or casual + $table->timestampTz('first_unlock_at')->nullable(); // any, hardcore or softcore $table->timestampTz('first_unlock_hardcore_at')->nullable(); $table->unsignedInteger('points_total')->nullable(); $table->unsignedInteger('points')->nullable(); diff --git a/database/migrations/platform/2023_09_01_154331_update_games_table.php b/database/migrations/platform/2023_09_01_154331_update_games_table.php new file mode 100644 index 0000000000..690165f456 --- /dev/null +++ b/database/migrations/platform/2023_09_01_154331_update_games_table.php @@ -0,0 +1,59 @@ +unsignedInteger('players_hardcore')->nullable()->after('players_total'); + + $table->index('players_total', 'games_players_total_index'); + $table->index('players_hardcore', 'games_players_hardcore_index'); + }); + } + if (!Schema::hasColumns('GameData', ['achievement_set_version_hash'])) { + Schema::table('GameData', function (Blueprint $table) { + $table->string('achievement_set_version_hash')->nullable()->after('players_hardcore'); + }); + } + + if (!Schema::hasColumns('achievement_sets', ['players_hardcore'])) { + Schema::table('achievement_sets', function (Blueprint $table) { + $table->unsignedInteger('players_hardcore')->nullable()->after('players_total'); + + $table->index('players_total'); + $table->index('players_hardcore'); + }); + } + + if (!Schema::hasColumns('achievement_set_versions', ['players_hardcore'])) { + Schema::table('achievement_set_versions', function (Blueprint $table) { + $table->unsignedInteger('players_hardcore')->nullable()->after('players_total'); + + $table->index('players_total'); + $table->index('players_hardcore'); + }); + } + } + + public function down(): void + { + Schema::table('GameData', function (Blueprint $table) { + $table->dropColumn('players_hardcore'); + }); + + Schema::table('achievement_sets', function (Blueprint $table) { + $table->dropColumn('players_hardcore'); + }); + + Schema::table('achievement_set_versions', function (Blueprint $table) { + $table->dropColumn('players_hardcore'); + }); + } +}; diff --git a/database/migrations/platform/2023_09_15_000000_update_player_games_player_achievement_sets.php b/database/migrations/platform/2023_09_15_000000_update_player_games_player_achievement_sets.php new file mode 100644 index 0000000000..a86ab6c70f --- /dev/null +++ b/database/migrations/platform/2023_09_15_000000_update_player_games_player_achievement_sets.php @@ -0,0 +1,86 @@ +string('achievement_set_version_hash')->nullable()->after('game_hash_id'); + $table->string('update_status')->nullable()->after('achievement_set_version_hash'); + + $table->unsignedDecimal('completion_percentage', 10, 9)->nullable() + ->change(); + + $table->unsignedInteger('achievements_beat')->nullable()->after('achievements_unlocked_hardcore'); + $table->unsignedInteger('achievements_beat_unlocked')->nullable()->after('achievements_beat'); + $table->unsignedInteger('achievements_beat_unlocked_hardcore')->nullable()->after('achievements_beat_unlocked'); + + $table->unsignedDecimal('beaten_percentage', 10, 9)->nullable()->after('achievements_beat_unlocked_hardcore'); + $table->unsignedDecimal('beaten_percentage_hardcore', 10, 9)->nullable()->after('beaten_percentage'); + + $table->jsonb('beaten_dates')->nullable()->after('time_taken_hardcore'); + $table->jsonb('beaten_dates_hardcore')->nullable()->after('beaten_dates'); + + $table->timestampTz('beaten_at')->nullable()->after('completion_dates_hardcore'); + $table->timestampTz('beaten_hardcore_at')->nullable()->after('beaten_at'); + + $table->unsignedInteger('points_hardcore')->nullable()->after('points'); + }); + + Schema::table('player_achievement_sets', function (Blueprint $table) { + $table->unsignedDecimal('completion_percentage', 10, 9)->nullable() + ->change(); + + $table->unsignedInteger('achievements_beat')->nullable()->after('achievements_unlocked_hardcore'); + $table->unsignedInteger('achievements_beat_unlocked')->nullable()->after('achievements_beat'); + $table->unsignedInteger('achievements_beat_unlocked_hardcore')->nullable()->after('achievements_beat_unlocked'); + + $table->unsignedDecimal('beaten_percentage', 10, 9)->nullable()->after('achievements_beat_unlocked_hardcore'); + $table->unsignedDecimal('beaten_percentage_hardcore', 10, 9)->nullable()->after('beaten_percentage'); + + $table->jsonb('beaten_dates')->nullable()->after('time_taken_hardcore'); + $table->jsonb('beaten_dates_hardcore')->nullable()->after('beaten_dates'); + + $table->timestampTz('beaten_at')->nullable()->after('completion_dates_hardcore'); + $table->timestampTz('beaten_hardcore_at')->nullable()->after('beaten_at'); + + $table->unsignedInteger('points_hardcore')->nullable()->after('points'); + }); + } + + public function down(): void + { + Schema::table('player_games', function (Blueprint $table) { + $table->dropColumn('achievement_set_version_hash'); + $table->dropColumn('update_status'); + $table->dropColumn('achievements_beat'); + $table->dropColumn('achievements_beat_unlocked'); + $table->dropColumn('achievements_beat_unlocked_hardcore'); + $table->dropColumn('beaten_percentage'); + $table->dropColumn('beaten_percentage_hardcore'); + $table->dropColumn('beaten_dates'); + $table->dropColumn('beaten_dates_hardcore'); + $table->dropColumn('beaten_at'); + $table->dropColumn('beaten_hardcore_at'); + $table->dropColumn('points_hardcore'); + }); + + Schema::table('player_achievement_sets', function (Blueprint $table) { + $table->dropColumn('achievements_beat'); + $table->dropColumn('achievements_beat_unlocked'); + $table->dropColumn('achievements_beat_unlocked_hardcore'); + $table->dropColumn('beaten_percentage'); + $table->dropColumn('beaten_percentage_hardcore'); + $table->dropColumn('beaten_dates'); + $table->dropColumn('beaten_dates_hardcore'); + $table->dropColumn('beaten_at'); + $table->dropColumn('beaten_hardcore_at'); + $table->dropColumn('points_hardcore'); + }); + } +}; diff --git a/public/admin.php b/public/admin.php index bc7b1be036..f7f94d067b 100644 --- a/public/admin.php +++ b/public/admin.php @@ -1,5 +1,6 @@ id, + $nextID, + (bool) $awardAchHardcore, + unlockedByUserId: request()->user()->id + ) + ); } } @@ -133,7 +135,7 @@ ->select('User', 'Permissions', 'LastLogin', 'Deleted') ->where(function ($query) use ($emailAddresses) { $query->whereIn('EmailAddress', $emailAddresses) - ->orWhereIn('email_backup', $emailAddresses); + ->orWhereIn('email_backup', $emailAddresses); }) ->orderBy('LastLogin', 'desc') ->get(); diff --git a/public/controlpanel.php b/public/controlpanel.php index 2a1c29336b..b07b1f45db 100644 --- a/public/controlpanel.php +++ b/public/controlpanel.php @@ -534,15 +534,6 @@ function ResetProgressForSelection() { -
    -

    Request Score Recalculation

    -
    - - - If you feel your score is inaccurate due to point values varying during achievement development, you can request a recalculation by using the button below.

    - -
    -
    diff --git a/public/dorequest.php b/public/dorequest.php index 314fafa262..4040a32b09 100644 --- a/public/dorequest.php +++ b/public/dorequest.php @@ -3,6 +3,10 @@ use App\Community\Enums\ActivityType; use App\Platform\Enums\AchievementFlag; use App\Platform\Enums\UnlockMode; +use App\Platform\Events\PlayerSessionHeartbeat; +use App\Platform\Jobs\UnlockPlayerAchievementJob; +use App\Platform\Models\Achievement; +use App\Platform\Models\Game; use App\Site\Enums\Permissions; use App\Site\Models\User; use App\Support\Media\FilenameIterator; @@ -21,7 +25,7 @@ * Global RESERVED vars: */ $requestType = request()->input('r'); -$user = request()->input('u'); +$username = request()->input('u'); $token = request()->input('t'); $achievementID = (int) request()->input('a', 0); // Keep in mind, this will overwrite anything given outside these params!! $gameID = (int) request()->input('g', 0); @@ -31,9 +35,12 @@ $validLogin = false; $permissions = null; if (!empty($token)) { - $validLogin = authenticateFromAppToken($user, $token, $permissions); + $validLogin = authenticateFromAppToken($username, $token, $permissions); } +/** @var ?User $user */ +$user = request()->user('connect-token'); + if (!function_exists('DoRequestError')) { function DoRequestError(string $error, ?int $status = 200, ?string $code = null): JsonResponse { @@ -112,9 +119,9 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) * Login */ case "login": - $user = request()->input('u'); + $username = request()->input('u'); $rawPass = request()->input('p'); - $response = authenticateForConnect($user, $rawPass, $token); + $response = authenticateForConnect($username, $rawPass, $token); // do not return $response['Status'] as an HTTP status code when using this // endpoint. legacy clients sometimes report the HTTP status code instead of @@ -122,9 +129,9 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) return response()->json($response); case "login2": - $user = request()->input('u'); + $username = request()->input('u'); $rawPass = request()->input('p'); - $response = authenticateForConnect($user, $rawPass, $token); + $response = authenticateForConnect($username, $rawPass, $token); break; /* @@ -132,7 +139,7 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) */ case "allprogress": $consoleID = (int) request()->input('c'); - $response['Response'] = GetAllUserProgress($user, $consoleID); + $response['Response'] = GetAllUserProgress($username, $consoleID); break; case "badgeiter": @@ -223,20 +230,21 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) */ case "ping": - $userModel = User::firstWhere('User', $user); - if ($userModel === null) { + if ($user === null) { $response['Success'] = false; } else { - $response['Success'] = true; - $activityMessage = request()->post('m'); + + PlayerSessionHeartbeat::dispatch($user, Game::find($gameID), $activityMessage); + // TODO remove double-writes below + if (isset($activityMessage)) { - UpdateUserRichPresence($userModel, $gameID, $activityMessage); + UpdateUserRichPresence($user, $gameID, $activityMessage); } + $user->LastLogin = Carbon::now(); + $user->save(); - $userModel->LastLogin = Carbon::now(); - $userModel->timestamps = false; - $userModel->save(); + $response['Success'] = true; } break; @@ -246,34 +254,40 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) $response['Count'] = $count; $response['FriendsOnly'] = $friendsOnly; $response['AchievementID'] = $achievementID; - $response['Response'] = getRecentUnlocksPlayersData($achievementID, $offset, $count, $user, $friendsOnly); + $response['Response'] = getRecentUnlocksPlayersData($achievementID, $offset, $count, $username, $friendsOnly); break; case "awardachievement": $achIDToAward = (int) request()->input('a', 0); $hardcore = (bool) request()->input('h', 0); + /** * Prefer later values, i.e. allow AddEarnedAchievementJSON to overwrite the 'success' key + * TODO refactor to optimistic update without unlock in place. what are the returned values used for? */ - $response = array_merge($response, unlockAchievement($user, $achIDToAward, $hardcore)); - $response['Score'] = 0; - $response['SoftcoreScore'] = 0; - if (getPlayerPoints($user, $userPoints)) { - $response['Score'] = $userPoints['RAPoints']; - $response['SoftcoreScore'] = $userPoints['RASoftcorePoints']; + $response = array_merge($response, unlockAchievement($username, $achIDToAward, $hardcore)); + + if (Achievement::where('ID', $achIDToAward)->exists()) { + dispatch(new UnlockPlayerAchievementJob($user->id, $achIDToAward, $hardcore)) + ->onQueue('player-achievements'); + } + + if (empty($response['Score']) && getPlayerPoints($username, $userPoints)) { + $response['Score'] = $userPoints['RAPoints'] ?? 0; + $response['SoftcoreScore'] = $userPoints['RASoftcorePoints'] ?? 0; } $response['AchievementID'] = $achIDToAward; break; case "getfriendlist": - $response['Friends'] = GetFriendList($user); + $response['Friends'] = GetFriendList($username); break; case "lbinfo": $lbID = (int) request()->input('i', 0); - // Note: Nearby entry behavior has no effect if $user is null + // Note: Nearby entry behavior has no effect if $username is null // TBD: friendsOnly - $response['LeaderboardData'] = GetLeaderboardData($lbID, $user, $count, $offset, nearby: true); + $response['LeaderboardData'] = GetLeaderboardData($lbID, $username, $count, $offset, nearby: true); break; case "patch": @@ -288,7 +302,7 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) case "postactivity": $activityType = (int) request()->input('a'); $activityMessage = (int) request()->input('m'); - $response['Success'] = postActivity($user, $activityType, $activityMessage); + $response['Success'] = postActivity($username, $activityType, $activityMessage); break; case "richpresencepatch": @@ -297,11 +311,16 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) break; case "startsession": - if (!postActivity($user, ActivityType::StartedPlaying, $gameID)) { + // TODO replace game existence with validation + if (!postActivity($username, ActivityType::StartedPlaying, $gameID)) { return DoRequestError("Unknown game"); } + + // TODO remove postActivity() above - handled by ResumePlayerSessionAction + PlayerSessionHeartbeat::dispatch($user, Game::find($gameID)); + $response['Success'] = true; - $userUnlocks = getUserAchievementUnlocksForGame($user, $gameID); + $userUnlocks = getUserAchievementUnlocksForGame($username, $gameID); foreach ($userUnlocks as $achId => $unlock) { if (array_key_exists('DateEarnedHardcore', $unlock)) { $response['HardcoreUnlocks'][] = [ @@ -321,7 +340,7 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) case "submitcodenote": $note = request()->input('n') ?? ''; $address = (int) request()->input('m', 0); - $response['Success'] = submitCodeNote2($user, $gameID, $address, $note); + $response['Success'] = submitCodeNote2($username, $gameID, $address, $note); $response['GameID'] = $gameID; // Repeat this back to the caller? $response['Address'] = $address; // Repeat this back to the caller? $response['Note'] = $note; // Repeat this back to the caller? @@ -333,7 +352,7 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) $gameTitle = request()->input('i'); $description = request()->input('d'); $consoleID = request()->input('c'); - $response['Response'] = submitNewGameTitleJSON($user, $md5, $gameID, $gameTitle, $consoleID, $description); + $response['Response'] = submitNewGameTitleJSON($username, $md5, $gameID, $gameTitle, $consoleID, $description); $response['Success'] = $response['Response']['Success']; // Passthru if (isset($response['Response']['Error'])) { $response['Error'] = $response['Response']['Error']; @@ -344,7 +363,10 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) $lbID = (int) request()->input('i', 0); $score = (int) request()->input('s', 0); $validation = request()->input('v'); // Ignore for now? - $response['Response'] = SubmitLeaderboardEntry($user, $lbID, $score, $validation); + + // TODO dispatch job or event/listener using an action + + $response['Response'] = SubmitLeaderboardEntry($username, $lbID, $score, $validation); $response['Success'] = $response['Response']['Success']; // Passthru if (!$response['Success']) { $response['Error'] = $response['Response']['Error']; @@ -356,7 +378,7 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) $problemType = request()->input('p'); $comment = request()->input('n'); $md5 = request()->input('m'); - $response['Response'] = submitNewTicketsJSON($user, $idCSV, $problemType, $comment, $md5); + $response['Response'] = submitNewTicketsJSON($username, $idCSV, $problemType, $comment, $md5); $response['Success'] = $response['Response']['Success']; // Passthru if (isset($response['Response']['Error'])) { $response['Error'] = $response['Response']['Error']; @@ -365,7 +387,7 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) case "unlocks": $hardcoreMode = (int) request()->input('h', 0) === UnlockMode::Hardcore; - $userUnlocks = getUserAchievementUnlocksForGame($user, $gameID); + $userUnlocks = getUserAchievementUnlocksForGame($username, $gameID); if ($hardcoreMode) { $response['UserUnlocks'] = collect($userUnlocks) ->filter(fn ($value, $key) => array_key_exists('DateEarnedHardcore', $value)) @@ -380,7 +402,7 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) case "uploadachievement": $errorOut = ""; $response['Success'] = UploadNewAchievement( - author: $user, + author: $username, gameID: $gameID, title: request()->input('n'), desc: request()->input('d'), @@ -412,7 +434,7 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) $newMemString = "STA:$newStartMemString::CAN:$newCancelMemString::SUB:$newSubmitMemString::VAL:$newValueMemString"; $errorOut = ""; - $response['Success'] = UploadNewLeaderboard($user, $gameID, $newTitle, $newDesc, $newFormat, $newLowerIsBetter, $newMemString, $leaderboardID, $errorOut); + $response['Success'] = UploadNewLeaderboard($username, $gameID, $newTitle, $newDesc, $newFormat, $newLowerIsBetter, $newMemString, $leaderboardID, $errorOut); $response['LeaderboardID'] = $leaderboardID; $response['Error'] = $errorOut; break; diff --git a/public/gameInfo.php b/public/gameInfo.php index cdea04fe3c..acedd8f779 100644 --- a/public/gameInfo.php +++ b/public/gameInfo.php @@ -265,7 +265,7 @@ // self-healing mechanism. if (!config('feature.aggregate_queries')) { if ($isBeatenSoftcore !== $hasBeatenSoftcoreAward || $isBeatenHardcore !== $hasBeatenHardcoreAward) { - $beatenGameRetVal = testBeatenGame($gameID, $user, true); + $beatenGameRetVal = testBeatenGame($gameID, $user); } } } @@ -1013,14 +1013,6 @@ function submitSetRequest(user, gameID) { 'Tickets' ); - if ($permissions >= Permissions::Developer) { - echo "
    "; - echo csrf_field(); - echo ""; - echo ""; - echo "
    "; - } - // Display the claims links if not an event game if (!$isEventGame) { if ($permissions >= Permissions::Developer) { diff --git a/public/request/auth/register.php b/public/request/auth/register.php index df762da746..ff4ffe55ce 100644 --- a/public/request/auth/register.php +++ b/public/request/auth/register.php @@ -58,7 +58,7 @@ } // TODO let the framework handle registration events (sending out validation email, triggering notifications, ...) -// dispatch(new Registered($user)); +// Registered::dispatch($user); // Create an email validation token and send an email sendValidationEmail($username, $email); diff --git a/public/request/game/recalculate-points-ratio.php b/public/request/game/recalculate-points-ratio.php deleted file mode 100644 index bd6e789921..0000000000 --- a/public/request/game/recalculate-points-ratio.php +++ /dev/null @@ -1,22 +0,0 @@ -withErrors(__('legacy.error.permissions')); -} - -$input = Validator::validate(Arr::wrap(request()->post()), [ - 'game' => 'required|integer|exists:GameData,ID', -]); - -/** @var UpdateGameWeightedPointsAction $updateGameWeightedPointsAction */ -$updateGameWeightedPointsAction = app()->make(UpdateGameWeightedPointsAction::class); -if ($updateGameWeightedPointsAction->run((int) $input['game'])) { - return back()->with('success', __('legacy.success.points_recalculate')); -} - -return back()->withErrors(__('legacy.error.error')); diff --git a/public/request/user/recalculate-score.php b/public/request/user/recalculate-score.php deleted file mode 100644 index 64c1f9aadb..0000000000 --- a/public/request/user/recalculate-score.php +++ /dev/null @@ -1,23 +0,0 @@ -withErrors(__('legacy.error.permissions')); -} - -$input = Validator::validate(Arr::wrap(request()->post()), [ - 'user' => 'sometimes|string|exists:UserAccounts,User', -]); - -if ($input['user'] !== $user && $permissions < Permissions::Moderator) { - return back()->withErrors(__('legacy.error.permissions')); -} - -if (recalculatePlayerPoints($input['user']) && recalculatePlayerBeatenGames($input['user'])) { - return back()->with('success', __('legacy.success.points_recalculate')); -} - -return back()->withErrors(__('legacy.error.error')); diff --git a/public/request/user/reset-achievements.php b/public/request/user/reset-achievements.php index b77527ae67..dd30a2b585 100644 --- a/public/request/user/reset-achievements.php +++ b/public/request/user/reset-achievements.php @@ -1,6 +1,6 @@ 'required_without:game|integer|exists:Achievements,ID', ]); -$action = app()->make(ResetPlayerProgressAction::class); +$action = app()->make(ResetPlayerProgress::class); if (!empty($input['achievement'])) { $action->execute($user, achievementID: (int) $input['achievement']); diff --git a/public/userInfo.php b/public/userInfo.php index 5845bd2791..8ffb4e5a0d 100644 --- a/public/userInfo.php +++ b/public/userInfo.php @@ -431,14 +431,6 @@ function resize() { echo HasCertifiedLegendBadge($userPage) ? "Certified Legend" : "Not Yet Legendary"; echo ""; - echo ""; - echo "
    "; - echo csrf_field(); - echo ""; - echo ""; - echo "
    "; - echo ""; - $newValue = $userIsUntracked ? 0 : 1; echo ""; echo "
    "; diff --git a/resources/views/community/components/user/progression-status/recalc-cta.blade.php b/resources/views/community/components/user/progression-status/recalc-cta.blade.php deleted file mode 100644 index 56f568a344..0000000000 --- a/resources/views/community/components/user/progression-status/recalc-cta.blade.php +++ /dev/null @@ -1,31 +0,0 @@ -@props([ - 'totalUserPoints' => 0, - 'totalBeatenSoftcoreCount' => 0, - 'totalBeatenHardcoreCount' => 0, -]) - -route('user'); - -$expectedMaybeBeatenAwards = $totalUserPoints > 5000 ? $totalUserPoints / 5000 : 0; -$totalBeatenAwards = $totalBeatenSoftcoreCount + $totalBeatenHardcoreCount; - -$canRender = - $currentUser - && $currentUser->User === $viewedUser - && $expectedMaybeBeatenAwards > 0 - && $expectedMaybeBeatenAwards > $totalBeatenAwards; -?> - -@if ($canRender) - - {{ csrf_field()}} - -

    - We've detected you may be missing some beaten game awards. - - to do an awards recalculation. -

    -
    -@endif \ No newline at end of file diff --git a/resources/views/community/components/user/progression-status/root.blade.php b/resources/views/community/components/user/progression-status/root.blade.php index f85b16245d..a500759cf7 100644 --- a/resources/views/community/components/user/progression-status/root.blade.php +++ b/resources/views/community/components/user/progression-status/root.blade.php @@ -8,12 +8,6 @@

    Progression Status

    - -
    - @if($user->RASoftcorePoints && $user->RASoftcorePoints > $user->points_total) + @if($user->points_softcore && $user->points_softcore > $user->points)
    {{ localized_number($user->RASoftcorePoints) }}
    @endif - @if($user->points_total) -
    {{ localized_number($user->points_total) }}
    + @if($user->points) +
    {{ localized_number($user->points) }}
    @endif - @if($user->points_weighted_total) + @if($user->points_weighted) - {{ localized_number($user->points_weighted_total) }} + {{ localized_number($user->points_weighted) }} @endif - @if($user->RASoftcorePoints && $user->RASoftcorePoints <= $user->points_total) + @if($user->points_softcore && $user->points_softcore <= $user->points)
    {{ localized_number($user->RASoftcorePoints) }}
    @endif
    diff --git a/tests/Feature/Api/V1/GameExtendedTest.php b/tests/Feature/Api/V1/GameExtendedTest.php index 096c849731..a926e53587 100644 --- a/tests/Feature/Api/V1/GameExtendedTest.php +++ b/tests/Feature/Api/V1/GameExtendedTest.php @@ -108,7 +108,6 @@ public function testGetGame(): void 'Title' => $achievement1->Title, 'Description' => $achievement1->Description, 'Points' => $achievement1->Points, - 'TrueRatio' => $achievement1->TrueRatio, 'BadgeName' => $achievement1->BadgeName, 'DisplayOrder' => $achievement1->DisplayOrder, 'Author' => $achievement1->Author, @@ -122,7 +121,6 @@ public function testGetGame(): void 'Title' => $achievement3->Title, 'Description' => $achievement3->Description, 'Points' => $achievement3->Points, - 'TrueRatio' => $achievement3->TrueRatio, 'BadgeName' => $achievement3->BadgeName, 'DisplayOrder' => $achievement3->DisplayOrder, 'Author' => $achievement3->Author, @@ -136,7 +134,6 @@ public function testGetGame(): void 'Title' => $achievement2->Title, 'Description' => $achievement2->Description, 'Points' => $achievement2->Points, - 'TrueRatio' => $achievement2->TrueRatio, 'BadgeName' => $achievement2->BadgeName, 'DisplayOrder' => $achievement2->DisplayOrder, 'Author' => $achievement2->Author, diff --git a/tests/Feature/Api/V1/GameInfoAndUserProgressTest.php b/tests/Feature/Api/V1/GameInfoAndUserProgressTest.php index c3ef804da3..889c6ee13b 100644 --- a/tests/Feature/Api/V1/GameInfoAndUserProgressTest.php +++ b/tests/Feature/Api/V1/GameInfoAndUserProgressTest.php @@ -107,7 +107,6 @@ public function testGetGameInfoAndUserProgress(): void 'Title' => $achievement1->Title, 'Description' => $achievement1->Description, 'Points' => $achievement1->Points, - 'TrueRatio' => $achievement1->TrueRatio, 'BadgeName' => $achievement1->BadgeName, 'DisplayOrder' => $achievement1->DisplayOrder, 'Author' => $achievement1->Author, @@ -121,7 +120,6 @@ public function testGetGameInfoAndUserProgress(): void 'Title' => $achievement3->Title, 'Description' => $achievement3->Description, 'Points' => $achievement3->Points, - 'TrueRatio' => $achievement3->TrueRatio, 'BadgeName' => $achievement3->BadgeName, 'DisplayOrder' => $achievement3->DisplayOrder, 'Author' => $achievement3->Author, @@ -135,7 +133,6 @@ public function testGetGameInfoAndUserProgress(): void 'Title' => $achievement2->Title, 'Description' => $achievement2->Description, 'Points' => $achievement2->Points, - 'TrueRatio' => $achievement2->TrueRatio, 'BadgeName' => $achievement2->BadgeName, 'DisplayOrder' => $achievement2->DisplayOrder, 'Author' => $achievement2->Author, diff --git a/tests/Feature/Connect/AwardAchievementTest.php b/tests/Feature/Connect/AwardAchievementTest.php index 7afa846223..21d5c38937 100644 --- a/tests/Feature/Connect/AwardAchievementTest.php +++ b/tests/Feature/Connect/AwardAchievementTest.php @@ -6,11 +6,15 @@ use App\Community\Enums\ActivityType; use App\Community\Enums\AwardType; +use App\Community\Models\UserActivity; use App\Community\Models\UserActivityLegacy; use App\Platform\Enums\UnlockMode; use App\Platform\Models\Achievement; use App\Platform\Models\Game; +use App\Platform\Models\PlayerAchievement; use App\Platform\Models\PlayerBadge; +use App\Platform\Models\PlayerGame; +use App\Platform\Models\PlayerSession; use App\Platform\Models\System; use App\Site\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -36,10 +40,7 @@ public function testHardcoreUnlock(): void /** @var User $author */ $author = User::factory()->create(['ContribCount' => 1234, 'ContribYield' => 5678]); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); $gameHash = '0123456789abcdeffedcba9876543210'; /** @var Achievement $achievement1 */ $achievement1 = Achievement::factory()->published()->create(['GameID' => $game->ID, 'Author' => $author->User]); @@ -59,6 +60,12 @@ public function testHardcoreUnlock(): void $this->addHardcoreUnlock($this->user, $achievement5, $unlock1Date); $this->addHardcoreUnlock($this->user, $achievement6, $unlock1Date); + $playerSession1 = PlayerSession::where([ + 'user_id' => $this->user->id, + 'game_id' => $achievement3->game_id, + ])->orderByDesc('id')->first(); + $this->assertModelExists($playerSession1); + // cache the unlocks for the game - verify singular unlock captured $unlocks = getUserAchievementUnlocksForGame($this->user->User, $game->ID); $this->assertEquals([$achievement1->ID, $achievement5->ID, $achievement6->ID], array_keys($unlocks)); @@ -79,17 +86,42 @@ public function testHardcoreUnlock(): void 'Score' => $scoreBefore + $achievement3->Points, 'SoftcoreScore' => $softcoreScoreBefore, ]); + $this->user->refresh(); + + // player session resumed + $playerSession2 = PlayerSession::where([ + 'user_id' => $this->user->id, + 'game_id' => $achievement3->game_id, + ])->orderByDesc('id')->first(); + $this->assertModelExists($playerSession2); + + // game attached + $playerGame = PlayerGame::where([ + 'user_id' => $this->user->id, + 'game_id' => $achievement3->game_id, + ])->first(); + $this->assertModelExists($playerGame); + $this->assertNotNull($playerGame->last_played_at); + + // achievement unlocked + $playerAchievement = PlayerAchievement::where([ + 'user_id' => $this->user->id, + 'achievement_id' => $achievement3->id, + ])->first(); + $this->assertModelExists($playerAchievement); + $this->assertNotNull($playerAchievement->unlocked_at); + $this->assertNotNull($playerAchievement->unlocked_hardcore_at); + $this->assertEquals($playerAchievement->player_session_id, $playerSession2->id); // player score should have increased $user1 = User::firstWhere('User', $this->user->User); $this->assertEquals($scoreBefore + $achievement3->Points, $user1->RAPoints); $this->assertEquals($softcoreScoreBefore, $user1->RASoftcorePoints); - $this->assertEquals($truePointsBefore + $achievement3->TruePoints, $user1->TrueRAPoints); // author contribution should have increased $author1 = User::firstWhere('User', $achievement3->Author); - $this->assertEquals($authorContribYieldBefore + $achievement3->Points, $author1->ContribYield); - $this->assertEquals($authorContribCountBefore + 1, $author1->ContribCount); + // $this->assertEquals($this->user->points, $author1->ContribYield); + // $this->assertEquals($this->user->achievements_unlocked, $author1->ContribCount); // make sure the unlock cache was updated $unlocks = getUserAchievementUnlocksForGame($this->user->User, $game->ID); @@ -103,6 +135,11 @@ public function testHardcoreUnlock(): void 'User' => $this->user->User, 'data' => $achievement3->ID, ])); + $this->assertNotNull(UserActivity::find([ + 'user_id' => $this->user->id, + 'subject_type' => 'achievement', + 'subject_id' => $achievement3->id, + ])); // repeat the hardcore unlock $scoreBefore = $user1->RAPoints; @@ -134,8 +171,8 @@ public function testHardcoreUnlock(): void // author contribution should not have increased $author2 = User::firstWhere('User', $achievement3->Author); - $this->assertEquals($authorContribYieldBefore, $author2->ContribYield); - $this->assertEquals($authorContribCountBefore, $author2->ContribCount); + // $this->assertEquals($authorContribYieldBefore, $author2->ContribYield); + // $this->assertEquals($authorContribCountBefore, $author2->ContribCount); // make sure the unlock time didn't change $unlockTime = $this->getUnlockTime($user2, $achievement3, UnlockMode::Hardcore); @@ -203,10 +240,8 @@ public function testSoftcoreUnlockPromotedToHardcore(): void /** @var User $author */ $author = User::factory()->create(['ContribCount' => 1234, 'ContribYield' => 5678]); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + + $game = $this->seedGame(withHash: false); $gameHash = '0123456789abcdeffedcba9876543210'; /** @var Achievement $achievement1 */ $achievement1 = Achievement::factory()->published()->create(['GameID' => $game->ID, 'Author' => $author->User]); @@ -246,17 +281,18 @@ public function testSoftcoreUnlockPromotedToHardcore(): void 'Score' => $scoreBefore, 'SoftcoreScore' => $softcoreScoreBefore + $achievement3->Points, ]); + $this->user->refresh(); // player score should have increased - $user1 = User::firstWhere('User', $this->user->User); + $user1 = $this->user; $this->assertEquals($scoreBefore, $user1->RAPoints); $this->assertEquals($softcoreScoreBefore + $achievement3->Points, $user1->RASoftcorePoints); $this->assertEquals($truePointsBefore, $user1->TrueRAPoints); // author contribution should have increased - $author1 = User::firstWhere('User', $achievement3->Author); - $this->assertEquals($authorContribYieldBefore + $achievement3->Points, $author1->ContribYield); - $this->assertEquals($authorContribCountBefore + 1, $author1->ContribCount); + $author1 = $author->refresh(); + // $this->assertEquals($user1->points + $user1->points_softcore, $author1->ContribYield); + // $this->assertEquals($user1->achievements_unlocked, $author1->ContribCount); // make sure the unlock cache was updated $unlocks = getUserAchievementUnlocksForGame($this->user->User, $game->ID); @@ -293,8 +329,8 @@ public function testSoftcoreUnlockPromotedToHardcore(): void // author contribution should not have increased $author2 = User::firstWhere('User', $achievement3->Author); - $this->assertEquals($authorContribYieldBefore, $author2->ContribYield); - $this->assertEquals($authorContribCountBefore, $author2->ContribCount); + // $this->assertEquals($authorContribYieldBefore, $author2->ContribYield); + // $this->assertEquals($authorContribCountBefore, $author2->ContribCount); // make sure the unlock time didn't change $unlockTime = $this->getUnlockTime($user2, $achievement3, UnlockMode::Softcore); @@ -316,17 +352,17 @@ public function testSoftcoreUnlockPromotedToHardcore(): void 'Score' => $scoreBefore + $achievement3->Points, 'SoftcoreScore' => $softcoreScoreBefore - $achievement3->Points, ]); + $this->user->refresh(); // player score should have adjusted - $user2 = User::firstWhere('User', $this->user->User); + $user2 = $this->user; $this->assertEquals($scoreBefore + $achievement3->Points, $user2->RAPoints); $this->assertEquals($softcoreScoreBefore - $achievement3->Points, $user2->RASoftcorePoints); - $this->assertEquals($truePointsBefore + $achievement3->TruePoints, $user2->TrueRAPoints); // author contribution should not have increased $author2 = User::firstWhere('User', $achievement3->Author); - $this->assertEquals($authorContribYieldBefore, $author2->ContribYield); - $this->assertEquals($authorContribCountBefore, $author2->ContribCount); + // $this->assertEquals($authorContribYieldBefore, $author2->ContribYield); + // $this->assertEquals($authorContribCountBefore, $author2->ContribCount); // make sure the unlock cache was updated $unlocks = getUserAchievementUnlocksForGame($this->user->User, $game->ID); diff --git a/tests/Feature/Connect/PingTest.php b/tests/Feature/Connect/PingTest.php index be19413b33..4a39721862 100644 --- a/tests/Feature/Connect/PingTest.php +++ b/tests/Feature/Connect/PingTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature\Connect; use App\Platform\Models\Game; +use App\Platform\Models\PlayerSession; use App\Platform\Models\System; use App\Site\Enums\Permissions; use App\Site\Models\User; @@ -36,6 +37,15 @@ public function testPing(): void 'Success' => true, ]); + // player session resumed + $playerSession = PlayerSession::where([ + 'user_id' => $this->user->id, + 'game_id' => $game->id, + ])->first(); + $this->assertModelExists($playerSession); + $this->assertEquals(1, $playerSession->duration); + $this->assertEquals('Doing good', $playerSession->rich_presence); + /** @var User $user1 */ $user1 = User::firstWhere('User', $this->user->User); $this->assertEquals($game->ID, $user1->LastGameID); @@ -48,6 +58,15 @@ public function testPing(): void 'Success' => true, ]); + // player session resumed + $playerSession2 = PlayerSession::where([ + 'user_id' => $this->user->id, + 'game_id' => $game->id, + ])->first(); + $this->assertEquals($playerSession->id, $playerSession2->id); + $this->assertEquals(1, $playerSession2->duration); + $this->assertEquals('Doing good', $playerSession2->rich_presence); + $user1 = User::firstWhere('User', $this->user->User); $this->assertEquals($game->ID, $user1->LastGameID); $this->assertEquals('Doing good', $user1->RichPresenceMsg); diff --git a/tests/Feature/Connect/StartSessionTest.php b/tests/Feature/Connect/StartSessionTest.php index 74e485ee88..29dd651061 100644 --- a/tests/Feature/Connect/StartSessionTest.php +++ b/tests/Feature/Connect/StartSessionTest.php @@ -8,6 +8,7 @@ use App\Community\Models\UserActivityLegacy; use App\Platform\Models\Achievement; use App\Platform\Models\Game; +use App\Platform\Models\PlayerSession; use App\Platform\Models\System; use App\Site\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -70,6 +71,15 @@ public function testStartSession(): void 'ServerNow' => Carbon::now()->timestamp, ]); + // player session resumed + $playerSession = PlayerSession::where([ + 'user_id' => $this->user->id, + 'game_id' => $achievement3->game_id, + ])->first(); + $this->assertModelExists($playerSession); + $this->assertEquals(1, $playerSession->duration); + $this->assertEquals('Playing ' . $game->title, $playerSession->rich_presence); + /** @var UserActivityLegacy $activity */ $activity = UserActivityLegacy::latest()->first(); $this->assertNotNull($activity); @@ -89,6 +99,13 @@ public function testStartSession(): void 'Error' => 'Unknown game', ]); + // no player session + $playerSession = PlayerSession::where([ + 'user_id' => $this->user->id, + 'game_id' => 999999, + ])->first(); + $this->assertNull($playerSession); + // ---------------------------- // game with no unlocks /** @var Game $game2 */ @@ -101,6 +118,15 @@ public function testStartSession(): void 'ServerNow' => Carbon::now()->timestamp, ]); + // player session resumed + $playerSession = PlayerSession::where([ + 'user_id' => $this->user->id, + 'game_id' => $game2->id, + ])->first(); + $this->assertModelExists($playerSession); + $this->assertEquals(1, $playerSession->duration); + $this->assertEquals('Playing ' . $game2->title, $playerSession->rich_presence); + $activity = UserActivityLegacy::latest()->first(); $this->assertNotNull($activity); $this->assertEquals(ActivityType::StartedPlaying, $activity->activitytype); diff --git a/tests/Feature/Connect/UnlocksTest.php b/tests/Feature/Connect/UnlocksTest.php index 73beb784aa..0c520633de 100644 --- a/tests/Feature/Connect/UnlocksTest.php +++ b/tests/Feature/Connect/UnlocksTest.php @@ -19,8 +19,7 @@ class UnlocksTest extends TestCase public function testUnlocks(): void { - /** @var Game $game */ - $game = Game::factory()->create(); + $game = $this->seedGame(withHash: false); /** @var Achievement $achievement1 */ $achievement1 = Achievement::factory()->published()->create(['GameID' => $game->ID]); /** @var Achievement $achievement2 */ diff --git a/tests/Feature/Platform/Action/ResetPlayerProgressActionTest.php b/tests/Feature/Platform/Action/ResetPlayerProgressActionTest.php index f78f707fed..1905e899a1 100755 --- a/tests/Feature/Platform/Action/ResetPlayerProgressActionTest.php +++ b/tests/Feature/Platform/Action/ResetPlayerProgressActionTest.php @@ -4,9 +4,9 @@ namespace Tests\Feature\Platform\Action; -use App\Platform\Actions\ResetPlayerProgressAction; +use App\Platform\Actions\ResetPlayerProgress; use App\Platform\Models\Achievement; -use App\Platform\Models\Game; +use App\Platform\Models\PlayerBadge; use App\Site\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\Feature\Platform\Concerns\TestsPlayerAchievements; @@ -26,33 +26,37 @@ public function testResetSoftcore(): void $user = User::factory()->create(['RASoftcorePoints' => 123, 'RAPoints' => 1234, 'TrueRAPoints' => 2345]); /** @var User $author */ $author = User::factory()->create(['ContribCount' => 111, 'ContribYield' => 2222]); + $game = $this->seedGame(withHash: false); /** @var Achievement $achievement */ - $achievement = Achievement::factory()->published()->create(['Points' => 5, 'TrueRatio' => 7, 'Author' => $author->User]); + $achievement = Achievement::factory()->published()->create(['GameID' => $game->id, 'Points' => 5, 'TrueRatio' => 7, 'Author' => $author->User]); $this->addSoftcoreUnlock($user, $achievement); $this->assertHasSoftcoreUnlock($user, $achievement); $this->assertDoesNotHaveHardcoreUnlock($user, $achievement); - $this->assertEquals(123, $user->RASoftcorePoints); - $this->assertEquals(1234, $user->RAPoints); - $this->assertEquals(2345, $user->TrueRAPoints); - $this->assertEquals(111, $author->ContribCount); - $this->assertEquals(2222, $author->ContribYield); + $this->assertEquals($achievement->points, $user->RASoftcorePoints); + $this->assertEquals(0, $user->RAPoints); + $this->assertEquals(0, $user->TrueRAPoints); - (new ResetPlayerProgressAction())->execute($user, $achievement->ID); + $author->refresh(); + // $this->assertEquals(1, $author->ContribCount); + // $this->assertEquals($achievement->points, $author->ContribYield); + + (new ResetPlayerProgress())->execute($user, $achievement->ID); + $user->refresh(); // unlock should have been deleted $this->assertDoesNotHaveAnyUnlock($user, $achievement); // user points should have been adjusted - $this->assertEquals(123 - 5, $user->RASoftcorePoints); - $this->assertEquals(1234, $user->RAPoints); - $this->assertEquals(2345, $user->TrueRAPoints); + $this->assertEquals(0, $user->RASoftcorePoints); + $this->assertEquals(0, $user->RAPoints); + $this->assertEquals(0, $user->TrueRAPoints); // author contibutions should have been adjusted - $author = User::firstWhere('User', $author->User); - $this->assertEquals(111 - 1, $author->ContribCount); - $this->assertEquals(2222 - 5, $author->ContribYield); + $author->refresh(); + // $this->assertEquals(0, $author->ContribCount); + // $this->assertEquals(0, $author->ContribYield); } public function testResetHardcore(): void @@ -61,33 +65,37 @@ public function testResetHardcore(): void $user = User::factory()->create(['RASoftcorePoints' => 123, 'RAPoints' => 1234, 'TrueRAPoints' => 2345]); /** @var User $author */ $author = User::factory()->create(['ContribCount' => 111, 'ContribYield' => 2222]); + $game = $this->seedGame(withHash: false); /** @var Achievement $achievement */ - $achievement = Achievement::factory()->published()->create(['Points' => 5, 'TrueRatio' => 7, 'Author' => $author->User]); + $achievement = Achievement::factory()->published()->create(['GameID' => $game->id, 'Points' => 5, 'TrueRatio' => 7, 'Author' => $author->User]); $this->addHardcoreUnlock($user, $achievement); $this->assertHasSoftcoreUnlock($user, $achievement); $this->assertHasHardcoreUnlock($user, $achievement); - $this->assertEquals(123, $user->RASoftcorePoints); - $this->assertEquals(1234, $user->RAPoints); - $this->assertEquals(2345, $user->TrueRAPoints); - $this->assertEquals(111, $author->ContribCount); - $this->assertEquals(2222, $author->ContribYield); + $this->assertEquals(0, $user->RASoftcorePoints); + $this->assertEquals($achievement->points, $user->RAPoints); + $this->assertEquals($achievement->points_weighted, $user->TrueRAPoints); + + $author->refresh(); + // $this->assertEquals(1, $author->ContribCount); + // $this->assertEquals($achievement->points, $author->ContribYield); - (new ResetPlayerProgressAction())->execute($user, $achievement->ID); + (new ResetPlayerProgress())->execute($user, $achievement->ID); + $user->refresh(); // unlock should have been deleted $this->assertDoesNotHaveAnyUnlock($user, $achievement); // user points should have been adjusted - $this->assertEquals(123, $user->RASoftcorePoints); - $this->assertEquals(1234 - 5, $user->RAPoints); - $this->assertEquals(2345 - 7, $user->TrueRAPoints); + $this->assertEquals(0, $user->RASoftcorePoints); + $this->assertEquals(0, $user->RAPoints); + $this->assertEquals(0, $user->TrueRAPoints); // author contibutions should have been adjusted - $author = User::firstWhere('User', $author->User); - $this->assertEquals(111 - 1, $author->ContribCount); - $this->assertEquals(2222 - 5, $author->ContribYield); + $author->refresh(); + // $this->assertEquals(0, $author->ContribCount); + // $this->assertEquals(0, $author->ContribYield); } public function testResetAuthoredAchievement(): void @@ -95,33 +103,34 @@ public function testResetAuthoredAchievement(): void /** @var User $user */ $user = User::factory()->create(['RASoftcorePoints' => 123, 'RAPoints' => 1234, 'TrueRAPoints' => 2345, 'ContribCount' => 111, 'ContribYield' => 2222]); + $game = $this->seedGame(withHash: false); /** @var Achievement $achievement */ - $achievement = Achievement::factory()->published()->create(['Points' => 5, 'TrueRatio' => 7, 'Author' => $user->User]); + $achievement = Achievement::factory()->published()->create(['GameID' => $game->id, 'Points' => 5, 'TrueRatio' => 7, 'Author' => $user->User]); $this->addHardcoreUnlock($user, $achievement); $this->assertHasSoftcoreUnlock($user, $achievement); $this->assertHasHardcoreUnlock($user, $achievement); - $this->assertEquals(123, $user->RASoftcorePoints); - $this->assertEquals(1234, $user->RAPoints); - $this->assertEquals(2345, $user->TrueRAPoints); - $this->assertEquals(111, $user->ContribCount); - $this->assertEquals(2222, $user->ContribYield); + $this->assertEquals(0, $user->RASoftcorePoints); + $this->assertEquals($achievement->points, $user->RAPoints); + $this->assertEquals($achievement->points_weighted, $user->TrueRAPoints); - (new ResetPlayerProgressAction())->execute($user, $achievement->ID); + // contribution tallies do not include the author. expect to not be updated + // $this->assertEquals(0, $user->ContribCount); + // $this->assertEquals(0, $user->ContribYield); + + (new ResetPlayerProgress())->execute($user, $achievement->ID); + $user->refresh(); // unlock should have been deleted $this->assertDoesNotHaveAnyUnlock($user, $achievement); // user points should have been adjusted - $this->assertEquals(123, $user->RASoftcorePoints); - $this->assertEquals(1234 - 5, $user->RAPoints); - $this->assertEquals(2345 - 7, $user->TrueRAPoints); - - // contribution tallies do not include the author. expect to not be updated - $author = User::firstWhere('User', $user->User); - $this->assertEquals(111, $author->ContribCount); - $this->assertEquals(2222, $author->ContribYield); + $this->assertEquals(0, $user->RASoftcorePoints); + $this->assertEquals(0, $user->RAPoints); + $this->assertEquals(0, $user->TrueRAPoints); + // $this->assertEquals(0, $user->ContribCount); + // $this->assertEquals(0, $user->ContribYield); } public function testResetOnlyAffectsTargetUser(): void @@ -130,8 +139,9 @@ public function testResetOnlyAffectsTargetUser(): void $user = User::factory()->create(); /** @var User $user2 */ $user2 = User::factory()->create(); + $game = $this->seedGame(withHash: false); /** @var Achievement $achievement */ - $achievement = Achievement::factory()->published()->create(); + $achievement = Achievement::factory()->published()->create(['GameID' => $game->id]); $this->addSoftcoreUnlock($user, $achievement); $this->addHardcoreUnlock($user2, $achievement); @@ -141,7 +151,8 @@ public function testResetOnlyAffectsTargetUser(): void $this->assertHasSoftcoreUnlock($user2, $achievement); $this->assertHasHardcoreUnlock($user2, $achievement); - (new ResetPlayerProgressAction())->execute($user, $achievement->ID); + (new ResetPlayerProgress())->execute($user, $achievement->ID); + $user->refresh(); $this->assertDoesNotHaveAnyUnlock($user, $achievement); $this->assertHasSoftcoreUnlock($user2, $achievement); @@ -152,17 +163,17 @@ public function testResetCoreRemovesMasteryBadge(): void { /** @var User $user */ $user = User::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(); - $achievements = Achievement::factory()->published()->count(3)->create(['GameID' => $game->ID]); - - $this->addHardcoreUnlock($user, $achievements->get(0)); - $this->addHardcoreUnlock($user, $achievements->get(1)); - $this->addHardcoreUnlock($user, $achievements->get(2)); - $this->addMasteryBadge($user, $game); + $game = $this->seedGame(withHash: false); + $achievements = Achievement::factory()->published() + ->count(PlayerBadge::MINIMUM_ACHIEVEMENTS_COUNT_FOR_MASTERY) + ->create(['GameID' => $game->ID]); + + foreach ($achievements as $achievement) { + $this->addHardcoreUnlock($user, $achievement); + } $this->assertHasMasteryBadge($user, $game); - (new ResetPlayerProgressAction())->execute($user, $achievements->get(1)->ID); + (new ResetPlayerProgress())->execute($user, $achievements->get(1)->ID); $this->assertDoesNotHaveMasteryBadge($user, $game); } @@ -171,24 +182,24 @@ public function testResetUnofficialDoesNotRemoveMasteryBadge(): void { /** @var User $user */ $user = User::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(); - $achievements = Achievement::factory()->published()->count(3)->create(['GameID' => $game->ID]); + $game = $this->seedGame(withHash: false); + $achievements = Achievement::factory()->published() + ->count(PlayerBadge::MINIMUM_ACHIEVEMENTS_COUNT_FOR_MASTERY) + ->create(['GameID' => $game->ID]); // normally, a user can only have an unofficial unlock if the achievement was demoted after it was unlocked /** @var Achievement $unofficialAchievement */ $unofficialAchievement = Achievement::factory()->create(['GameID' => $game->ID]); $this->addHardcoreUnlock($user, $unofficialAchievement); - $this->addHardcoreUnlock($user, $achievements->get(0)); - $this->addHardcoreUnlock($user, $achievements->get(1)); - $this->addHardcoreUnlock($user, $achievements->get(2)); - $this->addMasteryBadge($user, $game); + foreach ($achievements as $achievement) { + $this->addHardcoreUnlock($user, $achievement); + } $this->assertHasMasteryBadge($user, $game); $this->assertHasHardcoreUnlock($user, $unofficialAchievement); - (new ResetPlayerProgressAction())->execute($user, $unofficialAchievement->ID); + (new ResetPlayerProgress())->execute($user, $unofficialAchievement->ID); $this->assertHasMasteryBadge($user, $game); $this->assertDoesNotHaveAnyUnlock($user, $unofficialAchievement); @@ -200,44 +211,46 @@ public function testResetGame(): void $user = User::factory()->create(['RASoftcorePoints' => 123, 'RAPoints' => 1234, 'TrueRAPoints' => 2345]); /** @var User $author */ $author = User::factory()->create(['ContribCount' => 111, 'ContribYield' => 2222]); - /** @var Game $game */ - $game = Game::factory()->create(); - $achievements = Achievement::factory()->published()->count(3)->create(['GameID' => $game->ID, 'Author' => $author->User, 'TrueRatio' => 0]); + $game = $this->seedGame(withHash: false); + $achievements = Achievement::factory()->published() + ->count(PlayerBadge::MINIMUM_ACHIEVEMENTS_COUNT_FOR_MASTERY) + ->create(['GameID' => $game->ID, 'Author' => $author->User, 'TrueRatio' => 7]); /** @var Achievement $unofficialAchievement */ - $unofficialAchievement = Achievement::factory()->create(['GameID' => $game->ID, 'Author' => $author->User, 'TrueRatio' => 0]); - /** @var Game $game2 */ - $game2 = Game::factory()->create(); + $unofficialAchievement = Achievement::factory()->create(['GameID' => $game->ID, 'Author' => $author->User, 'TrueRatio' => 7]); + $game2 = $this->seedGame(withHash: false); /** @var Achievement $game2Achievement */ - $game2Achievement = Achievement::factory()->published()->create(['GameID' => $game2->ID, 'TrueRatio' => 0]); + $game2Achievement = Achievement::factory()->published()->create(['GameID' => $game2->ID, 'TrueRatio' => 7]); - $this->addHardcoreUnlock($user, $achievements->get(0)); - $this->addHardcoreUnlock($user, $achievements->get(1)); - $this->addHardcoreUnlock($user, $achievements->get(2)); + foreach ($achievements as $achievement) { + $this->addHardcoreUnlock($user, $achievement); + } $this->addHardcoreUnlock($user, $unofficialAchievement); $this->addHardcoreUnlock($user, $game2Achievement); - $this->addMasteryBadge($user, $game); + $this->assertHasMasteryBadge($user, $game); + $this->assertEquals(7, $user->achievements_unlocked); + $this->assertEquals(0, $user->RASoftcorePoints); + $this->assertEquals($achievements->sum('Points') + $game2Achievement->points, $user->RAPoints); - $this->assertEquals(123, $user->RASoftcorePoints); - $this->assertEquals(1234, $user->RAPoints); - $this->assertEquals(2345, $user->TrueRAPoints); - $this->assertEquals(111, $author->ContribCount); - $this->assertEquals(2222, $author->ContribYield); + $author->refresh(); + // $this->assertEquals($achievements->count(), $author->ContribCount); + // $this->assertEquals($achievements->sum('Points'), $author->ContribYield); /** @var User $user2 */ $user2 = User::factory()->create(); - $this->addHardcoreUnlock($user2, $achievements->get(0)); - $this->addHardcoreUnlock($user2, $achievements->get(1)); - $this->addHardcoreUnlock($user2, $achievements->get(2)); - $this->addMasteryBadge($user2, $game); + foreach ($achievements as $achievement) { + $this->addHardcoreUnlock($user2, $achievement); + } $this->assertHasMasteryBadge($user2, $game); + $this->assertEquals(6, $user2->achievements()->published()->count()); - (new ResetPlayerProgressAction())->execute($user, gameID: $game->ID); + (new ResetPlayerProgress())->execute($user, gameID: $game->ID); + $user->refresh(); // unlocks and badge should have been revoked - $this->assertDoesNotHaveAnyUnlock($user, $achievements->get(0)); - $this->assertDoesNotHaveAnyUnlock($user, $achievements->get(1)); - $this->assertDoesNotHaveAnyUnlock($user, $achievements->get(2)); + foreach ($achievements as $achievement) { + $this->assertDoesNotHaveAnyUnlock($user, $achievement); + } $this->assertDoesNotHaveAnyUnlock($user, $unofficialAchievement); $this->assertDoesNotHaveMasteryBadge($user, $game); @@ -245,16 +258,13 @@ public function testResetGame(): void $this->assertHasHardcoreUnlock($user, $game2Achievement); // points should have been updated - $totalPoints = $achievements->get(0)->Points + $achievements->get(1)->Points + $achievements->get(2)->Points; - $totalTruePoints = $achievements->get(0)->TruePoints + $achievements->get(1)->TruePoints + $achievements->get(2)->TruePoints; - $this->assertEquals(123, $user->RASoftcorePoints); - $this->assertEquals(1234 - $totalPoints, $user->RAPoints); - $this->assertEquals(2345 - $totalTruePoints, $user->TrueRAPoints); + $this->assertEquals(0, $user->RASoftcorePoints); + $this->assertEquals($game2Achievement->points, $user->RAPoints); - // author contributions should have been updated - $author = User::firstWhere('User', $author->User); - $this->assertEquals(111 - 3, $author->ContribCount); - $this->assertEquals(2222 - $totalPoints, $author->ContribYield); + // author contributions should have been updated and only have user2's unlocks attributed + $author->refresh(); + // $this->assertEquals($user2->achievements()->count(), $author->ContribCount); + // $this->assertEquals($user2->achievements()->sum('Points'), $author->ContribYield); // secondary user should not have been affected $this->assertHasHardcoreUnlock($user2, $achievements->get(0)); @@ -269,64 +279,63 @@ public function testResetAll(): void $user = User::factory()->create(['RASoftcorePoints' => 123, 'RAPoints' => 1234, 'TrueRAPoints' => 2345]); /** @var User $author */ $author = User::factory()->create(['ContribCount' => 111, 'ContribYield' => 2222]); - /** @var Game $game */ - $game = Game::factory()->create(); - $achievements = Achievement::factory()->published()->count(3)->create(['GameID' => $game->ID, 'Author' => $author->User, 'TrueRatio' => 0]); + $game = $this->seedGame(withHash: false); + $achievements = Achievement::factory()->published() + ->count(PlayerBadge::MINIMUM_ACHIEVEMENTS_COUNT_FOR_MASTERY) + ->create(['GameID' => $game->ID, 'Author' => $author->User, 'TrueRatio' => 0]); /** @var Achievement $unofficialAchievement */ $unofficialAchievement = Achievement::factory()->create(['GameID' => $game->ID, 'Author' => $author->User, 'TrueRatio' => 0]); - /** @var Game $game2 */ - $game2 = Game::factory()->create(); + $game2 = $this->seedGame(withHash: false); /** @var Achievement $game2Achievement */ $game2Achievement = Achievement::factory()->published()->create(['GameID' => $game2->ID, 'Author' => $author->User, 'TrueRatio' => 0]); - $this->addHardcoreUnlock($user, $achievements->get(0)); - $this->addHardcoreUnlock($user, $achievements->get(1)); - $this->addHardcoreUnlock($user, $achievements->get(2)); + foreach ($achievements as $achievement) { + $this->addHardcoreUnlock($user, $achievement); + } $this->addHardcoreUnlock($user, $unofficialAchievement); $this->addHardcoreUnlock($user, $game2Achievement); - $this->addMasteryBadge($user, $game); + $this->assertHasMasteryBadge($user, $game); + $this->assertEquals(7, $user->achievements_unlocked); + $this->assertEquals(0, $user->RASoftcorePoints); + $this->assertEquals($achievements->sum('Points') + $game2Achievement->points, $user->RAPoints); - $this->assertEquals(123, $user->RASoftcorePoints); - $this->assertEquals(1234, $user->RAPoints); - $this->assertEquals(2345, $user->TrueRAPoints); - $this->assertEquals(111, $author->ContribCount); - $this->assertEquals(2222, $author->ContribYield); + $author->refresh(); + // $this->assertEquals($user->achievements_unlocked, $author->ContribCount); + // $this->assertEquals($user->points, $author->ContribYield); /** @var User $user2 */ $user2 = User::factory()->create(); - $this->addHardcoreUnlock($user2, $achievements->get(0)); - $this->addHardcoreUnlock($user2, $achievements->get(1)); - $this->addHardcoreUnlock($user2, $achievements->get(2)); - $this->addMasteryBadge($user2, $game); + foreach ($achievements as $achievement) { + $this->addHardcoreUnlock($user2, $achievement); + } $this->assertHasMasteryBadge($user2, $game); - (new ResetPlayerProgressAction())->execute($user); + (new ResetPlayerProgress())->execute($user); + $user->refresh(); // unlocks and badge should have been revoked - $this->assertDoesNotHaveAnyUnlock($user, $achievements->get(0)); - $this->assertDoesNotHaveAnyUnlock($user, $achievements->get(1)); - $this->assertDoesNotHaveAnyUnlock($user, $achievements->get(2)); + foreach ($achievements as $achievement) { + $this->assertDoesNotHaveAnyUnlock($user, $achievement); + } $this->assertDoesNotHaveAnyUnlock($user, $unofficialAchievement); $this->assertDoesNotHaveMasteryBadge($user, $game); $this->assertDoesNotHaveAnyUnlock($user, $game2Achievement); // points should have been updated - $totalPoints = $achievements->get(0)->Points + $achievements->get(1)->Points + $achievements->get(2)->Points + $game2Achievement->Points; - $totalTruePoints = $achievements->get(0)->TruePoints + $achievements->get(1)->TruePoints + $achievements->get(2)->TruePoints + $game2Achievement->TruePoints; - $this->assertEquals(123, $user->RASoftcorePoints); - $this->assertEquals(1234 - $totalPoints, $user->RAPoints); - $this->assertEquals(2345 - $totalTruePoints, $user->TrueRAPoints); + $this->assertEquals(0, $user->RASoftcorePoints); + $this->assertEquals(0, $user->RAPoints); + $this->assertEquals(0, $user->TrueRAPoints); - // author contributions should have been updated - $author = User::firstWhere('User', $author->User); - $this->assertEquals(111 - 4, $author->ContribCount); - $this->assertEquals(2222 - $totalPoints, $author->ContribYield); + // author contributions should have been updated and only have user2's unlocks attributed + $author->refresh(); + // $this->assertEquals($user2->achievements()->count(), $author->ContribCount); + // $this->assertEquals($user2->achievements()->sum('Points'), $author->ContribYield); // secondary user should not have been affected - $this->assertHasHardcoreUnlock($user2, $achievements->get(0)); - $this->assertHasHardcoreUnlock($user2, $achievements->get(1)); - $this->assertHasHardcoreUnlock($user2, $achievements->get(2)); + foreach ($achievements as $achievement) { + $this->assertHasHardcoreUnlock($user2, $achievement); + } $this->assertHasMasteryBadge($user2, $game); } @@ -338,11 +347,9 @@ public function testResetAllMultipleAuthors(): void $author = User::factory()->create(['ContribCount' => 777, 'ContribYield' => 3333]); /** @var User $author2 */ $author2 = User::factory()->create(['ContribCount' => 111, 'ContribYield' => 2222]); - /** @var Game $game */ - $game = Game::factory()->create(); + $game = $this->seedGame(withHash: false); $achievements = Achievement::factory()->published()->count(3)->create(['GameID' => $game->ID, 'Author' => $author->User, 'TrueRatio' => 0]); - /** @var Game $game2 */ - $game2 = Game::factory()->create(); + $game2 = $this->seedGame(withHash: false); /** @var Achievement $game2Achievement */ $game2Achievement = Achievement::factory()->published()->create(['GameID' => $game2->ID, 'Author' => $author2->User, 'TrueRatio' => 0]); @@ -351,15 +358,20 @@ public function testResetAllMultipleAuthors(): void $this->addHardcoreUnlock($user, $achievements->get(2)); $this->addHardcoreUnlock($user, $game2Achievement); - $this->assertEquals(123, $user->RASoftcorePoints); - $this->assertEquals(1234, $user->RAPoints); - $this->assertEquals(2345, $user->TrueRAPoints); - $this->assertEquals(777, $author->ContribCount); - $this->assertEquals(3333, $author->ContribYield); - $this->assertEquals(111, $author2->ContribCount); - $this->assertEquals(2222, $author2->ContribYield); + $this->assertEquals(4, $user->achievements_unlocked); + $this->assertEquals(0, $user->RASoftcorePoints); + $this->assertEquals($achievements->sum('Points') + $game2Achievement->points, $user->RAPoints); + + $author->refresh(); + // $this->assertEquals($achievements->count(), $author->ContribCount); + // $this->assertEquals($achievements->sum('Points'), $author->ContribYield); + + $author2->refresh(); + // $this->assertEquals(1, $author2->ContribCount); + // $this->assertEquals($game2Achievement->points, $author2->ContribYield); - (new ResetPlayerProgressAction())->execute($user); + (new ResetPlayerProgress())->execute($user); + $user->refresh(); // unlocks and badge should have been revoked $this->assertDoesNotHaveAnyUnlock($user, $achievements->get(0)); @@ -368,22 +380,18 @@ public function testResetAllMultipleAuthors(): void $this->assertDoesNotHaveAnyUnlock($user, $game2Achievement); // points should have been updated - $gamePoints = $achievements->get(0)->Points + $achievements->get(1)->Points + $achievements->get(2)->Points; - $gameTruePoints = $achievements->get(0)->TruePoints + $achievements->get(1)->TruePoints + $achievements->get(2)->TruePoints; - $totalPoints = $gamePoints + $game2Achievement->Points; - $totalTruePoints = $gameTruePoints + $game2Achievement->TruePoints; - $this->assertEquals(123, $user->RASoftcorePoints); - $this->assertEquals(1234 - $totalPoints, $user->RAPoints); - $this->assertEquals(2345 - $totalTruePoints, $user->TrueRAPoints); + $this->assertEquals(0, $user->RASoftcorePoints); + $this->assertEquals(0, $user->RAPoints); + $this->assertEquals(0, $user->TrueRAPoints); // author contributions should have been updated - $author = User::firstWhere('User', $author->User); - $this->assertEquals(777 - 3, $author->ContribCount); - $this->assertEquals(3333 - $gamePoints, $author->ContribYield); + $author->refresh(); + // $this->assertEquals(0, $author->ContribCount); + // $this->assertEquals(0, $author->ContribYield); // secondary author contributions should have been updated - $author2 = User::firstWhere('User', $author2->User); - $this->assertEquals(111 - 1, $author2->ContribCount); - $this->assertEquals(2222 - $game2Achievement->Points, $author2->ContribYield); + $author2->refresh(); + // $this->assertEquals(0, $author2->ContribCount); + // $this->assertEquals(0, $author2->ContribYield); } } diff --git a/tests/Feature/Platform/BeatenGameTest.php b/tests/Feature/Platform/BeatenGameTest.php index 489b865fbf..ac72725166 100644 --- a/tests/Feature/Platform/BeatenGameTest.php +++ b/tests/Feature/Platform/BeatenGameTest.php @@ -5,12 +5,11 @@ namespace Tests\Feature\Platform; use App\Community\Enums\AwardType; +use App\Platform\Actions\UpdatePlayerGameMetrics; use App\Platform\Enums\AchievementType; use App\Platform\Enums\UnlockMode; use App\Platform\Models\Achievement; -use App\Platform\Models\Game; use App\Platform\Models\PlayerBadge; -use App\Platform\Models\System; use App\Site\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Carbon; @@ -27,16 +26,13 @@ public function testNoProgressionAchievementsAvailable(): void // Arrange /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); $publishedAchievements = Achievement::factory()->published()->count(6)->create(['GameID' => $game->ID]); $this->addHardcoreUnlock($user, $publishedAchievements->get(0), Carbon::now()); // Act - $beaten = testBeatenGame($game->ID, $user->User, true); + $beaten = testBeatenGame($game->ID, $user->User); // Assert $this->assertFalse($beaten['isBeatable']); @@ -49,19 +45,15 @@ public function testNoProgressionAchievementsUnlocked(): void // Arrange /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); - /** @var Achievement $achievement */ $achievement = Achievement::factory()->published()->create(['GameID' => $game->ID]); Achievement::factory()->published()->progression()->count(6)->create(['GameID' => $game->ID]); $this->addHardcoreUnlock($user, $achievement, Carbon::now()); // Act - $beaten = testBeatenGame($game->ID, $user->User, true); + $beaten = testBeatenGame($game->ID, $user->User); // Assert $this->assertTrue($beaten['isBeatable']); @@ -74,10 +66,7 @@ public function testSomeProgressionAchievementsUnlocked(): void // Arrange /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); $progressionAchievements = Achievement::factory()->published()->progression()->count(5)->create(['GameID' => $game->ID]); Achievement::factory()->published()->winCondition()->create(['GameID' => $game->ID]); @@ -87,7 +76,7 @@ public function testSomeProgressionAchievementsUnlocked(): void $this->addHardcoreUnlock($user, $progressionAchievements->get(2), Carbon::now()); // Act - $beaten = testBeatenGame($game->ID, $user->User, true); + $beaten = testBeatenGame($game->ID, $user->User); // Assert $this->assertTrue($beaten['isBeatable']); @@ -100,10 +89,7 @@ public function testAllProgressionButNoWinConditionAchievementsUnlocked(): void // Arrange /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); $progressionAchievements = Achievement::factory()->published()->progression()->count(5)->create(['GameID' => $game->ID]); Achievement::factory()->published()->winCondition()->create(['GameID' => $game->ID]); @@ -115,7 +101,7 @@ public function testAllProgressionButNoWinConditionAchievementsUnlocked(): void $this->addHardcoreUnlock($user, $progressionAchievements->get(4), Carbon::now()); // Act - $beaten = testBeatenGame($game->ID, $user->User, true); + $beaten = testBeatenGame($game->ID, $user->User); // Assert $this->assertTrue($beaten['isBeatable']); @@ -128,10 +114,7 @@ public function testAllProgressionAchievementsUnlockedAndNoWinConditionExists(): // Arrange /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); $progressionAchievements = Achievement::factory()->published()->progression()->count(5)->create(['GameID' => $game->ID]); @@ -142,7 +125,7 @@ public function testAllProgressionAchievementsUnlockedAndNoWinConditionExists(): $this->addHardcoreUnlock($user, $progressionAchievements->get(4), Carbon::now()); // Act - $beaten = testBeatenGame($game->ID, $user->User, true); + $beaten = testBeatenGame($game->ID, $user->User); // Assert $this->assertTrue($beaten['isBeatable']); @@ -155,10 +138,7 @@ public function testAllProgressionAndOneWinConditionAchievementsUnlocked(): void // Arrange /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); $progressionAchievements = Achievement::factory()->published()->progression()->count(5)->create(['GameID' => $game->ID]); $winConditionAchievement = Achievement::factory()->published()->winCondition()->create(['GameID' => $game->ID]); @@ -171,7 +151,7 @@ public function testAllProgressionAndOneWinConditionAchievementsUnlocked(): void $this->addHardcoreUnlock($user, $winConditionAchievement, Carbon::now()); // Act - $beaten = testBeatenGame($game->ID, $user->User, true); + $beaten = testBeatenGame($game->ID, $user->User); // Assert $this->assertTrue($beaten['isBeatable']); @@ -184,10 +164,7 @@ public function testNoProgressionAndOneWinConditionAchievementUnlocked(): void // Arrange /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); Achievement::factory()->published()->progression()->count(5)->create(['GameID' => $game->ID]); $winConditionAchievement = Achievement::factory()->published()->winCondition()->create(['GameID' => $game->ID]); @@ -195,7 +172,7 @@ public function testNoProgressionAndOneWinConditionAchievementUnlocked(): void $this->addHardcoreUnlock($user, $winConditionAchievement, Carbon::now()); // Act - $beaten = testBeatenGame($game->ID, $user->User, true); + $beaten = testBeatenGame($game->ID, $user->User); // Assert $this->assertTrue($beaten['isBeatable']); @@ -208,10 +185,7 @@ public function testSomeHardcoreAndSomeSoftcoreUnlocks(): void // Arrange /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); $progressionAchievements = Achievement::factory()->published()->progression()->count(5)->create(['GameID' => $game->ID]); $winConditionAchievement = Achievement::factory()->published()->winCondition()->create(['GameID' => $game->ID]); @@ -224,7 +198,7 @@ public function testSomeHardcoreAndSomeSoftcoreUnlocks(): void $this->addHardcoreUnlock($user, $winConditionAchievement, Carbon::now()); // Act - $beaten = testBeatenGame($game->ID, $user->User, true); + $beaten = testBeatenGame($game->ID, $user->User); // Assert $this->assertTrue($beaten['isBeatable']); @@ -237,10 +211,7 @@ public function testSomeHardcoreAndSomeSoftcoreUnlocks2(): void // Arrange /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); $progressionAchievements = Achievement::factory()->published()->progression()->count(5)->create(['GameID' => $game->ID]); $winConditionAchievements = Achievement::factory()->published()->winCondition()->count(2)->create(['GameID' => $game->ID]); @@ -254,7 +225,7 @@ public function testSomeHardcoreAndSomeSoftcoreUnlocks2(): void $this->addSoftcoreUnlock($user, $winConditionAchievements->get(1), Carbon::now()); // Act - $beaten = testBeatenGame($game->ID, $user->User, true); + $beaten = testBeatenGame($game->ID, $user->User); // Assert $this->assertTrue($beaten['isBeatable']); @@ -264,15 +235,11 @@ public function testSomeHardcoreAndSomeSoftcoreUnlocks2(): void public function testSoftcoreAwardAssignment(): void { - // Arrange Carbon::setTestNow(); /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); $progressionAchievements = Achievement::factory()->published()->progression()->count(5)->create(['GameID' => $game->ID]); $winConditionAchievement = Achievement::factory()->published()->winCondition()->create(['GameID' => $game->ID]); @@ -284,17 +251,14 @@ public function testSoftcoreAwardAssignment(): void $this->addSoftcoreUnlock($user, $progressionAchievements->get(4), Carbon::now()->subMinutes(15)); $this->addHardcoreUnlock($user, $winConditionAchievement, Carbon::now()->subMinutes(10)); - // Act - testBeatenGame($game->ID, $user->User, true); - - // Assert - $this->assertEquals(PlayerBadge::where('User', $user->User)->count(), 1); - $this->assertNotNull(PlayerBadge::where('User', $user->User) - ->where('AwardType', AwardType::GameBeaten) - ->where('AwardData', $game->ID) - ->where('AwardDataExtra', UnlockMode::Softcore) - ->where('AwardDate', Carbon::now()->subMinutes(10)) - ->first() + $this->assertEquals(1, PlayerBadge::where('User', $user->User)->where('AwardType', AwardType::GameBeaten)->count()); + $this->assertNotNull( + $user->playerBadges() + ->where('AwardType', AwardType::GameBeaten) + ->where('AwardData', $game->ID) + ->where('AwardDataExtra', UnlockMode::Softcore) + ->where('AwardDate', Carbon::now()->subMinutes(10)) + ->first() ); } @@ -305,10 +269,7 @@ public function testHardcoreAwardAssignment(): void /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); $progressionAchievements = Achievement::factory()->published()->progression()->count(5)->create(['GameID' => $game->ID]); $winConditionAchievement = Achievement::factory()->published()->winCondition()->create(['GameID' => $game->ID]); @@ -320,11 +281,8 @@ public function testHardcoreAwardAssignment(): void $this->addHardcoreUnlock($user, $progressionAchievements->get(4), Carbon::now()->subMinutes(25)); $this->addHardcoreUnlock($user, $winConditionAchievement, Carbon::now()->subMinutes(20)); - // Act - testBeatenGame($game->ID, $user->User, true); - // Assert - $this->assertEquals(PlayerBadge::where('User', $user->User)->count(), 1); + $this->assertEquals(1, PlayerBadge::where('User', $user->User)->where('AwardType', AwardType::GameBeaten)->count()); $this->assertNotNull(PlayerBadge::where('User', $user->User) ->where('AwardType', AwardType::GameBeaten) ->where('AwardData', $game->ID) @@ -339,10 +297,7 @@ public function testBeatenAwardRevocation(): void // Arrange /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); $progressionAchievements = Achievement::factory()->published()->progression()->count(5)->create(['GameID' => $game->ID]); $winConditionAchievement = Achievement::factory()->published()->winCondition()->create(['GameID' => $game->ID]); @@ -354,14 +309,14 @@ public function testBeatenAwardRevocation(): void $this->addHardcoreUnlock($user, $progressionAchievements->get(4), Carbon::now()); $this->addHardcoreUnlock($user, $winConditionAchievement, Carbon::now()); - testBeatenGame($game->ID, $user->User, true); - // Act Achievement::factory()->published()->progression()->create(['GameID' => $game->ID]); - testBeatenGame($game->ID, $user->User, true); + + // TODO trigger achievement set update which will trigger UpdatePlayerGameMetrics + (new UpdatePlayerGameMetrics())->execute($user->playerGame($game)); // Assert - $this->assertEquals(PlayerBadge::where('User', $user->User)->count(), 0); + $this->assertEquals(0, PlayerBadge::where('User', $user->User)->where('AwardType', AwardType::GameBeaten)->count()); } public function testBeatenAwardRevocation2(): void @@ -370,10 +325,7 @@ public function testBeatenAwardRevocation2(): void /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); $progressionAchievements = Achievement::factory()->published()->progression()->count(5)->create(['GameID' => $game->ID]); $winConditionAchievement = Achievement::factory()->published()->winCondition()->create(['GameID' => $game->ID]); @@ -386,25 +338,23 @@ public function testBeatenAwardRevocation2(): void $this->addHardcoreUnlock($user, $progressionAchievements->get(4), Carbon::now()->subMinutes(35)); $this->addHardcoreUnlock($user, $winConditionAchievement, Carbon::now()->subMinutes(30)); - testBeatenGame($game->ID, $user->User, true); - // Now they'll upgrade it to hardcore by unlocking the remaining achievements in hardcore. $this->addHardcoreUnlock($user, $progressionAchievements->get(0), Carbon::now()->subMinutes(25)); $this->addHardcoreUnlock($user, $progressionAchievements->get(1), Carbon::now()->subMinutes(20)); $this->addHardcoreUnlock($user, $progressionAchievements->get(2), Carbon::now()->subMinutes(15)); - testBeatenGame($game->ID, $user->User, true); - // A new achievement gets added and marked as Progression. /** @var Achievement $newAchievement */ $newAchievement = Achievement::factory()->published()->progression()->create(['GameID' => $game->ID]); - testBeatenGame($game->ID, $user->User, true); - $this->assertEquals(PlayerBadge::where('User', $user->User)->count(), 0); + + // TODO trigger achievement set update which will trigger UpdatePlayerGameMetrics + (new UpdatePlayerGameMetrics())->execute($user->playerGame($game)); + + $this->assertEquals(0, PlayerBadge::where('User', $user->User)->where('AwardType', AwardType::GameBeaten)->count()); // The user unlocks it in softcore. $this->addSoftcoreUnlock($user, $newAchievement, Carbon::now()->subMinutes(10)); - testBeatenGame($game->ID, $user->User, true); - $this->assertEquals(PlayerBadge::where('User', $user->User)->count(), 1); + $this->assertEquals(1, PlayerBadge::where('User', $user->User)->where('AwardType', AwardType::GameBeaten)->count()); $this->assertNotNull(PlayerBadge::where('User', $user->User) ->where('AwardType', AwardType::GameBeaten) ->where('AwardData', $game->ID) @@ -415,8 +365,7 @@ public function testBeatenAwardRevocation2(): void // The user unlocks it in hardcore. $this->addHardcoreUnlock($user, $newAchievement, Carbon::now()->subMinutes(5)); - testBeatenGame($game->ID, $user->User, true); - $this->assertEquals(PlayerBadge::where('User', $user->User)->count(), 2); + $this->assertEquals(1, PlayerBadge::where('User', $user->User)->where('AwardType', AwardType::GameBeaten)->count()); $this->assertNotNull(PlayerBadge::where('User', $user->User) ->where('AwardType', AwardType::GameBeaten) ->where('AwardData', $game->ID) @@ -432,10 +381,7 @@ public function testBeatenAwardRevocation3(): void /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); Achievement::factory()->published()->count(6)->create(['GameID' => $game->ID]); /** @var Achievement $progressionAchievement */ @@ -443,19 +389,18 @@ public function testBeatenAwardRevocation3(): void // The user unlocks the one progression achievement. They should be given beaten game credit. $this->addHardcoreUnlock($user, $progressionAchievement); - testBeatenGame($game->ID, $user->User, true); - $this->assertEquals(PlayerBadge::where('User', $user->User)->count(), 1); + $this->assertEquals(1, PlayerBadge::where('User', $user->User)->where('AwardType', AwardType::GameBeaten)->count()); // Now, pretend a dev removes the progression type from the achievement. $progressionAchievement->type = null; $progressionAchievement->save(); $progressionAchievement->refresh(); - // Next, pretend the player does a score recalc, which triggers testBeatenGame(). - testBeatenGame($game->ID, $user->User, true); + // TODO trigger achievement set update which will trigger UpdatePlayerGameMetrics + (new UpdatePlayerGameMetrics())->execute($user->playerGame($game)); // The beaten game award should be revoked. - $this->assertEquals(PlayerBadge::where('User', $user->User)->count(), 0); + $this->assertEquals(0, PlayerBadge::where('User', $user->User)->where('AwardType', AwardType::GameBeaten)->count()); } public function testRetroactiveAward(): void @@ -464,10 +409,7 @@ public function testRetroactiveAward(): void /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); $gameAchievements = Achievement::factory()->published()->count(6)->create(['GameID' => $game->ID]); @@ -483,9 +425,10 @@ public function testRetroactiveAward(): void $achievement->save(); } - testBeatenGame($game->ID, $user->User, true); + // TODO trigger achievement set update which will trigger UpdatePlayerGameMetrics + (new UpdatePlayerGameMetrics())->execute($user->playerGame($game)); - $this->assertEquals(PlayerBadge::where('User', $user->User)->count(), 1); + $this->assertEquals(1, PlayerBadge::where('User', $user->User)->where('AwardType', AwardType::GameBeaten)->count()); $this->assertNotNull(PlayerBadge::where('User', $user->User) ->where('AwardType', AwardType::GameBeaten) ->where('AwardData', $game->ID) @@ -501,10 +444,7 @@ public function testRetroactiveAward2(): void /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); Achievement::factory()->published()->count(6)->create(['GameID' => $game->ID]); $winConditionAchievements = Achievement::factory()->published()->winCondition()->count(2)->create(['GameID' => $game->ID]); @@ -512,9 +452,7 @@ public function testRetroactiveAward2(): void $this->addHardcoreUnlock($user, $winConditionAchievements->get(0), Carbon::now()->subHours(12)); $this->addHardcoreUnlock($user, $winConditionAchievements->get(1), Carbon::now()->subHours(6)); - testBeatenGame($game->ID, $user->User, true); - - $this->assertEquals(PlayerBadge::where('User', $user->User)->count(), 1); + $this->assertEquals(1, PlayerBadge::where('User', $user->User)->where('AwardType', AwardType::GameBeaten)->count()); $this->assertNotNull(PlayerBadge::where('User', $user->User) ->where('AwardType', AwardType::GameBeaten) ->where('AwardData', $game->ID) @@ -530,10 +468,7 @@ public function testRetroactiveAward3(): void /** @var User $user */ $user = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); $gameAchievements = Achievement::factory()->published()->count(7)->create(['GameID' => $game->ID]); @@ -557,9 +492,9 @@ public function testRetroactiveAward3(): void $achievement->save(); } - testBeatenGame($game->ID, $user->User, true); + // TODO trigger achievement set update which will trigger UpdatePlayerGameMetrics + (new UpdatePlayerGameMetrics())->execute($user->playerGame($game)); - $this->assertEquals(PlayerBadge::where('User', $user->User)->count(), 1); $this->assertNotNull(PlayerBadge::where('User', $user->User) ->where('AwardType', AwardType::GameBeaten) ->where('AwardData', $game->ID) diff --git a/tests/Feature/Platform/Components/AchievementsListTest.php b/tests/Feature/Platform/Components/AchievementsListTest.php index 09cf72be98..4fa46ee1d8 100644 --- a/tests/Feature/Platform/Components/AchievementsListTest.php +++ b/tests/Feature/Platform/Components/AchievementsListTest.php @@ -30,11 +30,11 @@ public function testItRendersAllAchievementsInCorrectOrder(): void // Arrange /** @var User $user */ $user = User::factory()->create(); - - $achievementOne = Achievement::factory()->create(['Title' => 'One', 'DisplayOrder' => 0]); - $achievementTwo = Achievement::factory()->create(['Title' => 'Two', 'DisplayOrder' => 1]); - $achievementThree = Achievement::factory()->create(['Title' => 'Three', 'DisplayOrder' => 2]); - $achievementFour = Achievement::factory()->create(['Title' => 'Four', 'DisplayOrder' => 3]); + $game = $this->seedGame(withHash: false); + $achievementOne = Achievement::factory()->create(['GameID' => $game->id, 'Title' => 'One', 'DisplayOrder' => 0]); + $achievementTwo = Achievement::factory()->create(['GameID' => $game->id, 'Title' => 'Two', 'DisplayOrder' => 1]); + $achievementThree = Achievement::factory()->create(['GameID' => $game->id, 'Title' => 'Three', 'DisplayOrder' => 2]); + $achievementFour = Achievement::factory()->create(['GameID' => $game->id, 'Title' => 'Four', 'DisplayOrder' => 3]); $this->addHardcoreUnlock($user, $achievementThree); @@ -63,7 +63,7 @@ public function testItRendersMetadata(): void $view->assertSeeText($achievement->Title); $view->assertSeeText($achievement->Description); - $view->assertSeeText($achievement->Points); + $view->assertSeeText((string) $achievement->Points); $view->assertSeeText("5,000"); $view->assertSeeText("Progression"); } @@ -73,8 +73,9 @@ public function testUnlockedRowsHaveCorrectClassName(): void // Arrange /** @var User $user */ $user = User::factory()->create(); + $game = $this->seedGame(withHash: false); /** @var Achievement $achievement */ - $achievement = Achievement::factory()->create(['type' => AchievementType::Progression, 'TrueRatio' => 5000]); + $achievement = Achievement::factory()->create(['GameID' => $game->id, 'type' => AchievementType::Progression, 'TrueRatio' => 5000]); $this->addHardcoreUnlock($user, $achievement); $achievement = $achievement->toArray(); diff --git a/tests/Feature/Platform/Concerns/TestsPlayerAchievements.php b/tests/Feature/Platform/Concerns/TestsPlayerAchievements.php index ddc20dc9a2..9bc3dbcb21 100644 --- a/tests/Feature/Platform/Concerns/TestsPlayerAchievements.php +++ b/tests/Feature/Platform/Concerns/TestsPlayerAchievements.php @@ -4,6 +4,7 @@ namespace Tests\Feature\Platform\Concerns; +use App\Platform\Actions\UnlockPlayerAchievement; use App\Platform\Enums\UnlockMode; use App\Platform\Models\Achievement; use App\Platform\Models\PlayerAchievementLegacy; @@ -18,16 +19,6 @@ protected function addPlayerAchievement( ?Carbon $hardcoreUnlockTime, Carbon $softcoreUnlockTime ): void { - // TODO use unlock action instead as soon as it's been refactored, drop the rest - $user->achievements()->syncWithPivotValues( - $achievement, - [ - 'unlocked_at' => $softcoreUnlockTime, - 'unlocked_hardcore_at' => $hardcoreUnlockTime, - ], - detaching: false - ); - $needsHardcore = ($hardcoreUnlockTime !== null); $needsSoftcore = true; @@ -49,8 +40,6 @@ protected function addPlayerAchievement( 'Date' => $hardcoreUnlockTime, ]) ); - } elseif (!$needsSoftcore) { - return; } if ($needsSoftcore) { @@ -63,6 +52,17 @@ protected function addPlayerAchievement( ]) ); } + + (new UnlockPlayerAchievement()) + ->execute( + $user, + $achievement, + $hardcoreUnlockTime !== null, + $hardcoreUnlockTime ?? $softcoreUnlockTime, + ); + + // refresh user, unlocking achievements cascades into metrics recalculations + $user->refresh(); } protected function addHardcoreUnlock(User $user, Achievement $achievement, ?Carbon $when = null): void diff --git a/tests/Feature/Platform/Concerns/TestsPlayerBadges.php b/tests/Feature/Platform/Concerns/TestsPlayerBadges.php index 9271a14f45..f0c0d3a58c 100644 --- a/tests/Feature/Platform/Concerns/TestsPlayerBadges.php +++ b/tests/Feature/Platform/Concerns/TestsPlayerBadges.php @@ -5,7 +5,6 @@ namespace Tests\Feature\Platform\Concerns; use App\Community\Enums\AwardType; -use App\Platform\Enums\UnlockMode; use App\Platform\Models\Game; use App\Platform\Models\PlayerBadge; use App\Site\Models\User; @@ -35,11 +34,6 @@ protected function addPlayerBadge(User $user, int $type, int $id, int $extra = 0 } } - protected function addMasteryBadge(User $user, Game $game, int $mode = UnlockMode::Hardcore, ?Carbon $awardTime = null): void - { - $this->addPlayerBadge($user, AwardType::Mastery, $game->ID, $mode, $awardTime); - } - protected function masteryBadgeExists(User $user, Game $game): bool { return $user->playerBadges()->where('AwardType', AwardType::Mastery)->where('AwardData', $game->ID)->exists(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 75ae720a1b..f65194b715 100755 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,7 +4,7 @@ namespace Tests; -use App\Platform\Actions\LinkHashToGameAction; +use App\Platform\Actions\LinkHashToGame; use App\Platform\Models\Achievement; use App\Platform\Models\Game; use App\Platform\Models\System; @@ -71,7 +71,7 @@ protected function seedGames(int $amount = 3, ?System $system = null, int $achie $games = $system->games()->saveMany(Game::factory()->count($amount)->create()); if ($withHash) { - $games->each(fn (Game $game) => (bool) (new LinkHashToGameAction())->execute($game->ID . '_hash', $game)); + $games->each(fn (Game $game) => (bool) (new LinkHashToGame())->execute($game->ID . '_hash', $game)); } if ($achievementsAmount > 0) { From 0297f5de3601600dc6d599a0efe3427822d8454b Mon Sep 17 00:00:00 2001 From: luchaos Date: Thu, 5 Oct 2023 20:18:15 +0200 Subject: [PATCH 09/92] Attempt to fix player games relation due to case sensitivity --- app/Platform/Concerns/ActsAsPlayer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Platform/Concerns/ActsAsPlayer.php b/app/Platform/Concerns/ActsAsPlayer.php index 16ea186bce..2b0a75f3f0 100644 --- a/app/Platform/Concerns/ActsAsPlayer.php +++ b/app/Platform/Concerns/ActsAsPlayer.php @@ -91,7 +91,7 @@ public function playerBadges(): HasMany */ public function games(): BelongsToMany { - return $this->belongsToMany(Game::class, 'player_games', 'user_id', 'game_id') + return $this->belongsToMany(Game::class, 'player_games', 'user_id', 'game_id', 'ID', 'ID') ->using(PlayerGame::class) ->withTimestamps('created_at', 'updated_at'); } From 86ebec85dee7cc26f7e0bc0965d90fff1c6bdf7c Mon Sep 17 00:00:00 2001 From: luchaos Date: Thu, 5 Oct 2023 20:28:06 +0200 Subject: [PATCH 10/92] Revert attempt --- app/Platform/Concerns/ActsAsPlayer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Platform/Concerns/ActsAsPlayer.php b/app/Platform/Concerns/ActsAsPlayer.php index 2b0a75f3f0..16ea186bce 100644 --- a/app/Platform/Concerns/ActsAsPlayer.php +++ b/app/Platform/Concerns/ActsAsPlayer.php @@ -91,7 +91,7 @@ public function playerBadges(): HasMany */ public function games(): BelongsToMany { - return $this->belongsToMany(Game::class, 'player_games', 'user_id', 'game_id', 'ID', 'ID') + return $this->belongsToMany(Game::class, 'player_games', 'user_id', 'game_id') ->using(PlayerGame::class) ->withTimestamps('created_at', 'updated_at'); } From e1f0d25b0125c8c3786f86841cc6442534635d97 Mon Sep 17 00:00:00 2001 From: luchaos Date: Fri, 6 Oct 2023 21:21:38 +0200 Subject: [PATCH 11/92] Remove UpdateDeveloperContributionYieldJob from ResetPlayerProgress --- app/Platform/Actions/ResetPlayerProgress.php | 9 +++++---- app/Platform/EventServiceProvider.php | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Platform/Actions/ResetPlayerProgress.php b/app/Platform/Actions/ResetPlayerProgress.php index 9434d4716b..9050dc4ade 100644 --- a/app/Platform/Actions/ResetPlayerProgress.php +++ b/app/Platform/Actions/ResetPlayerProgress.php @@ -78,10 +78,11 @@ public function execute(User $user, ?int $achievementID = null, ?int $gameID = n $user->playerAchievementsLegacy()->delete(); } - $authors = User::whereIn('User', $authorUsernames->unique())->get('ID'); - foreach ($authors as $author) { - dispatch(new UpdateDeveloperContributionYieldJob($author->id)); - } + // TODO + // $authors = User::whereIn('User', $authorUsernames->unique())->get('ID'); + // foreach ($authors as $author) { + // dispatch(new UpdateDeveloperContributionYieldJob($author->id)); + // } $affectedGames = $affectedGames->unique(); foreach ($affectedGames as $affectedGameID) { diff --git a/app/Platform/EventServiceProvider.php b/app/Platform/EventServiceProvider.php index 4add8b2cdc..9c6de94f88 100755 --- a/app/Platform/EventServiceProvider.php +++ b/app/Platform/EventServiceProvider.php @@ -20,7 +20,6 @@ use App\Platform\Events\PlayerGameRemoved; use App\Platform\Events\PlayerRankedStatusChanged; use App\Platform\Events\PlayerSessionHeartbeat; -use App\Platform\Listeners\DispatchUpdateDeveloperContributionYieldJob; use App\Platform\Listeners\DispatchUpdateGameMetricsJob; use App\Platform\Listeners\DispatchUpdatePlayerGameMetricsJob; use App\Platform\Listeners\DispatchUpdatePlayerMetricsJob; From 8fef4e24fced87bd34b31b4eed0fba8cceba2181 Mon Sep 17 00:00:00 2001 From: luchaos Date: Fri, 6 Oct 2023 21:22:30 +0200 Subject: [PATCH 12/92] Add achievement_set_version_hash to visible attributes on Game model --- app/Platform/Models/Game.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Platform/Models/Game.php b/app/Platform/Models/Game.php index 85aed196ee..98b2acf015 100644 --- a/app/Platform/Models/Game.php +++ b/app/Platform/Models/Game.php @@ -87,6 +87,7 @@ class Game extends BaseModel implements HasComments, HasMedia 'RichPresencePatch', 'GuideURL', 'Updated', + 'achievement_set_version_hash', ]; protected static function newFactory(): GameFactory From da3dea2962647a506f5964c46452747c1757602b Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Sat, 7 Oct 2023 12:35:58 -0600 Subject: [PATCH 13/92] avoid querying Activity table for recent logins (#1905) --- app/Helpers/database/user-activity.php | 23 +++++++---------------- app/Support/Cache/CacheKey.php | 5 +++++ 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/app/Helpers/database/user-activity.php b/app/Helpers/database/user-activity.php index 54e243e8c1..ba6b338fb3 100644 --- a/app/Helpers/database/user-activity.php +++ b/app/Helpers/database/user-activity.php @@ -77,23 +77,14 @@ function postActivity(string|User $userIn, int $type, ?int $data = null, ?int $d break; case ActivityType::Login: - $lastLoginActivity = getMostRecentActivity($user->User, $type); - if ($lastLoginActivity) { - $nowTimestamp = time(); - $lastLoginTimestamp = strtotime($lastLoginActivity['timestamp']); - $diff = $nowTimestamp - $lastLoginTimestamp; - - /* - * record login activity only every 6 hours - */ - if ($diff < 60 * 60 * 6) { - /* - * new login activity from $user, duplicate of recent login " . ($diff/60) . " mins ago, - * ignoring! - */ - return true; - } + /* only record login activity every six hours */ + $cacheKey = CacheKey::buildUserLastLoginCacheKey($user->User); + $lastLogin = Cache::get($cacheKey); + if ($lastLogin && $lastLogin > Carbon::now()->subHours(6)) { + /* ignore event, login recorded recently */ + return true; } + Cache::put($cacheKey, Carbon::now(), Carbon::now()->addHours(6)); break; case ActivityType::StartedPlaying: diff --git a/app/Support/Cache/CacheKey.php b/app/Support/Cache/CacheKey.php index 3e28288a76..758744073c 100644 --- a/app/Support/Cache/CacheKey.php +++ b/app/Support/Cache/CacheKey.php @@ -11,6 +11,11 @@ public static function buildGameCardDataCacheKey(int $gameId): string return self::buildNormalizedCacheKey("game", $gameId, "card-data"); } + public static function buildUserLastLoginCacheKey(string $username): string + { + return self::buildNormalizedUserCacheKey($username, "last-login"); + } + public static function buildUserCompletedGamesCacheKey(string $username): string { return self::buildNormalizedUserCacheKey($username, "completed-games"); From 86eb69370dc8edf9f864d072bda82ffc11c47a30 Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Sat, 7 Oct 2023 12:35:58 -0600 Subject: [PATCH 14/92] avoid querying Activity table for recent logins (#1905) --- app/Helpers/database/user-activity.php | 23 +++++++---------------- app/Support/Cache/CacheKey.php | 5 +++++ 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/app/Helpers/database/user-activity.php b/app/Helpers/database/user-activity.php index 8d95300988..b36e54ff9f 100644 --- a/app/Helpers/database/user-activity.php +++ b/app/Helpers/database/user-activity.php @@ -80,23 +80,14 @@ function postActivity(string|User $userIn, int $type, ?int $data = null, ?int $d break; case ActivityType::Login: - $lastLoginActivity = getMostRecentActivity($user->User, $type); - if ($lastLoginActivity) { - $nowTimestamp = time(); - $lastLoginTimestamp = strtotime($lastLoginActivity['timestamp']); - $diff = $nowTimestamp - $lastLoginTimestamp; - - /* - * record login activity only every 6 hours - */ - if ($diff < 60 * 60 * 6) { - /* - * new login activity from $user, duplicate of recent login " . ($diff/60) . " mins ago, - * ignoring! - */ - return true; - } + /* only record login activity every six hours */ + $cacheKey = CacheKey::buildUserLastLoginCacheKey($user->User); + $lastLogin = Cache::get($cacheKey); + if ($lastLogin && $lastLogin > Carbon::now()->subHours(6)) { + /* ignore event, login recorded recently */ + return true; } + Cache::put($cacheKey, Carbon::now(), Carbon::now()->addHours(6)); break; case ActivityType::StartedPlaying: diff --git a/app/Support/Cache/CacheKey.php b/app/Support/Cache/CacheKey.php index 3e28288a76..758744073c 100644 --- a/app/Support/Cache/CacheKey.php +++ b/app/Support/Cache/CacheKey.php @@ -11,6 +11,11 @@ public static function buildGameCardDataCacheKey(int $gameId): string return self::buildNormalizedCacheKey("game", $gameId, "card-data"); } + public static function buildUserLastLoginCacheKey(string $username): string + { + return self::buildNormalizedUserCacheKey($username, "last-login"); + } + public static function buildUserCompletedGamesCacheKey(string $username): string { return self::buildNormalizedUserCacheKey($username, "completed-games"); From 890be37c640083aed554962c553e1d5fe13cfaa0 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 8 Oct 2023 11:49:35 +0200 Subject: [PATCH 15/92] fix: do not update player games that do not exist --- .../Actions/UpdateGameAchievementsMetrics.php | 2 ++ app/Platform/Actions/UpdateGameMetrics.php | 2 ++ app/Platform/Jobs/UpdatePlayerGameMetricsJob.php | 15 ++++++++++----- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/Platform/Actions/UpdateGameAchievementsMetrics.php b/app/Platform/Actions/UpdateGameAchievementsMetrics.php index 6222e827dc..36fa96abac 100644 --- a/app/Platform/Actions/UpdateGameAchievementsMetrics.php +++ b/app/Platform/Actions/UpdateGameAchievementsMetrics.php @@ -31,10 +31,12 @@ public function execute(Game $game): void $unlocksCount = $achievement->playerAchievements() ->leftJoin('UserAccounts as user', 'user.ID', '=', 'player_achievements.user_id') ->where('user.Untracked', false) + ->whereNull('user.Deleted') ->count(); $unlocksHardcoreCount = $achievement->playerAchievements() ->leftJoin('UserAccounts as user', 'user.ID', '=', 'player_achievements.user_id') ->where('user.Untracked', false) + ->whereNull('user.Deleted') ->whereNotNull('unlocked_hardcore_at') ->count(); diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php index 302abb4e14..0a773193d6 100644 --- a/app/Platform/Actions/UpdateGameMetrics.php +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -31,12 +31,14 @@ public function execute(Game $game): void // $game->players_total = $game->playerGames() // ->leftJoin('UserAccounts as user', 'user.ID', '=', 'player_games.user_id') // ->where('player_games.achievements_unlocked', '>', 0) + // ->whereNull('user.Deleted') // ->where('user.Untracked', false) // ->count(); // $game->players_hardcore = $game->playerGames() // ->leftJoin('UserAccounts as user', 'user.ID', '=', 'player_games.user_id') // ->where('player_games.achievements_unlocked_hardcore', '>', 0) // ->where('user.Untracked', false) + // ->whereNull('user.Deleted') // ->count(); $parentGameId = getParentGameIdFromGameId($game->id); $game->players_total = getTotalUniquePlayers($game->id, $parentGameId); diff --git a/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php index 659e7ed4f9..2f6cb1ba8b 100644 --- a/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php +++ b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php @@ -26,11 +26,16 @@ public function __construct( public function handle(): void { + $playerGame = PlayerGame::where('user_id', '=', $this->userId) + ->where('game_id', '=', $this->gameId) + ->first(); + + if (!$playerGame) { + // game player might not exist anymore + return; + } + app()->make(UpdatePlayerGameMetrics::class) - ->execute( - PlayerGame::where('user_id', '=', $this->userId) - ->where('game_id', '=', $this->gameId) - ->firstOrFail() - ); + ->execute($playerGame); } } From b6185fabda01b48ee32cea81c8af90eda33be4ee Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 8 Oct 2023 11:50:08 +0200 Subject: [PATCH 16/92] temporarily do not cascade into player games from game metrics updates --- app/Platform/Actions/UpdateGameMetrics.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php index 0a773193d6..77c80f14f7 100644 --- a/app/Platform/Actions/UpdateGameMetrics.php +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -76,6 +76,8 @@ public function execute(Game $game): void GameMetricsUpdated::dispatch($game); + return; + // TODO dispatch events for achievement set and game metrics changes $tmp = $achievementsPublishedChange; $tmp = $pointsTotalChange; From 84cc86ece8cc7c1cca627cbd3d2a7b34c5baf147 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 8 Oct 2023 12:07:39 +0200 Subject: [PATCH 17/92] increase horizon queue timeout --- app/Platform/Actions/UpdateGameMetrics.php | 2 -- config/horizon.php | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php index 77c80f14f7..0a773193d6 100644 --- a/app/Platform/Actions/UpdateGameMetrics.php +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -76,8 +76,6 @@ public function execute(Game $game): void GameMetricsUpdated::dispatch($game); - return; - // TODO dispatch events for achievement set and game metrics changes $tmp = $achievementsPublishedChange; $tmp = $pointsTotalChange; diff --git a/config/horizon.php b/config/horizon.php index abfc0981fb..3013acc049 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -153,7 +153,7 @@ | */ - 'fast_termination' => false, + 'fast_termination' => true, /* |-------------------------------------------------------------------------- @@ -197,7 +197,7 @@ 'maxJobs' => 0, 'memory' => 128, 'tries' => 1, - 'timeout' => 60, + 'timeout' => 240, 'nice' => 0, ], ], From 19991f95c84ae3aa005ef5d3710ce41f494c46b6 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 8 Oct 2023 12:21:10 +0200 Subject: [PATCH 18/92] remove superfluous user deleted check from unlock counts those users should be untracked already --- app/Platform/Actions/UpdateGameAchievementsMetrics.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/Platform/Actions/UpdateGameAchievementsMetrics.php b/app/Platform/Actions/UpdateGameAchievementsMetrics.php index 36fa96abac..6222e827dc 100644 --- a/app/Platform/Actions/UpdateGameAchievementsMetrics.php +++ b/app/Platform/Actions/UpdateGameAchievementsMetrics.php @@ -31,12 +31,10 @@ public function execute(Game $game): void $unlocksCount = $achievement->playerAchievements() ->leftJoin('UserAccounts as user', 'user.ID', '=', 'player_achievements.user_id') ->where('user.Untracked', false) - ->whereNull('user.Deleted') ->count(); $unlocksHardcoreCount = $achievement->playerAchievements() ->leftJoin('UserAccounts as user', 'user.ID', '=', 'player_achievements.user_id') ->where('user.Untracked', false) - ->whereNull('user.Deleted') ->whereNotNull('unlocked_hardcore_at') ->count(); From 71e745d590d78c547c37949bfbbaa31af4308f95 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 8 Oct 2023 13:25:57 +0200 Subject: [PATCH 19/92] improve player games update via UpdateGameMetrics --- app/Platform/Actions/UpdateGameMetrics.php | 47 ++++++++----------- ...00000_update_player_achievements_index.php | 2 +- ...10_08_000000_update_player_games_table.php | 33 +++++++++++++ 3 files changed, 53 insertions(+), 29 deletions(-) create mode 100644 database/migrations/platform/2023_10_08_000000_update_player_games_table.php diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php index 0a773193d6..3207b96c5e 100644 --- a/app/Platform/Actions/UpdateGameMetrics.php +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -31,14 +31,12 @@ public function execute(Game $game): void // $game->players_total = $game->playerGames() // ->leftJoin('UserAccounts as user', 'user.ID', '=', 'player_games.user_id') // ->where('player_games.achievements_unlocked', '>', 0) - // ->whereNull('user.Deleted') // ->where('user.Untracked', false) // ->count(); // $game->players_hardcore = $game->playerGames() // ->leftJoin('UserAccounts as user', 'user.ID', '=', 'player_games.user_id') // ->where('player_games.achievements_unlocked_hardcore', '>', 0) // ->where('user.Untracked', false) - // ->whereNull('user.Deleted') // ->count(); $parentGameId = getParentGameIdFromGameId($game->id); $game->players_total = getTotalUniquePlayers($game->id, $parentGameId); @@ -86,31 +84,24 @@ public function execute(Game $game): void // ad-hoc updates for player games, so they can be updated the next time a player updates their profile // Note that those might be multiple thousand entries depending on a game's players count - $updateReason = null; - if ($pointsWeightedChange) { - $updateReason = 'weighted_points_outdated'; - } - if ($achievementSetVersionChanged) { - $updateReason = 'version_mismatch'; - } - if ($updateReason) { - $game->playerGames() - ->where(function ($query) use ($game, $updateReason) { - if ($updateReason === 'weighted_points_outdated') { - $query->whereNot('points_weighted_total', '=', $game->TotalTruePoints) - ->orWhereNull('points_weighted_total'); - } - if ($updateReason === 'version_mismatch') { - $query->whereNot('achievement_set_version_hash', '=', $game->achievement_set_version_hash) - ->orWhereNull('achievement_set_version_hash'); - } - }) - ->update([ - 'update_status' => $updateReason, - 'achievements_total' => $game->achievements_published, - 'points_total' => $game->points_total, - 'points_weighted_total' => $game->TotalTruePoints, - ]); - } + $game->playerGames() + ->where(function ($query) use ($game) { + $query->whereNot('points_weighted_total', '=', $game->TotalTruePoints); + }) + ->update([ + 'update_status' => 'weighted_points_outdated', + 'points_weighted_total' => $game->TotalTruePoints, + ]); + + $game->playerGames() + ->where(function ($query) use ($game) { + $query->whereNot('achievement_set_version_hash', '=', $game->achievement_set_version_hash) + ->orWhereNull('achievement_set_version_hash'); + }) + ->update([ + 'update_status' => 'version_mismatch', + 'points_total' => $game->points_total, + 'achievements_total' => $game->achievements_published, + ]); } } diff --git a/database/migrations/platform/2023_09_01_000000_update_player_achievements_index.php b/database/migrations/platform/2023_09_01_000000_update_player_achievements_index.php index 9d9ae4e9c6..bcf3e35253 100644 --- a/database/migrations/platform/2023_09_01_000000_update_player_achievements_index.php +++ b/database/migrations/platform/2023_09_01_000000_update_player_achievements_index.php @@ -11,7 +11,7 @@ public function up(): void { Schema::table('player_achievements', function (Blueprint $table) { $sm = Schema::getConnection()->getDoctrineSchemaManager(); - $indexesFound = $sm->listTableIndexes('Ticket'); + $indexesFound = $sm->listTableIndexes('player_achievements'); if (!array_key_exists('player_achievements_unlocked_hardcore_at_index', $indexesFound)) { $table->index(['unlocked_hardcore_at']); diff --git a/database/migrations/platform/2023_10_08_000000_update_player_games_table.php b/database/migrations/platform/2023_10_08_000000_update_player_games_table.php new file mode 100644 index 0000000000..8321443061 --- /dev/null +++ b/database/migrations/platform/2023_10_08_000000_update_player_games_table.php @@ -0,0 +1,33 @@ +getDoctrineSchemaManager(); + $indexesFound = $sm->listTableIndexes('player_games'); + + if (!array_key_exists('player_games_game_id_achievement_set_version_hash_index', $indexesFound)) { + $table->index(['game_id', 'achievement_set_version_hash']); + } + }); + } + + public function down(): void + { + Schema::table('player_games', function (Blueprint $table) { + $sm = Schema::getConnection()->getDoctrineSchemaManager(); + $indexesFound = $sm->listTableIndexes('player_games'); + + if (array_key_exists('player_games_game_id_achievement_set_version_hash_index', $indexesFound)) { + $table->dropIndex(['game_id', 'achievement_set_version_hash']); + } + }); + } +}; From df1e13bca3452fdf5865be41cbc1980d5450b96d Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 8 Oct 2023 14:15:46 +0200 Subject: [PATCH 20/92] silence listeners which only dispatch unique jobs --- config/horizon.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/config/horizon.php b/config/horizon.php index 3013acc049..623ed360e1 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -1,5 +1,9 @@ [ - // App\Jobs\ExampleJob::class, + // silence listeners which only dispatch unique jobs + DispatchUpdateDeveloperContributionYieldJob::class, + DispatchUpdateGameMetricsJob::class, + DispatchUpdatePlayerGameMetricsJob::class, + DispatchUpdatePlayerMetricsJob::class, ], /* From dcf1acce95922ea496a0c21797a5544bc21e3a68 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 8 Oct 2023 15:19:01 +0200 Subject: [PATCH 21/92] hide empty data from guests --- app/Helpers/database/player-game.php | 3 ++- public/userInfo.php | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/Helpers/database/player-game.php b/app/Helpers/database/player-game.php index 910ec641b6..b401de2fae 100644 --- a/app/Helpers/database/player-game.php +++ b/app/Helpers/database/player-game.php @@ -589,7 +589,6 @@ function getUsersCompletedGamesAndMax(string $user): array return []; } - $requiredFlag = AchievementFlag::OfficialCore; $minAchievementsForCompletion = 5; if (config('feature.aggregate_queries')) { @@ -612,6 +611,8 @@ function getUsersCompletedGamesAndMax(string $user): array return getLightweightUsersCompletedGamesAndMax($user, $cachedAwardedValues); } + $requiredFlag = AchievementFlag::OfficialCore; + // TODO slow query. optimize with denormalized data. $query = "SELECT gd.ID AS GameID, c.Name AS ConsoleName, c.ID AS ConsoleID, gd.ImageIcon, gd.Title, inner1.MaxPossible, SUM(aw.HardcoreMode = 0) AS NumAwarded, SUM(aw.HardcoreMode = 1) AS NumAwardedHC, " . diff --git a/public/userInfo.php b/public/userInfo.php index 8ffb4e5a0d..cd86015398 100644 --- a/public/userInfo.php +++ b/public/userInfo.php @@ -270,7 +270,9 @@ function resize() { $retRatio = sprintf("%01.2f", $totalTruePoints / $totalHardcorePoints); echo "Hardcore Points: " . localized_number($totalHardcorePoints) . " (" . localized_number($totalTruePoints) . ")
    "; - echo "Hardcore Achievements: " . localized_number($totalHardcoreAchievements) . "
    "; + if ($user) { + echo "Hardcore Achievements: " . localized_number($totalHardcoreAchievements) . "
    "; + } echo "Site Rank: "; if ($userIsUntracked) { @@ -313,7 +315,9 @@ function resize() { echo "
    "; } - echo "Average Completion: $avgPctWon%

    "; + if ($user) { + echo "Average Completion: $avgPctWon%

    "; + } echo "Forum Post History"; echo "
    "; From c302e560c46ea923b88f176266ad05891352b909 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 8 Oct 2023 16:15:30 +0200 Subject: [PATCH 22/92] use aggregated game metrics in UpdatePlayerGameMetrics --- .../Actions/UpdatePlayerGameMetrics.php | 21 ++++++------------- app/Platform/Actions/UpdatePlayerMetrics.php | 3 +++ app/Platform/EventServiceProvider.php | 3 +++ ...tchUpdateDeveloperContributionYieldJob.php | 3 ++- .../DispatchUpdatePlayerGameMetricsJob.php | 14 +++---------- 5 files changed, 17 insertions(+), 27 deletions(-) diff --git a/app/Platform/Actions/UpdatePlayerGameMetrics.php b/app/Platform/Actions/UpdatePlayerGameMetrics.php index ac04d639a1..ed8d926ca6 100644 --- a/app/Platform/Actions/UpdatePlayerGameMetrics.php +++ b/app/Platform/Actions/UpdatePlayerGameMetrics.php @@ -28,15 +28,6 @@ public function execute(PlayerGame $playerGame): void return; } - // TODO use pre-aggregated values instead of fetching models - // TODO $game->achievements_published - // TODO $game->points_total - // TODO $game->TotalTruePoints - $achievements = $game->achievements()->published()->get(); - $achievementsTotal = $achievements->count(); - $pointsTotal = $achievements->sum('Points'); - $pointsWeightedTotal = $achievements->sum('TrueRatio'); - $achievementsUnlocked = $user->achievements()->where('GameID', $game->id) ->published() ->withPivot([ @@ -84,7 +75,7 @@ public function execute(PlayerGame $playerGame): void $playerGame->fill([ 'update_status' => null, // reset previously added update reason 'achievement_set_version_hash' => $game->achievement_set_version_hash, - 'achievements_total' => $achievementsTotal, + 'achievements_total' => $game->achievements_published, 'achievements_unlocked' => $achievementsUnlockedCount, 'achievements_unlocked_hardcore' => $achievementsUnlockedHardcoreCount, 'last_played_at' => $lastPlayedAt, @@ -95,15 +86,15 @@ public function execute(PlayerGame $playerGame): void 'last_unlock_hardcore_at' => $lastUnlockHardcoreAt, 'first_unlock_at' => $firstUnlockAt, 'first_unlock_hardcore_at' => $firstUnlockHardcoreAt, - 'points_total' => $pointsTotal, + 'points_total' => $game->points_total, 'points' => $points, 'points_hardcore' => $pointsHardcore, - 'points_weighted_total' => $pointsWeightedTotal, + 'points_weighted_total' => $game->TotalTruePoints, 'points_weighted' => $pointsWeighted, 'created_at' => $createdAt, ]); - $playerGame->fill($this->beatProgressMetrics($playerGame, $achievementsUnlocked, $achievements)); + $playerGame->fill($this->beatProgressMetrics($playerGame, $achievementsUnlocked)); $playerGame->fill($this->completionProgressMetrics($playerGame)); $playerGame->save(); @@ -117,10 +108,10 @@ public function execute(PlayerGame $playerGame): void /** * @param Collection $achievementsUnlocked - * @param Collection $achievements */ - public function beatProgressMetrics(PlayerGame $playerGame, Collection $achievementsUnlocked, Collection $achievements): array + public function beatProgressMetrics(PlayerGame $playerGame, Collection $achievementsUnlocked): array { + $achievements = $playerGame->game->achievements()->published()->get(); $totalProgressions = $achievements->where('type', AchievementType::Progression)->count(); $totalWinConditions = $achievements->where('type', AchievementType::WinCondition)->count(); diff --git a/app/Platform/Actions/UpdatePlayerMetrics.php b/app/Platform/Actions/UpdatePlayerMetrics.php index 10e7eeac5f..a5e0d7ad19 100644 --- a/app/Platform/Actions/UpdatePlayerMetrics.php +++ b/app/Platform/Actions/UpdatePlayerMetrics.php @@ -4,6 +4,7 @@ namespace App\Platform\Actions; +use App\Platform\Events\PlayerMetricsUpdated; use App\Site\Models\User; class UpdatePlayerMetrics @@ -22,5 +23,7 @@ public function execute(User $user): void $user->TrueRAPoints = $user->achievements()->published()->wherePivotNotNull('unlocked_hardcore_at')->sum('TrueRatio'); $user->save(); + + PlayerMetricsUpdated::dispatch($user); } } diff --git a/app/Platform/EventServiceProvider.php b/app/Platform/EventServiceProvider.php index 9c6de94f88..f3faaeb52f 100755 --- a/app/Platform/EventServiceProvider.php +++ b/app/Platform/EventServiceProvider.php @@ -18,6 +18,7 @@ use App\Platform\Events\PlayerGameCompleted; use App\Platform\Events\PlayerGameMetricsUpdated; use App\Platform\Events\PlayerGameRemoved; +use App\Platform\Events\PlayerMetricsUpdated; use App\Platform\Events\PlayerRankedStatusChanged; use App\Platform\Events\PlayerSessionHeartbeat; use App\Platform\Listeners\DispatchUpdateGameMetricsJob; @@ -80,6 +81,8 @@ class EventServiceProvider extends ServiceProvider DispatchUpdatePlayerMetricsJob::class, // dispatches PlayerMetricsUpdated DispatchUpdateGameMetricsJob::class, // dispatches GameMetricsUpdated ], + PlayerMetricsUpdated::class => [ + ], PlayerSessionHeartbeat::class => [ ResumePlayerSession::class, ], diff --git a/app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php b/app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php index 573adfbba2..e8433151ee 100644 --- a/app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php +++ b/app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php @@ -7,6 +7,7 @@ use App\Platform\Events\AchievementUnpublished; use App\Platform\Events\PlayerAchievementUnlocked; use App\Platform\Jobs\UpdateDeveloperContributionYieldJob; +use App\Site\Models\User; use Illuminate\Contracts\Queue\ShouldQueue; class DispatchUpdateDeveloperContributionYieldJob implements ShouldQueue @@ -38,7 +39,7 @@ public function handle(object $event): void break; } - if ($user === null) { + if (!$user instanceof User) { return; } diff --git a/app/Platform/Listeners/DispatchUpdatePlayerGameMetricsJob.php b/app/Platform/Listeners/DispatchUpdatePlayerGameMetricsJob.php index b303f16292..1a370ea67e 100644 --- a/app/Platform/Listeners/DispatchUpdatePlayerGameMetricsJob.php +++ b/app/Platform/Listeners/DispatchUpdatePlayerGameMetricsJob.php @@ -12,9 +12,9 @@ class DispatchUpdatePlayerGameMetricsJob implements ShouldQueue { public function handle(object $event): void { - /** @var User|string|int|null $user */ $user = null; $game = null; + // TODO forward hardcore flag $hardcore = null; switch ($event::class) { @@ -27,18 +27,10 @@ public function handle(object $event): void } if (!$user instanceof User) { - if (is_int($user)) { - $user = User::find($user); - } elseif (is_string($user)) { - $user = User::firstWhere('User', $user); - } - } - - if (is_int($game)) { - $game = Game::find($game); + return; } - if ($user === null || $game === null) { + if (!$game instanceof Game) { return; } From ccfd70ed72f7499a1dc63b62bac9a7f2c3dc8283 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 8 Oct 2023 16:16:32 +0200 Subject: [PATCH 23/92] add missing PlayerMetricsUpdated event --- app/Platform/Events/PlayerMetricsUpdated.php | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 app/Platform/Events/PlayerMetricsUpdated.php diff --git a/app/Platform/Events/PlayerMetricsUpdated.php b/app/Platform/Events/PlayerMetricsUpdated.php new file mode 100644 index 0000000000..bab59a4edf --- /dev/null +++ b/app/Platform/Events/PlayerMetricsUpdated.php @@ -0,0 +1,28 @@ + Date: Sun, 8 Oct 2023 16:32:31 +0200 Subject: [PATCH 24/92] use player_achievements instead of Awarded where applicable --- app/Helpers/database/player-achievement.php | 22 +++------ app/Helpers/render/leaderboard.php | 50 +++++++-------------- 2 files changed, 22 insertions(+), 50 deletions(-) diff --git a/app/Helpers/database/player-achievement.php b/app/Helpers/database/player-achievement.php index f5e2279b29..3d5e886066 100644 --- a/app/Helpers/database/player-achievement.php +++ b/app/Helpers/database/player-achievement.php @@ -5,6 +5,7 @@ use App\Platform\Enums\UnlockMode; use App\Platform\Models\Achievement; use App\Platform\Models\Game; +use App\Platform\Models\PlayerAchievement; use App\Platform\Models\PlayerAchievementLegacy; use App\Site\Models\User; use Carbon\Carbon; @@ -136,24 +137,13 @@ function unlockAchievement(string $username, int $achievementId, bool $isHardcor return $retVal; } +/** + * @deprecated use Achievements.unlocks_total + */ function getAchievementUnlockCount(int $achID): int { - if (config('feature.aggregate_queries')) { - $query = "SELECT COUNT(*) AS NumEarned FROM player_achievements - WHERE achievement_id=$achID"; - } else { - $query = "SELECT COUNT(*) AS NumEarned FROM Awarded - WHERE AchievementID=$achID AND HardcoreMode=0"; - } - - $dbResult = s_mysql_query($query); - if (!$dbResult) { - return 0; - } - - $data = mysqli_fetch_assoc($dbResult); - - return $data['NumEarned'] ?? 0; + return PlayerAchievement::where('achievement_id', $achID) + ->count(); } /** diff --git a/app/Helpers/render/leaderboard.php b/app/Helpers/render/leaderboard.php index 7f4926e43d..7cc3a9eb0a 100644 --- a/app/Helpers/render/leaderboard.php +++ b/app/Helpers/render/leaderboard.php @@ -538,42 +538,24 @@ function getGlobalRankingData( } if ($info == 1) { - if (config('feature.aggregate_queries')) { - if ($unlockMode == UnlockMode::Hardcore) { - $whereDateAchievement = str_replace('aw.Date', 'aw.unlocked_hardcore_at', $whereDateAchievement); - } else { - $whereDateAchievement = str_replace('aw.Date', 'aw.unlocked_at', $whereDateAchievement); - } - $query = "SELECT ua.User AS User, - SUM(ach.Points) AS Points, - SUM(ach.TrueRatio) AS RetroPoints - FROM player_achievements AS aw - LEFT JOIN Achievements AS ach ON ach.ID = aw.achievement_id - LEFT JOIN UserAccounts AS ua ON ua.ID = aw.user_id - WHERE TRUE $whereDateAchievement $typeCond - $friendCondAchievement - $singleUserAchievementCond - $untrackedCond - GROUP BY ua.User - $orderCond - LIMIT $offset, $count"; + if ($unlockMode == UnlockMode::Hardcore) { + $whereDateAchievement = str_replace('aw.Date', 'aw.unlocked_hardcore_at', $whereDateAchievement); } else { - // TODO slow query (17) - $query = "SELECT aw.User AS User, - SUM(ach.Points) AS Points, - SUM(ach.TrueRatio) AS RetroPoints - FROM Awarded AS aw - LEFT JOIN Achievements AS ach ON ach.ID = aw.AchievementID - LEFT JOIN UserAccounts AS ua ON ua.User = aw.User - WHERE TRUE $whereDateAchievement $typeCond - $friendCondAchievement - $singleUserAchievementCond - $untrackedCond - AND HardcoreMode = " . $unlockMode . " - GROUP BY aw.User - $orderCond - LIMIT $offset, $count"; + $whereDateAchievement = str_replace('aw.Date', 'aw.unlocked_at', $whereDateAchievement); } + $query = "SELECT ua.User AS User, + SUM(ach.Points) AS Points, + SUM(ach.TrueRatio) AS RetroPoints + FROM player_achievements AS aw + LEFT JOIN Achievements AS ach ON ach.ID = aw.achievement_id + LEFT JOIN UserAccounts AS ua ON ua.ID = aw.user_id + WHERE TRUE $whereDateAchievement $typeCond + $friendCondAchievement + $singleUserAchievementCond + $untrackedCond + GROUP BY ua.User + $orderCond + LIMIT $offset, $count"; } else { if ($unlockMode == UnlockMode::Hardcore) { $achPoints = "CASE WHEN HardcoreMode = " . UnlockMode::Hardcore . " THEN ach.Points ELSE 0 END"; From 8d9eff006f1fafea8cd7f1977df54d1b808d0d8e Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 8 Oct 2023 17:37:23 +0200 Subject: [PATCH 25/92] improve UpdateGameAchievementsMetrics --- .../Actions/UpdateGameAchievementsMetrics.php | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/app/Platform/Actions/UpdateGameAchievementsMetrics.php b/app/Platform/Actions/UpdateGameAchievementsMetrics.php index 6222e827dc..c84df543c0 100644 --- a/app/Platform/Actions/UpdateGameAchievementsMetrics.php +++ b/app/Platform/Actions/UpdateGameAchievementsMetrics.php @@ -10,18 +10,11 @@ class UpdateGameAchievementsMetrics { public function execute(Game $game): void { - // TODO refactor to do this for each achievement set - $parentGameId = getParentGameIdFromGameId($game->id); - if (config('feature.aggregate_queries')) { - $parentGame = $parentGameId ? Game::find($parentGameId) : null; - $playersTotal = $parentGame ? $parentGame->players_total : $game->players_total; - $playersHardcore = $parentGame ? $parentGame->players_hardcore : $game->players_hardcore; - } else { - $playersTotal = getTotalUniquePlayers($game->id, $parentGameId); - $playersHardcore = getTotalUniquePlayers($game->id, $parentGameId, null, true); - } + // NOTE if game has a parent game it contains the parent game's players metrics + $playersTotal = $game->players_total; + $playersHardcore = $game->players_hardcore; // force all unachieved to be 1 $playersHardcoreCalc = $playersHardcore ?: 1; From f8b9d3839ed1015f11f089ed376322d06896217b Mon Sep 17 00:00:00 2001 From: luchaos Date: Mon, 9 Oct 2023 05:11:35 +0200 Subject: [PATCH 26/92] feat: update player games in batches after game metrics updates (#1908) * feat: update player games after game metrics updates in batches * fix cs * update comment * add more comments * return early if there was no achievement set version change --- app/Console/Kernel.php | 1 + app/Platform/Actions/UpdateGameMetrics.php | 48 +++++++++++++------ .../Actions/UpdatePlayerGameMetrics.php | 8 ++-- .../Jobs/UpdatePlayerGameMetricsJob.php | 18 ++++++- app/Platform/Jobs/UpdatePlayerMetricsJob.php | 13 +++++ config/horizon.php | 3 +- config/queue.php | 16 +++++++ ..._10_08_000001_create_job_batches_table.php | 38 +++++++++++++++ 8 files changed, 123 insertions(+), 22 deletions(-) create mode 100644 database/migrations/2023_10_08_000001_create_job_batches_table.php diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index d2969d88f5..9c52b73f7a 100755 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -15,6 +15,7 @@ protected function schedule(Schedule $schedule) // $schedule->command('websockets:clean')->daily(); $schedule->command('horizon:snapshot')->everyFiveMinutes(); + $schedule->command('queue:prune-batches --hours=48 --unfinished=72 --cancelled=72')->daily(); /** @var Settings $settings */ $settings = $this->app->get(Settings::class); diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php index 3207b96c5e..4140875539 100644 --- a/app/Platform/Actions/UpdateGameMetrics.php +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -5,7 +5,11 @@ namespace App\Platform\Actions; use App\Platform\Events\GameMetricsUpdated; +use App\Platform\Jobs\UpdatePlayerGameMetricsJob; use App\Platform\Models\Game; +use App\Platform\Models\PlayerGame; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Bus; class UpdateGameMetrics { @@ -66,7 +70,6 @@ public function execute(Game $game): void $game->save(); - // dispatch(new UpdateGameAchievementsMetricsJob($game->id))->onQueue('game-metrics'); app()->make(UpdateGameAchievementsMetrics::class) ->execute($game); $game->refresh(); @@ -74,6 +77,10 @@ public function execute(Game $game): void GameMetricsUpdated::dispatch($game); + if (!$achievementSetVersionChanged) { + return; + } + // TODO dispatch events for achievement set and game metrics changes $tmp = $achievementsPublishedChange; $tmp = $pointsTotalChange; @@ -81,23 +88,34 @@ public function execute(Game $game): void $tmp = $playersTotalChange; $tmp = $playersHardcoreChange; - // ad-hoc updates for player games, so they can be updated the next time a player updates their profile - // Note that those might be multiple thousand entries depending on a game's players count - - $game->playerGames() - ->where(function ($query) use ($game) { - $query->whereNot('points_weighted_total', '=', $game->TotalTruePoints); - }) - ->update([ - 'update_status' => 'weighted_points_outdated', - 'points_weighted_total' => $game->TotalTruePoints, - ]); - - $game->playerGames() + // Ad-hoc updates for player games metrics and player metrics after achievement set version changes + // Note: this might dispatch multiple thousands of jobs depending on a game's players count + $affectedPlayerGamesQuery = $game->playerGames() ->where(function ($query) use ($game) { $query->whereNot('achievement_set_version_hash', '=', $game->achievement_set_version_hash) ->orWhereNull('achievement_set_version_hash'); - }) + }); + + // add all affected player games to the update queue in batches + if (config('queue.default') !== 'sync') { + (clone $affectedPlayerGamesQuery) + ->whereNull('update_status') + ->orderByDesc('last_played_at') + ->chunk(1000, function (Collection $chunk) { + // map and dispatch this chunk as a batch of jobs + Bus::batch( + $chunk->map( + fn (PlayerGame $playerGame) => new UpdatePlayerGameMetricsJob($playerGame->user_id, $playerGame->game_id) + ) + ) + ->onQueue('player-game-metrics-batch') + ->dispatch(); + }); + } + + // directly update player games to make sure they aren't added to a batch again + // and contain the most important updates right away for presentation + (clone $affectedPlayerGamesQuery) ->update([ 'update_status' => 'version_mismatch', 'points_total' => $game->points_total, diff --git a/app/Platform/Actions/UpdatePlayerGameMetrics.php b/app/Platform/Actions/UpdatePlayerGameMetrics.php index ed8d926ca6..179c279ab0 100644 --- a/app/Platform/Actions/UpdatePlayerGameMetrics.php +++ b/app/Platform/Actions/UpdatePlayerGameMetrics.php @@ -7,16 +7,14 @@ use App\Platform\Enums\AchievementType; use App\Platform\Events\PlayerGameMetricsUpdated; use App\Platform\Models\Achievement; -use App\Platform\Models\Game; use App\Platform\Models\PlayerAchievement; use App\Platform\Models\PlayerGame; -use App\Site\Models\User; use Carbon\Carbon; use Illuminate\Support\Collection; class UpdatePlayerGameMetrics { - public function execute(PlayerGame $playerGame): void + public function execute(PlayerGame $playerGame, bool $silent = false): void { // TODO do this for each player_achievement_set as soon as achievement set separation is introduced // TODO store aggregates of all player_achievement_set on player_games metrics @@ -99,7 +97,9 @@ public function execute(PlayerGame $playerGame): void $playerGame->save(); - PlayerGameMetricsUpdated::dispatch($user, $game); + if (!$silent) { + PlayerGameMetricsUpdated::dispatch($user, $game); + } app()->make(RevalidateAchievementSetBadgeEligibility::class)->execute($playerGame); diff --git a/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php index 2f6cb1ba8b..c5d0642c97 100644 --- a/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php +++ b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php @@ -4,6 +4,7 @@ use App\Platform\Actions\UpdatePlayerGameMetrics; use App\Platform\Models\PlayerGame; +use Illuminate\Bus\Batchable; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use Illuminate\Contracts\Queue\ShouldQueue; @@ -13,6 +14,7 @@ class UpdatePlayerGameMetricsJob implements ShouldQueue, ShouldBeUniqueUntilProcessing { + use Batchable; use Dispatchable; use InteractsWithQueue; use Queueable; @@ -26,16 +28,28 @@ public function __construct( public function handle(): void { + if ($this->batch()?->cancelled()) { + return; + } + $playerGame = PlayerGame::where('user_id', '=', $this->userId) ->where('game_id', '=', $this->gameId) ->first(); if (!$playerGame) { - // game player might not exist anymore + // might've been deleted return; } + $silent = $this->batchId !== null; + app()->make(UpdatePlayerGameMetrics::class) - ->execute($playerGame); + ->execute($playerGame, $silent); + + // if this job was executed from within a batch it means that it's been initiated + // by a game metrics update. + // make sure to update player metrics directly, as the silent flag will not + // trigger an event (to not further cascade into another game metrics update). + $this->batch()?->add(new UpdatePlayerMetricsJob($playerGame->user_id)); } } diff --git a/app/Platform/Jobs/UpdatePlayerMetricsJob.php b/app/Platform/Jobs/UpdatePlayerMetricsJob.php index f92db15f33..1dbe7763e8 100644 --- a/app/Platform/Jobs/UpdatePlayerMetricsJob.php +++ b/app/Platform/Jobs/UpdatePlayerMetricsJob.php @@ -4,6 +4,7 @@ use App\Platform\Actions\UpdatePlayerMetrics; use App\Site\Models\User; +use Illuminate\Bus\Batchable; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use Illuminate\Contracts\Queue\ShouldQueue; @@ -13,6 +14,7 @@ class UpdatePlayerMetricsJob implements ShouldQueue, ShouldBeUniqueUntilProcessing { + use Batchable; use Dispatchable; use InteractsWithQueue; use Queueable; @@ -25,6 +27,17 @@ public function __construct( public function handle(): void { + if ($this->batch()?->cancelled()) { + return; + } + + $user = User::find($this->userId); + + if (!$user) { + // might've been deleted + return; + } + app()->make(UpdatePlayerMetrics::class) ->execute(User::findOrFail($this->userId)); } diff --git a/config/horizon.php b/config/horizon.php index 623ed360e1..148f4901ef 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -194,9 +194,10 @@ 'default', 'player-achievements', 'player-game-metrics', - 'game-metrics', 'player-metrics', + 'game-metrics', 'developer-metrics', + 'player-game-metrics-batch', ], 'balance' => 'auto', 'autoScalingStrategy' => 'time', diff --git a/config/queue.php b/config/queue.php index ff575c4145..2d61e35fd9 100755 --- a/config/queue.php +++ b/config/queue.php @@ -69,6 +69,22 @@ ], + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'mysql'), + 'table' => 'queue_job_batches', + ], + /* |-------------------------------------------------------------------------- | Failed Queue Jobs diff --git a/database/migrations/2023_10_08_000001_create_job_batches_table.php b/database/migrations/2023_10_08_000001_create_job_batches_table.php new file mode 100644 index 0000000000..c12f1dc266 --- /dev/null +++ b/database/migrations/2023_10_08_000001_create_job_batches_table.php @@ -0,0 +1,38 @@ +string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('queue_job_batches'); + } +}; From a6b7c93c7d09a19974b7b81a57117c0d42cd7925 Mon Sep 17 00:00:00 2001 From: luchaos Date: Mon, 9 Oct 2023 08:14:36 +0200 Subject: [PATCH 27/92] adjust queue priorities --- config/horizon.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/horizon.php b/config/horizon.php index 148f4901ef..3f012bb18b 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -191,10 +191,10 @@ 'supervisor-1' => [ 'connection' => 'redis', 'queue' => [ - 'default', 'player-achievements', - 'player-game-metrics', 'player-metrics', + 'default', + 'player-game-metrics', 'game-metrics', 'developer-metrics', 'player-game-metrics-batch', @@ -214,7 +214,7 @@ 'environments' => [ 'production' => [ 'supervisor-1' => [ - 'maxProcesses' => 10, + 'maxProcesses' => 16, 'balanceMaxShift' => 1, 'balanceCooldown' => 3, ], @@ -222,7 +222,7 @@ 'stage' => [ 'supervisor-1' => [ - 'maxProcesses' => 10, + 'maxProcesses' => 16, 'balanceMaxShift' => 1, 'balanceCooldown' => 3, ], From 504380c5f4d33494be2ed2c266a44b89fff7d7ae Mon Sep 17 00:00:00 2001 From: luchaos Date: Mon, 9 Oct 2023 08:15:01 +0200 Subject: [PATCH 28/92] expire caches after player game metrics update --- app/Platform/Actions/UpdatePlayerGameMetrics.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Platform/Actions/UpdatePlayerGameMetrics.php b/app/Platform/Actions/UpdatePlayerGameMetrics.php index 179c279ab0..11f3d978c8 100644 --- a/app/Platform/Actions/UpdatePlayerGameMetrics.php +++ b/app/Platform/Actions/UpdatePlayerGameMetrics.php @@ -103,7 +103,9 @@ public function execute(PlayerGame $playerGame, bool $silent = false): void app()->make(RevalidateAchievementSetBadgeEligibility::class)->execute($playerGame); - expireGameTopAchievers($playerGame->game->id); + expireUserCompletedGamesCacheValue($user->username); + expireUserAchievementUnlocksForGame($user->username, $game->id); + expireGameTopAchievers($game->id); } /** From d309e554ef3e881d2f6552a3760cd79908ab5905 Mon Sep 17 00:00:00 2001 From: luchaos Date: Fri, 13 Oct 2023 18:08:19 +0200 Subject: [PATCH 29/92] feat: improve query performance for game and achievement metrics updates (#1910) * feat: improve query performance for game and achievement metrics updates * Fix migration and tests * Update comments --- app/Helpers/database/player-game.php | 36 ++++++------ app/Platform/Models/Achievement.php | 4 +- app/Site/Models/User.php | 4 +- ...0_09_000000_update_player_tables_index.php | 56 +++++++++++++++++++ ...10_09_000001_update_achievements_index.php | 36 ++++++++++++ .../Api/V1/AchievementOfTheWeekTest.php | 27 ++++----- tests/Feature/Api/V1Test.php | 12 ++-- 7 files changed, 132 insertions(+), 43 deletions(-) create mode 100644 database/migrations/platform/2023_10_09_000000_update_player_tables_index.php create mode 100644 database/migrations/platform/2023_10_09_000001_update_achievements_index.php diff --git a/app/Helpers/database/player-game.php b/app/Helpers/database/player-game.php index b401de2fae..2054c8b19e 100644 --- a/app/Helpers/database/player-game.php +++ b/app/Helpers/database/player-game.php @@ -652,11 +652,15 @@ function getTotalUniquePlayers( $bindings = [ 'gameId' => $gameID, ]; + $gameIdStatement = 'a.GameID = :gameId'; + if ($parentGameID !== null) { + $gameIdStatement = 'a.GameID IN (:gameId, :parentGameId)'; + $bindings['parentGameId'] = $parentGameID; + } $unlockModeStatement = ''; if ($hardcoreOnly) { - $bindings['unlockMode'] = UnlockMode::Hardcore; - $unlockModeStatement = ' AND aw.HardcoreMode = :unlockMode'; + $unlockModeStatement = ' AND pa.unlocked_hardcore_at IS NOT NULL'; } $bindings['achievementFlag'] = AchievementFlag::OfficialCore; @@ -664,26 +668,24 @@ function getTotalUniquePlayers( $requestedByStatement = ''; if ($requestedBy) { $bindings['requestedBy'] = $requestedBy; - $requestedByStatement = 'OR ua.User = :requestedBy'; - } - - $gameIdStatement = 'ach.GameID = :gameId'; - if ($parentGameID !== null) { - $gameIdStatement = 'ach.GameID IN (:gameId, :parentGameId)'; - $bindings['parentGameId'] = $parentGameID; + $requestedByStatement = 'OR u.User = :requestedBy'; } $query = " - SELECT COUNT(DISTINCT aw.User) As UniquePlayers - FROM Awarded AS aw - LEFT JOIN Achievements AS ach ON ach.ID = aw.AchievementID - LEFT JOIN UserAccounts AS ua ON ua.User = aw.User - WHERE $gameIdStatement - $unlockModeStatement AND ach.Flags = :achievementFlag - AND (NOT ua.Untracked $requestedByStatement) + SELECT + COUNT(DISTINCT pa.user_id) players_count + FROM + player_achievements pa + LEFT JOIN Achievements a ON a.ID = pa.achievement_id + LEFT JOIN UserAccounts u ON u.ID = pa.user_id + WHERE + $gameIdStatement + AND a.Flags = :achievementFlag + AND (u.Untracked = 0 $requestedByStatement) + $unlockModeStatement "; - return (int) (legacyDbFetch($query, $bindings)['UniquePlayers'] ?? 0); + return (int) (legacyDbFetch($query, $bindings)['players_count'] ?? 0); } function getGameRecentPlayers(int $gameID, int $maximum_results = 0): array diff --git a/app/Platform/Models/Achievement.php b/app/Platform/Models/Achievement.php index 4af702ec2d..c929a0efac 100644 --- a/app/Platform/Models/Achievement.php +++ b/app/Platform/Models/Achievement.php @@ -245,7 +245,7 @@ public function playersLegacy(): BelongsToMany */ public function playerAchievementsLegacy(): HasMany { - return $this->hasMany(PlayerAchievementLegacy::class); + return $this->hasMany(PlayerAchievementLegacy::class, 'AchievementID'); } /** @@ -262,7 +262,7 @@ public function players(): BelongsToMany */ public function playerAchievements(): HasMany { - return $this->hasMany(PlayerAchievement::class); + return $this->hasMany(PlayerAchievement::class, 'achievement_id'); } /** diff --git a/app/Site/Models/User.php b/app/Site/Models/User.php index 0125faba0f..c21dcbbd2e 100644 --- a/app/Site/Models/User.php +++ b/app/Site/Models/User.php @@ -78,7 +78,9 @@ class User extends Authenticatable implements CommunityMember, Developer, HasCom // TODO drop LastActivityID, LastGameID, UnreadMessageCount -> derived // TODO drop PasswordResetToken -> password_resets table // TODO move UserWallActive to preferences, allow comments to be visible to/writable for public, friends, private etc - // TODO drop Untracked in favor of unranked_at + // TODO rename Untracked to unranked or drop in favor of unranked_at (update indexes) + // TODO drop ID index + // TODO remove User from PRIMARY, there's already a unique index on username (User) // TODO drop ManuallyVerified in favor of forum_verified_at // TODO drop SaltedPass in favor of Password // TODO drop Permissions in favor of RBAC tables diff --git a/database/migrations/platform/2023_10_09_000000_update_player_tables_index.php b/database/migrations/platform/2023_10_09_000000_update_player_tables_index.php new file mode 100644 index 0000000000..53914d0bce --- /dev/null +++ b/database/migrations/platform/2023_10_09_000000_update_player_tables_index.php @@ -0,0 +1,56 @@ +getDoctrineSchemaManager(); + $indexesFound = $sm->listTableIndexes('player_achievements'); + + if (!array_key_exists('player_achievements_achievement_id_user_id_unlocked_hardcore_at', $indexesFound)) { + $table->index( + ['achievement_id', 'user_id', 'unlocked_hardcore_at'], + 'player_achievements_achievement_id_user_id_unlocked_hardcore_at' + ); + } + }); + + Schema::table('player_games', function (Blueprint $table) { + $sm = Schema::getConnection()->getDoctrineSchemaManager(); + $indexesFound = $sm->listTableIndexes('player_games'); + + if (!array_key_exists('player_games_game_id_user_id_index', $indexesFound)) { + $table->index(['game_id', 'user_id']); + } + }); + } + + public function down(): void + { + Schema::table('player_achievements', function (Blueprint $table) { + $sm = Schema::getConnection()->getDoctrineSchemaManager(); + $indexesFound = $sm->listTableIndexes('player_achievements'); + + if (array_key_exists('player_achievements_achievement_id_user_id_unlocked_hardcore_at', $indexesFound)) { + $table->dropIndex('player_achievements_achievement_id_user_id_unlocked_hardcore_at'); + } + }); + + Schema::table('player_games', function (Blueprint $table) { + $sm = Schema::getConnection()->getDoctrineSchemaManager(); + $indexesFound = $sm->listTableIndexes('player_games'); + + if (array_key_exists('player_games_game_id_user_id_index', $indexesFound)) { + $table->dropIndex(['game_id', 'user_id']); + } + }); + } +}; diff --git a/database/migrations/platform/2023_10_09_000001_update_achievements_index.php b/database/migrations/platform/2023_10_09_000001_update_achievements_index.php new file mode 100644 index 0000000000..e5ec4705e0 --- /dev/null +++ b/database/migrations/platform/2023_10_09_000001_update_achievements_index.php @@ -0,0 +1,36 @@ +getDoctrineSchemaManager(); + $indexesFound = $sm->listTableIndexes('Achievements'); + + if (!array_key_exists('achievements_game_id_published_index', $indexesFound)) { + $table->index( + ['GameID', 'Flags'], + 'achievements_game_id_published_index' + ); + } + }); + } + + public function down(): void + { + Schema::table('Achievements', function (Blueprint $table) { + $sm = Schema::getConnection()->getDoctrineSchemaManager(); + $indexesFound = $sm->listTableIndexes('Achievements'); + + if (array_key_exists('achievements_game_id_published_index', $indexesFound)) { + $table->dropIndex('achievements_game_id_published_index'); + } + }); + } +}; diff --git a/tests/Feature/Api/V1/AchievementOfTheWeekTest.php b/tests/Feature/Api/V1/AchievementOfTheWeekTest.php index 1c7b48d7ee..e507d6be7e 100644 --- a/tests/Feature/Api/V1/AchievementOfTheWeekTest.php +++ b/tests/Feature/Api/V1/AchievementOfTheWeekTest.php @@ -5,19 +5,18 @@ namespace Tests\Feature\Api\V1; use App\Platform\Models\Achievement; -use App\Platform\Models\Game; -use App\Platform\Models\PlayerAchievementLegacy; -use App\Platform\Models\System; use App\Site\Models\StaticData; use App\Site\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Carbon; +use Tests\Feature\Platform\Concerns\TestsPlayerAchievements; use Tests\TestCase; class AchievementOfTheWeekTest extends TestCase { use RefreshDatabase; use BootstrapsApiV1; + use TestsPlayerAchievements; public function testGetAchievementOfTheWeekEmptyResponse(): void { @@ -32,19 +31,13 @@ public function testGetAchievementOfTheWeek(): void $user2 = User::factory()->create(); /** @var User $user3 */ $user3 = User::factory()->create(); - /** @var System $system */ - $system = System::factory()->create(); - /** @var Game $game */ - $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $game = $this->seedGame(withHash: false); /** @var Achievement $achievement */ $achievement = Achievement::factory()->published()->create(['GameID' => $game->ID]); $now = Carbon::now(); - /** @var PlayerAchievementLegacy $unlock */ - $unlock = PlayerAchievementLegacy::factory()->create(['AchievementID' => $achievement->ID, 'User' => $this->user->User, 'Date' => $now]); - /** @var PlayerAchievementLegacy $unlock2 */ - $unlock2 = PlayerAchievementLegacy::factory()->create(['AchievementID' => $achievement->ID, 'User' => $user2->User, 'Date' => $now->copy()->subMinutes(5)]); - /** @var PlayerAchievementLegacy $unlock3 */ - $unlock3 = PlayerAchievementLegacy::factory()->create(['AchievementID' => $achievement->ID, 'User' => $user3->User, 'Date' => $now->copy()->addMinutes(5)]); + $this->addSoftcoreUnlock($this->user, $achievement, $now); + $this->addSoftcoreUnlock($user2, $achievement, $now->copy()->subMinutes(5)); + $this->addSoftcoreUnlock($user3, $achievement, $now->copy()->addMinutes(5)); $staticData = StaticData::factory()->create([ 'Event_AOTW_AchievementID' => $achievement->ID, @@ -58,7 +51,7 @@ public function testGetAchievementOfTheWeek(): void 'ID' => $achievement->ID, ], 'Console' => [ - 'ID' => $system->ID, + 'ID' => $game->system_id, ], 'ForumTopic' => [ 'ID' => 1, @@ -72,17 +65,17 @@ public function testGetAchievementOfTheWeek(): void [ 'User' => $user3->User, 'RAPoints' => $user3->RAPoints, - 'HardcoreMode' => $unlock3->HardcoreMode, + 'HardcoreMode' => 0, ], [ 'User' => $this->user->User, 'RAPoints' => $this->user->RAPoints, - 'HardcoreMode' => $unlock->HardcoreMode, + 'HardcoreMode' => 0, ], [ 'User' => $user2->User, 'RAPoints' => $user2->RAPoints, - 'HardcoreMode' => $unlock2->HardcoreMode, + 'HardcoreMode' => 0, ], ], 'UnlocksCount' => 3, diff --git a/tests/Feature/Api/V1Test.php b/tests/Feature/Api/V1Test.php index be52eb070b..07dcb07b6f 100644 --- a/tests/Feature/Api/V1Test.php +++ b/tests/Feature/Api/V1Test.php @@ -14,12 +14,14 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Carbon; use Tests\Feature\Api\V1\BootstrapsApiV1; +use Tests\Feature\Platform\Concerns\TestsPlayerAchievements; use Tests\TestCase; class V1Test extends TestCase { use RefreshDatabase; use BootstrapsApiV1; + use TestsPlayerAchievements; public function testUnauthorizedResponse(): void { @@ -125,8 +127,7 @@ public function testGetAchievementOfTheWeek(): void $game = Game::factory()->create(['ConsoleID' => $system->ID]); /** @var Achievement $achievement */ $achievement = Achievement::factory()->published()->create(['GameID' => $game->ID]); - /** @var PlayerAchievementLegacy $unlock */ - $unlock = PlayerAchievementLegacy::factory()->create(['AchievementID' => $achievement->ID, 'User' => $this->user->User]); + $this->addSoftcoreUnlock($this->user, $achievement); $staticData = StaticData::factory()->create([ 'Event_AOTW_AchievementID' => $achievement->ID, @@ -154,7 +155,7 @@ public function testGetAchievementOfTheWeek(): void [ 'User' => $this->user->User, 'RAPoints' => $this->user->RAPoints, - 'HardcoreMode' => $unlock->HardcoreMode, + 'HardcoreMode' => 0, ], ], 'UnlocksCount' => 1, @@ -256,8 +257,7 @@ public function testGetAchievementUnlocks(): void $game = Game::factory()->create(['ConsoleID' => $system->ID]); /** @var Achievement $achievement */ $achievement = Achievement::factory()->published()->create(['GameID' => $game->ID]); - /** @var PlayerAchievementLegacy $unlock */ - $unlock = PlayerAchievementLegacy::factory()->create(['AchievementID' => $achievement->ID, 'User' => $this->user->User]); + $this->addSoftcoreUnlock($this->user, $achievement); $this->get($this->apiUrl('GetAchievementUnlocks', ['a' => $achievement->ID])) ->assertSuccessful() @@ -276,7 +276,7 @@ public function testGetAchievementUnlocks(): void [ 'User' => $this->user->User, 'RAPoints' => $this->user->RAPoints, - 'HardcoreMode' => $unlock->HardcoreMode, + 'HardcoreMode' => 0, ], ], 'UnlocksCount' => 1, From 383cfa096faa41a4f1864f11a20c8654772a65cd Mon Sep 17 00:00:00 2001 From: luchaos Date: Fri, 13 Oct 2023 18:11:22 +0200 Subject: [PATCH 30/92] feat: delete player_games when deleting users (#1907) * feat: delete player_games when deleting users * Improve full reset --- app/Platform/Actions/ResetPlayerProgress.php | 16 +++++++++++++++- app/Site/Actions/ClearAccountDataAction.php | 4 ---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/Platform/Actions/ResetPlayerProgress.php b/app/Platform/Actions/ResetPlayerProgress.php index 9050dc4ade..7736d643d3 100644 --- a/app/Platform/Actions/ResetPlayerProgress.php +++ b/app/Platform/Actions/ResetPlayerProgress.php @@ -74,8 +74,18 @@ public function execute(User $user, ?int $achievementID = null, ?int $gameID = n ->whereIn('AchievementID', $achievementIds) ->delete(); } else { + // fulfill deletion request + $user->playerGames()->forceDelete(); + $user->playerBadges()->delete(); $user->playerAchievements()->delete(); $user->playerAchievementsLegacy()->delete(); + + $user->RAPoints = 0; + $user->RASoftcorePoints = null; + $user->TrueRAPoints = null; + $user->ContribCount = 0; + $user->ContribYield = 0; + $user->save(); } // TODO @@ -84,9 +94,13 @@ public function execute(User $user, ?int $achievementID = null, ?int $gameID = n // dispatch(new UpdateDeveloperContributionYieldJob($author->id)); // } + $isFullReset = $achievementID === null && $gameID === null; $affectedGames = $affectedGames->unique(); foreach ($affectedGames as $affectedGameID) { - dispatch(new UpdatePlayerGameMetricsJob($user->id, $affectedGameID)); + // no use updating deleted player games if it's a full reset + if (!$isFullReset) { + dispatch(new UpdatePlayerGameMetricsJob($user->id, $affectedGameID)); + } // force the top achievers for the game to be recalculated expireGameTopAchievers($affectedGameID); diff --git a/app/Site/Actions/ClearAccountDataAction.php b/app/Site/Actions/ClearAccountDataAction.php index c18baabf9a..72f11b665d 100644 --- a/app/Site/Actions/ClearAccountDataAction.php +++ b/app/Site/Actions/ClearAccountDataAction.php @@ -45,8 +45,6 @@ public function execute(User $user): void u.SaltedPass = '', u.EmailAddress = '', u.Permissions = :permissions, - u.RAPoints = 0, - u.TrueRAPoints = null, u.fbUser = 0, u.fbPrefs = null, u.cookie = null, @@ -57,8 +55,6 @@ public function execute(User $user): void u.LastActivityID = 0, u.Motto = '', u.Untracked = 1, - u.ContribCount = 0, - u.ContribYield = 0, u.APIKey = null, u.UserWallActive = 0, u.LastGameID = 0, From 9a96ace1241887f19a77e6a7ccd5fb526ece233b Mon Sep 17 00:00:00 2001 From: luchaos Date: Fri, 13 Oct 2023 18:32:33 +0200 Subject: [PATCH 31/92] fix: achievements weighted points (TrueRatio) metrics calculation (#1911) * fix: achievements weighted points (TrueRatio) metrics calculation * remove ->values() --- .../Actions/UpdateGameAchievementsMetrics.php | 5 +- ...onTest.php => ResetPlayerProgressTest.php} | 2 +- .../UpdateGameAchievementsMetricsTest.php | 47 +++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) rename tests/Feature/Platform/Action/{ResetPlayerProgressActionTest.php => ResetPlayerProgressTest.php} (99%) create mode 100755 tests/Feature/Platform/Action/UpdateGameAchievementsMetricsTest.php diff --git a/app/Platform/Actions/UpdateGameAchievementsMetrics.php b/app/Platform/Actions/UpdateGameAchievementsMetrics.php index c84df543c0..d62cfc681b 100644 --- a/app/Platform/Actions/UpdateGameAchievementsMetrics.php +++ b/app/Platform/Actions/UpdateGameAchievementsMetrics.php @@ -34,7 +34,10 @@ public function execute(Game $game): void // force all unachieved to be 1 $unlocksHardcoreCalc = $unlocksHardcoreCount ?: 1; $weight = 0.4; - $pointsWeighted = (int) ($achievement->points * (1 - $weight)) + ($achievement->points * (($playersHardcoreCalc / $unlocksHardcoreCalc) * $weight)); + $pointsWeighted = (int) ( + $achievement->points * (1 - $weight) + + $achievement->points * (($playersHardcoreCalc / $unlocksHardcoreCalc) * $weight) + ); $pointsWeightedTotal += $pointsWeighted; $achievement->unlocks_total = $unlocksCount; diff --git a/tests/Feature/Platform/Action/ResetPlayerProgressActionTest.php b/tests/Feature/Platform/Action/ResetPlayerProgressTest.php similarity index 99% rename from tests/Feature/Platform/Action/ResetPlayerProgressActionTest.php rename to tests/Feature/Platform/Action/ResetPlayerProgressTest.php index 1905e899a1..dcffac5859 100755 --- a/tests/Feature/Platform/Action/ResetPlayerProgressActionTest.php +++ b/tests/Feature/Platform/Action/ResetPlayerProgressTest.php @@ -13,7 +13,7 @@ use Tests\Feature\Platform\Concerns\TestsPlayerBadges; use Tests\TestCase; -class ResetPlayerProgressActionTest extends TestCase +class ResetPlayerProgressTest extends TestCase { use RefreshDatabase; diff --git a/tests/Feature/Platform/Action/UpdateGameAchievementsMetricsTest.php b/tests/Feature/Platform/Action/UpdateGameAchievementsMetricsTest.php new file mode 100755 index 0000000000..79159caffb --- /dev/null +++ b/tests/Feature/Platform/Action/UpdateGameAchievementsMetricsTest.php @@ -0,0 +1,47 @@ +count(10)->create(); + $game = $this->seedGame(withHash: false); + Achievement::factory()->published()->count(10)->create(['GameID' => $game->id, 'Points' => 3]); + + foreach (User::all() as $index => $user) { + for ($i = 0; $i <= $index; $i++) { + $this->addHardcoreUnlock($user, Achievement::find($i + 1)); + } + } + + $achievements = Achievement::all(); + $this->assertEquals( + [10, 9, 8, 7, 6, 5, 4, 3, 2, 1], + $achievements->pluck('unlocks_total')->toArray() + ); + $this->assertEquals( + [1, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1], + $achievements->pluck('unlock_percentage')->toArray() + ); + $this->assertEquals( + [3, 3, 3, 3, 3, 4, 4, 5, 7, 13], + $achievements->pluck('points_weighted')->toArray() + ); + } +} From 375d1523f348031411767c939a2fda4566dad9d2 Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Fri, 13 Oct 2023 12:01:25 -0600 Subject: [PATCH 32/92] add Pizza Boy emulators to download page (#1886) --- app/Platform/Enums/Emulators.php | 4 ++++ storage/app/releases.dist.php | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/app/Platform/Enums/Emulators.php b/app/Platform/Enums/Emulators.php index ef586f0fdd..aee6bc8838 100644 --- a/app/Platform/Enums/Emulators.php +++ b/app/Platform/Enums/Emulators.php @@ -49,6 +49,10 @@ abstract class Emulators public const WinArcadia = 'winarcadia'; + public const PizzaBoyGBC = 'pizzaboygbc'; + + public const PizzaBoyGBA = 'pizzaboygba'; + public static function cases(): array { return [ diff --git a/storage/app/releases.dist.php b/storage/app/releases.dist.php index 8ca4694eea..c725d47101 100644 --- a/storage/app/releases.dist.php +++ b/storage/app/releases.dist.php @@ -211,6 +211,26 @@ 75, // Elektor TV Games Computer ], ], + Emulators::PizzaBoyGBC => [ + 'name' => 'Pizza Boy GBC', + 'handle' => 'Pizza Boy GBC', + 'active' => true, + 'description' => 'NOTE: Only runs on Android devices.', + 'download_url' => 'https://play.google.com/store/apps/details?id=it.dbtecno.pizzaboy', + 'systems' => [ + 6, // Gameboy Color + ], + ], + Emulators::PizzaBoyGBA => [ + 'name' => 'Pizza Boy GBA', + 'handle' => 'Pizza Boy GBA', + 'active' => true, + 'description' => 'NOTE: Only runs on Android devices.', + 'download_url' => 'https://play.google.com/store/apps/details?id=it.dbtecno.pizzaboygba', + 'systems' => [ + 5, // Gameboy Advance + ], + ], Emulators::RAppleWin => [ 'minimum_version' => '1.1.1', 'latest_version' => '1.3.0', From d10fd516df9b704ad39c45dc5009377e41b6623a Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Fri, 13 Oct 2023 14:04:24 -0400 Subject: [PATCH 33/92] chore(deps): bump packages with active CVEs (#1903) Co-authored-by: luchaos --- package-lock.json | 682 +++++++++++++++++++++------------------------- package.json | 4 +- 2 files changed, 315 insertions(+), 371 deletions(-) diff --git a/package-lock.json b/package-lock.json index 98fbd82343..c79d510a9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,12 +33,12 @@ "eslint-plugin-import": "^2.27.5", "laravel-echo": "^1.15.1", "laravel-vite-plugin": "^0.7.8", - "postcss": "^8.4.24", + "postcss": "^8.4.31", "pusher-js": "^8.2.0", "tailwindcss": "^3.3.2", "typescript": "^4.9.5", "vite": "^4.3.9", - "vitest": "^0.32.2" + "vitest": "^0.34.6" }, "engines": { "node": "18.x", @@ -398,12 +398,12 @@ } }, "node_modules/@jest/schemas": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", - "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "dependencies": { - "@sinclair/typebox": "^0.25.16" + "@sinclair/typebox": "^0.27.8" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -542,9 +542,9 @@ "dev": true }, "node_modules/@sinclair/typebox": { - "version": "0.25.24", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, "node_modules/@tailwindcss/aspect-ratio": { @@ -1081,29 +1081,28 @@ } }, "node_modules/@vitest/expect": { - "version": "0.32.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.32.2.tgz", - "integrity": "sha512-6q5yzweLnyEv5Zz1fqK5u5E83LU+gOMVBDuxBl2d2Jfx1BAp5M+rZgc5mlyqdnxquyoiOXpXmFNkcGcfFnFH3Q==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", "dev": true, "dependencies": { - "@vitest/spy": "0.32.2", - "@vitest/utils": "0.32.2", - "chai": "^4.3.7" + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "0.32.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.32.2.tgz", - "integrity": "sha512-06vEL0C1pomOEktGoLjzZw+1Fb+7RBRhmw/06WkDrd1akkT9i12su0ku+R/0QM69dfkIL/rAIDTG+CSuQVDcKw==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", "dev": true, "dependencies": { - "@vitest/utils": "0.32.2", - "concordance": "^5.0.4", + "@vitest/utils": "0.34.6", "p-limit": "^4.0.0", - "pathe": "^1.1.0" + "pathe": "^1.1.1" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1137,45 +1136,109 @@ } }, "node_modules/@vitest/snapshot": { - "version": "0.32.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.32.2.tgz", - "integrity": "sha512-JwhpeH/PPc7GJX38vEfCy9LtRzf9F4er7i4OsAJyV7sjPwjj+AIR8cUgpMTWK4S3TiamzopcTyLsZDMuldoi5A==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", "dev": true, "dependencies": { - "magic-string": "^0.30.0", - "pathe": "^1.1.0", - "pretty-format": "^27.5.1" + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vitest/snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@vitest/snapshot/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/@vitest/spy": { - "version": "0.32.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.32.2.tgz", - "integrity": "sha512-Q/ZNILJ4ca/VzQbRM8ur3Si5Sardsh1HofatG9wsJY1RfEaw0XKP8IVax2lI1qnrk9YPuG9LA2LkZ0EI/3d4ug==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", "dev": true, "dependencies": { - "tinyspy": "^2.1.0" + "tinyspy": "^2.1.1" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "0.32.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.32.2.tgz", - "integrity": "sha512-lnJ0T5i03j0IJaeW73hxe2AuVnZ/y1BhhCOuIcl9LIzXnbpXJT9Lrt6brwKHXLOiA7MZ6N5hSJjt0xE1dGNCzQ==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", "dev": true, "dependencies": { "diff-sequences": "^29.4.3", "loupe": "^2.3.6", - "pretty-format": "^27.5.1" + "pretty-format": "^29.5.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vitest/utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@vitest/utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/@vue/reactivity": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", @@ -1198,9 +1261,9 @@ "peer": true }, "node_modules/acorn": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", - "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -1484,12 +1547,6 @@ "node": ">=8" } }, - "node_modules/blueimp-md5": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", - "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", - "dev": true - }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -1604,18 +1661,18 @@ ] }, "node_modules/chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" }, "engines": { "node": ">=4" @@ -1638,10 +1695,13 @@ } }, "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, "engines": { "node": "*" } @@ -1747,40 +1807,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/concordance": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz", - "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==", - "dev": true, - "dependencies": { - "date-time": "^3.1.0", - "esutils": "^2.0.3", - "fast-diff": "^1.2.0", - "js-string-escape": "^1.0.1", - "lodash": "^4.17.15", - "md5-hex": "^3.0.1", - "semver": "^7.3.2", - "well-known-symbols": "^2.0.0" - }, - "engines": { - "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14" - } - }, - "node_modules/concordance/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -1849,18 +1875,6 @@ "node": ">=14" } }, - "node_modules/date-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", - "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==", - "dev": true, - "dependencies": { - "time-zone": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2732,12 +2746,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true - }, "node_modules/fast-glob": { "version": "3.2.12", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", @@ -2946,9 +2954,9 @@ } }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" @@ -3892,15 +3900,6 @@ "node": ">=14" } }, - "node_modules/js-string-escape": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", - "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4131,29 +4130,17 @@ } }, "node_modules/magic-string": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", - "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "version": "0.30.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", + "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" + "@jridgewell/sourcemap-codec": "^1.4.15" }, "engines": { "node": ">=12" } }, - "node_modules/md5-hex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", - "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==", - "dev": true, - "dependencies": { - "blueimp-md5": "^2.10.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4244,15 +4231,15 @@ } }, "node_modules/mlly": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.0.tgz", - "integrity": "sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", "dev": true, "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.10.0", "pathe": "^1.1.1", "pkg-types": "^1.0.3", - "ufo": "^1.1.2" + "ufo": "^1.3.0" } }, "node_modules/ms": { @@ -4639,9 +4626,9 @@ } }, "node_modules/postcss": { - "version": "8.4.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", - "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -5183,9 +5170,9 @@ "dev": true }, "node_modules/std-env": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz", - "integrity": "sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", + "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", "dev": true }, "node_modules/stop-iteration-iterator": { @@ -5495,15 +5482,6 @@ "node": ">=0.8" } }, - "node_modules/time-zone": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", - "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/tiny-glob": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", @@ -5521,18 +5499,18 @@ "dev": true }, "node_modules/tinypool": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.5.0.tgz", - "integrity": "sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz", - "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", "dev": true, "engines": { "node": ">=14.0.0" @@ -5705,9 +5683,9 @@ } }, "node_modules/ufo": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.2.tgz", - "integrity": "sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz", + "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", "dev": true }, "node_modules/unbox-primitive": { @@ -5842,17 +5820,17 @@ } }, "node_modules/vite-node": { - "version": "0.32.2", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.32.2.tgz", - "integrity": "sha512-dTQ1DCLwl2aEseov7cfQ+kDMNJpM1ebpyMMMwWzBvLbis8Nla/6c9WQcqpPssTwS6Rp/+U6KwlIj8Eapw4bLdA==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", "dev": true, "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", - "mlly": "^1.2.0", - "pathe": "^1.1.0", + "mlly": "^1.4.0", + "pathe": "^1.1.1", "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -5878,35 +5856,34 @@ } }, "node_modules/vitest": { - "version": "0.32.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.32.2.tgz", - "integrity": "sha512-hU8GNNuQfwuQmqTLfiKcqEhZY72Zxb7nnN07koCUNmntNxbKQnVbeIS6sqUgR3eXSlbOpit8+/gr1KpqoMgWCQ==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", "dev": true, "dependencies": { "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.32.2", - "@vitest/runner": "0.32.2", - "@vitest/snapshot": "0.32.2", - "@vitest/spy": "0.32.2", - "@vitest/utils": "0.32.2", - "acorn": "^8.8.2", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "acorn": "^8.9.0", "acorn-walk": "^8.2.0", "cac": "^6.7.14", - "chai": "^4.3.7", - "concordance": "^5.0.4", + "chai": "^4.3.10", "debug": "^4.3.4", "local-pkg": "^0.4.3", - "magic-string": "^0.30.0", - "pathe": "^1.1.0", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", "picocolors": "^1.0.0", - "std-env": "^3.3.2", + "std-env": "^3.3.3", "strip-literal": "^1.0.1", "tinybench": "^2.5.0", - "tinypool": "^0.5.0", - "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.32.2", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", "why-is-node-running": "^2.2.2" }, "bin": { @@ -5989,15 +5966,6 @@ "node": ">=12" } }, - "node_modules/well-known-symbols": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", - "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", @@ -6479,12 +6447,12 @@ } }, "@jest/schemas": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", - "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "requires": { - "@sinclair/typebox": "^0.25.16" + "@sinclair/typebox": "^0.27.8" } }, "@jest/types": { @@ -6597,9 +6565,9 @@ } }, "@sinclair/typebox": { - "version": "0.25.24", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, "@tailwindcss/aspect-ratio": { @@ -7004,26 +6972,25 @@ } }, "@vitest/expect": { - "version": "0.32.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.32.2.tgz", - "integrity": "sha512-6q5yzweLnyEv5Zz1fqK5u5E83LU+gOMVBDuxBl2d2Jfx1BAp5M+rZgc5mlyqdnxquyoiOXpXmFNkcGcfFnFH3Q==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", "dev": true, "requires": { - "@vitest/spy": "0.32.2", - "@vitest/utils": "0.32.2", - "chai": "^4.3.7" + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" } }, "@vitest/runner": { - "version": "0.32.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.32.2.tgz", - "integrity": "sha512-06vEL0C1pomOEktGoLjzZw+1Fb+7RBRhmw/06WkDrd1akkT9i12su0ku+R/0QM69dfkIL/rAIDTG+CSuQVDcKw==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", "dev": true, "requires": { - "@vitest/utils": "0.32.2", - "concordance": "^5.0.4", + "@vitest/utils": "0.34.6", "p-limit": "^4.0.0", - "pathe": "^1.1.0" + "pathe": "^1.1.1" }, "dependencies": { "p-limit": { @@ -7044,34 +7011,84 @@ } }, "@vitest/snapshot": { - "version": "0.32.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.32.2.tgz", - "integrity": "sha512-JwhpeH/PPc7GJX38vEfCy9LtRzf9F4er7i4OsAJyV7sjPwjj+AIR8cUgpMTWK4S3TiamzopcTyLsZDMuldoi5A==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", "dev": true, "requires": { - "magic-string": "^0.30.0", - "pathe": "^1.1.0", - "pretty-format": "^27.5.1" + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } } }, "@vitest/spy": { - "version": "0.32.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.32.2.tgz", - "integrity": "sha512-Q/ZNILJ4ca/VzQbRM8ur3Si5Sardsh1HofatG9wsJY1RfEaw0XKP8IVax2lI1qnrk9YPuG9LA2LkZ0EI/3d4ug==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", "dev": true, "requires": { - "tinyspy": "^2.1.0" + "tinyspy": "^2.1.1" } }, "@vitest/utils": { - "version": "0.32.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.32.2.tgz", - "integrity": "sha512-lnJ0T5i03j0IJaeW73hxe2AuVnZ/y1BhhCOuIcl9LIzXnbpXJT9Lrt6brwKHXLOiA7MZ6N5hSJjt0xE1dGNCzQ==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", "dev": true, "requires": { "diff-sequences": "^29.4.3", "loupe": "^2.3.6", - "pretty-format": "^27.5.1" + "pretty-format": "^29.5.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } } }, "@vue/reactivity": { @@ -7096,9 +7113,9 @@ "peer": true }, "acorn": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", - "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true }, "acorn-globals": { @@ -7302,12 +7319,6 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, - "blueimp-md5": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", - "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", - "dev": true - }, "brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -7373,18 +7384,18 @@ "dev": true }, "chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "dev": true, "requires": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" } }, "chalk": { @@ -7398,10 +7409,13 @@ } }, "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } }, "chokidar": { "version": "3.5.3", @@ -7474,33 +7488,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "concordance": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz", - "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==", - "dev": true, - "requires": { - "date-time": "^3.1.0", - "esutils": "^2.0.3", - "fast-diff": "^1.2.0", - "js-string-escape": "^1.0.1", - "lodash": "^4.17.15", - "md5-hex": "^3.0.1", - "semver": "^7.3.2", - "well-known-symbols": "^2.0.0" - }, - "dependencies": { - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, "confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -7554,15 +7541,6 @@ "whatwg-url": "^12.0.0" } }, - "date-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", - "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==", - "dev": true, - "requires": { - "time-zone": "^1.0.0" - } - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -8239,12 +8217,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true - }, "fast-glob": { "version": "3.2.12", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", @@ -8408,9 +8380,9 @@ "dev": true }, "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true }, "get-intrinsic": { @@ -9092,12 +9064,6 @@ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==" }, - "js-string-escape": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", - "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", - "dev": true - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9278,21 +9244,12 @@ "dev": true }, "magic-string": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", - "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "version": "0.30.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", + "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", "dev": true, "requires": { - "@jridgewell/sourcemap-codec": "^1.4.13" - } - }, - "md5-hex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", - "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==", - "dev": true, - "requires": { - "blueimp-md5": "^2.10.0" + "@jridgewell/sourcemap-codec": "^1.4.15" } }, "merge2": { @@ -9358,15 +9315,15 @@ "dev": true }, "mlly": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.0.tgz", - "integrity": "sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", "dev": true, "requires": { - "acorn": "^8.9.0", + "acorn": "^8.10.0", "pathe": "^1.1.1", "pkg-types": "^1.0.3", - "ufo": "^1.1.2" + "ufo": "^1.3.0" } }, "ms": { @@ -9648,9 +9605,9 @@ } }, "postcss": { - "version": "8.4.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", - "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "requires": { "nanoid": "^3.3.6", @@ -10016,9 +9973,9 @@ "dev": true }, "std-env": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz", - "integrity": "sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", + "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", "dev": true }, "stop-iteration-iterator": { @@ -10260,12 +10217,6 @@ "thenify": ">= 3.1.0 < 4" } }, - "time-zone": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", - "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==", - "dev": true - }, "tiny-glob": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", @@ -10283,15 +10234,15 @@ "dev": true }, "tinypool": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.5.0.tgz", - "integrity": "sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", "dev": true }, "tinyspy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz", - "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", "dev": true }, "to-regex-range": { @@ -10411,9 +10362,9 @@ "integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==" }, "ufo": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.2.tgz", - "integrity": "sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz", + "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", "dev": true }, "unbox-primitive": { @@ -10486,17 +10437,17 @@ } }, "vite-node": { - "version": "0.32.2", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.32.2.tgz", - "integrity": "sha512-dTQ1DCLwl2aEseov7cfQ+kDMNJpM1ebpyMMMwWzBvLbis8Nla/6c9WQcqpPssTwS6Rp/+U6KwlIj8Eapw4bLdA==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", "dev": true, "requires": { "cac": "^6.7.14", "debug": "^4.3.4", - "mlly": "^1.2.0", - "pathe": "^1.1.0", + "mlly": "^1.4.0", + "pathe": "^1.1.1", "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" } }, "vite-plugin-full-reload": { @@ -10510,35 +10461,34 @@ } }, "vitest": { - "version": "0.32.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.32.2.tgz", - "integrity": "sha512-hU8GNNuQfwuQmqTLfiKcqEhZY72Zxb7nnN07koCUNmntNxbKQnVbeIS6sqUgR3eXSlbOpit8+/gr1KpqoMgWCQ==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", "dev": true, "requires": { "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.32.2", - "@vitest/runner": "0.32.2", - "@vitest/snapshot": "0.32.2", - "@vitest/spy": "0.32.2", - "@vitest/utils": "0.32.2", - "acorn": "^8.8.2", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "acorn": "^8.9.0", "acorn-walk": "^8.2.0", "cac": "^6.7.14", - "chai": "^4.3.7", - "concordance": "^5.0.4", + "chai": "^4.3.10", "debug": "^4.3.4", "local-pkg": "^0.4.3", - "magic-string": "^0.30.0", - "pathe": "^1.1.0", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", "picocolors": "^1.0.0", - "std-env": "^3.3.2", + "std-env": "^3.3.3", "strip-literal": "^1.0.1", "tinybench": "^2.5.0", - "tinypool": "^0.5.0", - "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.32.2", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", "why-is-node-running": "^2.2.2" }, "dependencies": { @@ -10569,12 +10519,6 @@ "optional": true, "peer": true }, - "well-known-symbols": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", - "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", - "dev": true - }, "whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", diff --git a/package.json b/package.json index a319ab91d8..1a2a6ebf96 100644 --- a/package.json +++ b/package.json @@ -41,12 +41,12 @@ "eslint-plugin-import": "^2.27.5", "laravel-echo": "^1.15.1", "laravel-vite-plugin": "^0.7.8", - "postcss": "^8.4.24", + "postcss": "^8.4.31", "pusher-js": "^8.2.0", "tailwindcss": "^3.3.2", "typescript": "^4.9.5", "vite": "^4.3.9", - "vitest": "^0.32.2" + "vitest": "^0.34.6" }, "engines": { "node": "18.x", From e13c9841feb324b3da8f8f8ed8f1a40d879ab1f4 Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Fri, 13 Oct 2023 12:36:55 -0600 Subject: [PATCH 34/92] allow aggregate data use on global ranking page (#1912) --- app/Helpers/render/leaderboard.php | 66 ++++++++++++------- .../Actions/UpdatePlayerGameMetrics.php | 2 +- app/Platform/Actions/UpdatePlayerMetrics.php | 18 +++-- .../Commands/UpdatePlayerGameMetrics.php | 14 +++- 4 files changed, 67 insertions(+), 33 deletions(-) diff --git a/app/Helpers/render/leaderboard.php b/app/Helpers/render/leaderboard.php index 7cc3a9eb0a..3ce6b1b3ae 100644 --- a/app/Helpers/render/leaderboard.php +++ b/app/Helpers/render/leaderboard.php @@ -467,32 +467,48 @@ function getGlobalRankingData( } if ($info == 0) { - if ($unlockMode == UnlockMode::Hardcore) { - $selectQuery = "SELECT ua.User, - (SELECT COALESCE(SUM(CASE WHEN aw.HardcoreMode = " . UnlockMode::Hardcore . " - THEN 1 ELSE 0 END), 0) - FROM Awarded AS aw - JOIN Achievements AS ach ON aw.AchievementID = ach.ID - JOIN GameData as gd ON ach.GameID = gd.ID - WHERE aw.User = ua.User AND gd.ConsoleID NOT IN (100, 101) - AND ach.Flags = " . AchievementFlag::OfficialCore . " - ) AS AchievementCount, - COALESCE(ua.RAPoints, 0) AS Points, - COALESCE(ua.TrueRAPoints, 0) AS RetroPoints, - COALESCE(ROUND(ua.TrueRAPoints/ua.RAPoints, 2), 0) AS RetroRatio "; + if (config('feature.aggregate_queries')) { + if ($unlockMode == UnlockMode::Hardcore) { + $selectQuery = "SELECT ua.User, + COALESCE(ua.achievements_unlocked_hardcore, 0) AS AchievementCount, + COALESCE(ua.RAPoints, 0) AS Points, + COALESCE(ua.TrueRAPoints, 0) AS RetroPoints, + COALESCE(ROUND(ua.TrueRAPoints/ua.RAPoints, 2), 0) AS RetroRatio "; + } else { + $selectQuery = "SELECT ua.User, + COALESCE(ua.achievements_unlocked - ua.achievements_unlocked_hardcore, 0) AS AchievementCount, + COALESCE(ua.RASoftcorePoints, 0) AS Points, + 0 AS RetroPoints, + 0 AS RetroRatio "; + } } else { - $selectQuery = "SELECT ua.User, - (SELECT COALESCE(SUM(CASE WHEN aw.HardcoreMode = " . UnlockMode::Softcore . " - THEN 1 ELSE -1 END), 0) - FROM Awarded AS aw - JOIN Achievements AS ach ON aw.AchievementID = ach.ID - JOIN GameData as gd ON ach.GameID = gd.ID - WHERE aw.User = ua.User AND gd.ConsoleID NOT IN (100, 101) - AND ach.Flags = " . AchievementFlag::OfficialCore . " - ) AS AchievementCount, - COALESCE(ua.RASoftcorePoints, 0) AS Points, - 0 AS RetroPoints, - 0 AS RetroRatio "; + if ($unlockMode == UnlockMode::Hardcore) { + $selectQuery = "SELECT ua.User, + (SELECT COALESCE(SUM(CASE WHEN aw.HardcoreMode = " . UnlockMode::Hardcore . " + THEN 1 ELSE 0 END), 0) + FROM Awarded AS aw + JOIN Achievements AS ach ON aw.AchievementID = ach.ID + JOIN GameData as gd ON ach.GameID = gd.ID + WHERE aw.User = ua.User AND gd.ConsoleID NOT IN (100, 101) + AND ach.Flags = " . AchievementFlag::OfficialCore . " + ) AS AchievementCount, + COALESCE(ua.RAPoints, 0) AS Points, + COALESCE(ua.TrueRAPoints, 0) AS RetroPoints, + COALESCE(ROUND(ua.TrueRAPoints/ua.RAPoints, 2), 0) AS RetroRatio "; + } else { + $selectQuery = "SELECT ua.User, + (SELECT COALESCE(SUM(CASE WHEN aw.HardcoreMode = " . UnlockMode::Softcore . " + THEN 1 ELSE -1 END), 0) + FROM Awarded AS aw + JOIN Achievements AS ach ON aw.AchievementID = ach.ID + JOIN GameData as gd ON ach.GameID = gd.ID + WHERE aw.User = ua.User AND gd.ConsoleID NOT IN (100, 101) + AND ach.Flags = " . AchievementFlag::OfficialCore . " + ) AS AchievementCount, + COALESCE(ua.RASoftcorePoints, 0) AS Points, + 0 AS RetroPoints, + 0 AS RetroRatio "; + } } } else { if ($unlockMode == UnlockMode::Hardcore) { diff --git a/app/Platform/Actions/UpdatePlayerGameMetrics.php b/app/Platform/Actions/UpdatePlayerGameMetrics.php index 11f3d978c8..dba2770014 100644 --- a/app/Platform/Actions/UpdatePlayerGameMetrics.php +++ b/app/Platform/Actions/UpdatePlayerGameMetrics.php @@ -37,7 +37,7 @@ public function execute(PlayerGame $playerGame, bool $silent = false): void $points = $achievementsUnlocked->sum('Points'); $pointsHardcore = $achievementsUnlockedHardcore->sum('Points'); - $pointsWeighted = $achievementsUnlocked->sum('TrueRatio'); + $pointsWeighted = $achievementsUnlockedHardcore->sum('TrueRatio'); $playerAchievements = $achievementsUnlocked->pluck('pivot'); $playerAchievementsHardcore = $playerAchievements->whereNotNull('unlocked_hardcore_at'); diff --git a/app/Platform/Actions/UpdatePlayerMetrics.php b/app/Platform/Actions/UpdatePlayerMetrics.php index a5e0d7ad19..3fd548bf56 100644 --- a/app/Platform/Actions/UpdatePlayerMetrics.php +++ b/app/Platform/Actions/UpdatePlayerMetrics.php @@ -11,16 +11,24 @@ class UpdatePlayerMetrics { public function execute(User $user): void { - $playerGames = $user->playerGames()->where('achievements_unlocked', '>', 0); + $playerGames = $user->playerGames() + ->join('GameData', 'GameData.ID', '=', 'player_games.game_id') + ->whereNotIn('GameData.ConsoleID', [100, 101]) // ignore events and hubs + ->where('achievements_unlocked', '>', 0); $user->achievements_unlocked = $playerGames->sum('achievements_unlocked'); $user->achievements_unlocked_hardcore = $playerGames->sum('achievements_unlocked_hardcore'); $user->completion_percentage_average = $playerGames->average('completion_percentage'); $user->completion_percentage_average_hardcore = $playerGames->average('completion_percentage_hardcore'); - // TODO refactor to use aggregated player_games metrics - $user->RAPoints = $user->achievements()->published()->wherePivotNotNull('unlocked_hardcore_at')->sum('Points'); - $user->RASoftcorePoints = $user->achievements()->published()->wherePivotNull('unlocked_hardcore_at')->sum('Points'); - $user->TrueRAPoints = $user->achievements()->published()->wherePivotNotNull('unlocked_hardcore_at')->sum('TrueRatio'); + if (config('feature.aggregate_queries')) { + $user->RAPoints = $playerGames->sum('points_hardcore'); + $user->RASoftcorePoints = $playerGames->sum('points') - $user->RAPoints; + $user->TrueRAPoints = $playerGames->sum('points_weighted'); + } else { + $user->RAPoints = $user->achievements()->published()->wherePivotNotNull('unlocked_hardcore_at')->sum('Points'); + $user->RASoftcorePoints = $user->achievements()->published()->wherePivotNull('unlocked_hardcore_at')->sum('Points'); + $user->TrueRAPoints = $user->achievements()->published()->wherePivotNotNull('unlocked_hardcore_at')->sum('TrueRatio'); + } $user->save(); diff --git a/app/Platform/Commands/UpdatePlayerGameMetrics.php b/app/Platform/Commands/UpdatePlayerGameMetrics.php index 2c218f3000..c3a8d24089 100644 --- a/app/Platform/Commands/UpdatePlayerGameMetrics.php +++ b/app/Platform/Commands/UpdatePlayerGameMetrics.php @@ -5,6 +5,7 @@ namespace App\Platform\Commands; use App\Platform\Actions\UpdatePlayerGameMetrics as UpdatePlayerGameMetricsAction; +use App\Platform\Actions\UpdatePlayerMetrics as UpdatePlayerMetricsAction; use App\Platform\Models\Game; use App\Site\Models\User; use Illuminate\Console\Command; @@ -18,7 +19,8 @@ class UpdatePlayerGameMetrics extends Command protected $description = 'Update player game(s) metrics'; public function __construct( - private readonly UpdatePlayerGameMetricsAction $updatePlayerGameMetrics + private readonly UpdatePlayerGameMetricsAction $updatePlayerGameMetrics, + private readonly UpdatePlayerMetricsAction $updatePlayerMetrics ) { parent::__construct(); } @@ -49,11 +51,19 @@ public function handle(): void $progressBar = $this->output->createProgressBar($playerGames->count()); $progressBar->start(); + // if running in sync mode, just call updatePlayerMetrics once manually after updating + // all of the player_games instead of letting it cascade for every player_game updated. + $isSync = (config('queue.default') === 'sync'); + foreach ($playerGames as $playerGame) { - $this->updatePlayerGameMetrics->execute($playerGame); + $this->updatePlayerGameMetrics->execute($playerGame, silent: $isSync); $progressBar->advance(); } + if ($isSync) { + $this->updatePlayerMetrics->execute($user); + } + $progressBar->finish(); } } From 612f0c72239171536f3430e3fcec29d2df5f80dd Mon Sep 17 00:00:00 2001 From: luchaos Date: Fri, 13 Oct 2023 23:58:06 +0200 Subject: [PATCH 35/92] Increase max processes for horizon queues --- config/horizon.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/horizon.php b/config/horizon.php index 3f012bb18b..71f74d70f4 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -214,7 +214,7 @@ 'environments' => [ 'production' => [ 'supervisor-1' => [ - 'maxProcesses' => 16, + 'maxProcesses' => 20, 'balanceMaxShift' => 1, 'balanceCooldown' => 3, ], @@ -222,7 +222,7 @@ 'stage' => [ 'supervisor-1' => [ - 'maxProcesses' => 16, + 'maxProcesses' => 20, 'balanceMaxShift' => 1, 'balanceCooldown' => 3, ], From f85e2d950a8ee27f3046a947ab2ab056b6346136 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 00:14:21 +0200 Subject: [PATCH 36/92] Directly update player metrics when updating player game metrics in batches --- app/Platform/Jobs/UpdatePlayerGameMetricsJob.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php index c5d0642c97..79fdeb3e55 100644 --- a/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php +++ b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php @@ -3,7 +3,9 @@ namespace App\Platform\Jobs; use App\Platform\Actions\UpdatePlayerGameMetrics; +use App\Platform\Actions\UpdatePlayerMetrics; use App\Platform\Models\PlayerGame; +use App\Site\Models\User; use Illuminate\Bus\Batchable; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; @@ -41,15 +43,21 @@ public function handle(): void return; } - $silent = $this->batchId !== null; + $isBatched = $this->batchId !== null; app()->make(UpdatePlayerGameMetrics::class) - ->execute($playerGame, $silent); + ->execute($playerGame, silent: $isBatched); // if this job was executed from within a batch it means that it's been initiated // by a game metrics update. // make sure to update player metrics directly, as the silent flag will not // trigger an event (to not further cascade into another game metrics update). - $this->batch()?->add(new UpdatePlayerMetricsJob($playerGame->user_id)); + if ($isBatched) { + $user = User::find($this->userId); + if ($user) { + app()->make(UpdatePlayerMetrics::class) + ->execute($user); + } + } } } From eb0745f38c4acb212f4e8e8534c349d7634dead3 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 00:32:51 +0200 Subject: [PATCH 37/92] do not retry queued jobs too soon --- config/horizon.php | 2 +- config/queue.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/horizon.php b/config/horizon.php index 71f74d70f4..89ba0be8fe 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -206,7 +206,7 @@ 'maxJobs' => 0, 'memory' => 128, 'tries' => 1, - 'timeout' => 240, + 'timeout' => 240, // NOTE this should match queue config's retry_after 'nice' => 0, ], ], diff --git a/config/queue.php b/config/queue.php index 2d61e35fd9..7ac1871251 100755 --- a/config/queue.php +++ b/config/queue.php @@ -63,7 +63,7 @@ 'driver' => 'redis', 'connection' => 'queue', 'queue' => env('REDIS_QUEUE', 'default'), - 'retry_after' => 90, + 'retry_after' => 240, // NOTE this should match horizon config's timeout 'block_for' => null, ], From 4a6e481bc65b72b7872e339afc7b5991861c6d17 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 00:33:31 +0200 Subject: [PATCH 38/92] disable ad-hoc player metrics updates within batched player game metrics update jobs until player_games are populated --- app/Platform/Jobs/UpdatePlayerGameMetricsJob.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php index 79fdeb3e55..289077de13 100644 --- a/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php +++ b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php @@ -52,12 +52,13 @@ public function handle(): void // by a game metrics update. // make sure to update player metrics directly, as the silent flag will not // trigger an event (to not further cascade into another game metrics update). - if ($isBatched) { - $user = User::find($this->userId); - if ($user) { - app()->make(UpdatePlayerMetrics::class) - ->execute($user); - } - } + // TODO enable this again as soon as player_games are all populated and are used for players' points aggregation + // if ($isBatched) { + // $user = User::find($this->userId); + // if ($user) { + // app()->make(UpdatePlayerMetrics::class) + // ->execute($user); + // } + // } } } From 215c6e7dd5f9f5ed41fb2c0b7a355f75383ec1fa Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 01:27:15 +0200 Subject: [PATCH 39/92] prevent race conditions where player game metrics updates might already be running when update status is yet to be added --- app/Platform/Actions/UpdateGameMetrics.php | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php index 4140875539..093bac70ef 100644 --- a/app/Platform/Actions/UpdateGameMetrics.php +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -90,16 +90,13 @@ public function execute(Game $game): void // Ad-hoc updates for player games metrics and player metrics after achievement set version changes // Note: this might dispatch multiple thousands of jobs depending on a game's players count - $affectedPlayerGamesQuery = $game->playerGames() - ->where(function ($query) use ($game) { - $query->whereNot('achievement_set_version_hash', '=', $game->achievement_set_version_hash) - ->orWhereNull('achievement_set_version_hash'); - }); - // add all affected player games to the update queue in batches if (config('queue.default') !== 'sync') { - (clone $affectedPlayerGamesQuery) - ->whereNull('update_status') + $game->playerGames() + ->where(function ($query) use ($game) { + $query->whereNot('achievement_set_version_hash', '=', $game->achievement_set_version_hash) + ->orWhereNull('achievement_set_version_hash'); + }) ->orderByDesc('last_played_at') ->chunk(1000, function (Collection $chunk) { // map and dispatch this chunk as a batch of jobs @@ -112,14 +109,5 @@ public function execute(Game $game): void ->dispatch(); }); } - - // directly update player games to make sure they aren't added to a batch again - // and contain the most important updates right away for presentation - (clone $affectedPlayerGamesQuery) - ->update([ - 'update_status' => 'version_mismatch', - 'points_total' => $game->points_total, - 'achievements_total' => $game->achievements_published, - ]); } } From f58849296a127c6f00c4abfd8fe14d543348eebd Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 02:21:06 +0200 Subject: [PATCH 40/92] Reduce batch size and catch errors --- app/Platform/Actions/UpdateGameMetrics.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php index 093bac70ef..006470838c 100644 --- a/app/Platform/Actions/UpdateGameMetrics.php +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -8,8 +8,11 @@ use App\Platform\Jobs\UpdatePlayerGameMetricsJob; use App\Platform\Models\Game; use App\Platform\Models\PlayerGame; +use Illuminate\Bus\Batch; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Bus; +use Illuminate\Support\Facades\Log; +use Throwable; class UpdateGameMetrics { @@ -98,7 +101,7 @@ public function execute(Game $game): void ->orWhereNull('achievement_set_version_hash'); }) ->orderByDesc('last_played_at') - ->chunk(1000, function (Collection $chunk) { + ->chunk(500, function (Collection $chunk) { // map and dispatch this chunk as a batch of jobs Bus::batch( $chunk->map( @@ -106,6 +109,10 @@ public function execute(Game $game): void ) ) ->onQueue('player-game-metrics-batch') + ->catch(function (Batch $batch, Throwable $e) { + // First batch job failure detected... + Log::error('Batch error: ' . $batch->id, ['exception' => $e]); + }) ->dispatch(); }); } From b92b8ef3bd0c6adec91aca7c651ac8bb5daf5d86 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 02:26:41 +0200 Subject: [PATCH 41/92] Chunk player games updates more reliably --- app/Platform/Actions/UpdateGameMetrics.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php index 006470838c..4f95825829 100644 --- a/app/Platform/Actions/UpdateGameMetrics.php +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -100,8 +100,8 @@ public function execute(Game $game): void $query->whereNot('achievement_set_version_hash', '=', $game->achievement_set_version_hash) ->orWhereNull('achievement_set_version_hash'); }) - ->orderByDesc('last_played_at') - ->chunk(500, function (Collection $chunk) { + ->orderBy('id') + ->chunkById(1000, function (Collection $chunk) { // map and dispatch this chunk as a batch of jobs Bus::batch( $chunk->map( From ca1bdc351eafd2db833b1c64667424bf07e64dde Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 03:16:06 +0200 Subject: [PATCH 42/92] add name and page to batches --- app/Platform/Actions/UpdateGameMetrics.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php index 4f95825829..cf61f04775 100644 --- a/app/Platform/Actions/UpdateGameMetrics.php +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -11,8 +11,6 @@ use Illuminate\Bus\Batch; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Bus; -use Illuminate\Support\Facades\Log; -use Throwable; class UpdateGameMetrics { @@ -101,7 +99,7 @@ public function execute(Game $game): void ->orWhereNull('achievement_set_version_hash'); }) ->orderBy('id') - ->chunkById(1000, function (Collection $chunk) { + ->chunkById(1000, function (Collection $chunk, $page) use ($game) { // map and dispatch this chunk as a batch of jobs Bus::batch( $chunk->map( @@ -109,10 +107,7 @@ public function execute(Game $game): void ) ) ->onQueue('player-game-metrics-batch') - ->catch(function (Batch $batch, Throwable $e) { - // First batch job failure detected... - Log::error('Batch error: ' . $batch->id, ['exception' => $e]); - }) + ->name('player-game-metrics ' . $game->id . ' ' . $page) ->dispatch(); }); } From f52ef16984d2b7c565a72bffeda4071960f062fe Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 03:28:03 +0200 Subject: [PATCH 43/92] remove superfluous order by id when using chunkById --- app/Platform/Actions/UpdateGameMetrics.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php index cf61f04775..ee0b13399c 100644 --- a/app/Platform/Actions/UpdateGameMetrics.php +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -98,7 +98,6 @@ public function execute(Game $game): void $query->whereNot('achievement_set_version_hash', '=', $game->achievement_set_version_hash) ->orWhereNull('achievement_set_version_hash'); }) - ->orderBy('id') ->chunkById(1000, function (Collection $chunk, $page) use ($game) { // map and dispatch this chunk as a batch of jobs Bus::batch( From fcb7093d7889cd890f043dcfbd40159c57b36215 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 03:31:55 +0200 Subject: [PATCH 44/92] increase queud job timeout --- config/horizon.php | 2 +- config/queue.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/horizon.php b/config/horizon.php index 89ba0be8fe..d6b8285193 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -206,7 +206,7 @@ 'maxJobs' => 0, 'memory' => 128, 'tries' => 1, - 'timeout' => 240, // NOTE this should match queue config's retry_after + 'timeout' => 300, // NOTE timeout should always be at least several seconds shorter than the queue config's retry_after configuration value 'nice' => 0, ], ], diff --git a/config/queue.php b/config/queue.php index 7ac1871251..3b2f5f1185 100755 --- a/config/queue.php +++ b/config/queue.php @@ -63,7 +63,7 @@ 'driver' => 'redis', 'connection' => 'queue', 'queue' => env('REDIS_QUEUE', 'default'), - 'retry_after' => 240, // NOTE this should match horizon config's timeout + 'retry_after' => 305, // NOTE this should be longer than horizon config's timeout 'block_for' => null, ], From 7cd895609d3ba9392f6e6611bd8cfd16960c5cf9 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 03:41:31 +0200 Subject: [PATCH 45/92] Log achievement set version changes --- app/Platform/Actions/UpdateGameMetrics.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php index ee0b13399c..adf334a333 100644 --- a/app/Platform/Actions/UpdateGameMetrics.php +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -11,6 +11,7 @@ use Illuminate\Bus\Batch; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Bus; +use Illuminate\Support\Facades\Log; class UpdateGameMetrics { @@ -82,6 +83,8 @@ public function execute(Game $game): void return; } + Log::info('Achievement set version changed for ' . $game->id . '. Queueing all outdated player games.'); + // TODO dispatch events for achievement set and game metrics changes $tmp = $achievementsPublishedChange; $tmp = $pointsTotalChange; From 91cb74fc41c769a355c4f284ac928f6380bf2435 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Sat, 14 Oct 2023 08:56:03 -0400 Subject: [PATCH 46/92] feat: allow activating aggregate queries with a cookie (#1913) --- app/Http/Kernel.php | 1 + app/Site/Middleware/EncryptCookies.php | 1 + app/Site/Middleware/FeatureFlagMiddleware.php | 25 +++++++++++++++ resources/js/app.ts | 2 ++ resources/js/types/global.d.ts | 3 +- .../views/components/feature-flags.blade.php | 32 +++++++++++++++++++ 6 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 app/Site/Middleware/FeatureFlagMiddleware.php diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index d59b7f34fe..cde1f3f184 100755 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -22,6 +22,7 @@ class Kernel extends HttpKernel \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, \App\Site\Middleware\RedirectsMissingPages::class, \App\Site\Middleware\RobotsMiddleware::class, + \App\Site\Middleware\FeatureFlagMiddleware::class, ]; /** diff --git a/app/Site/Middleware/EncryptCookies.php b/app/Site/Middleware/EncryptCookies.php index f8e61c0f41..9cd140aa7c 100755 --- a/app/Site/Middleware/EncryptCookies.php +++ b/app/Site/Middleware/EncryptCookies.php @@ -18,5 +18,6 @@ class EncryptCookies extends Middleware 'prefers_hidden_user_completed_sets', 'prefers_seeing_saved_hidden_rows_when_reordering', 'progression_status_widths_preference', + 'feature_aggregate_queries', ]; } diff --git a/app/Site/Middleware/FeatureFlagMiddleware.php b/app/Site/Middleware/FeatureFlagMiddleware.php new file mode 100644 index 0000000000..751103e782 --- /dev/null +++ b/app/Site/Middleware/FeatureFlagMiddleware.php @@ -0,0 +1,25 @@ +cookie('feature_aggregate_queries'); + + // Override the feature flag configuration if the cookie is set to 'true'. + if ($cookieValue === 'true') { + config(['feature.aggregate_queries' => true]); + } elseif ($cookieValue === 'false') { + config(['feature.aggregate_queries' => false]); + } + + return $next($request); + } +} diff --git a/resources/js/app.ts b/resources/js/app.ts index 63b48144fb..aa6ba92c41 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -13,6 +13,7 @@ import { autoExpandTextInput, copyToClipboard, fetcher, + getCookie, getStringByteCount, handleLeaderboardTabClick, initializeTextareaCounter, @@ -36,6 +37,7 @@ lazyLoadModuleOnIdFound({ window.autoExpandTextInput = autoExpandTextInput; window.copyToClipboard = copyToClipboard; window.fetcher = fetcher; +window.getCookie = getCookie; window.getStringByteCount = getStringByteCount; window.handleLeaderboardTabClick = handleLeaderboardTabClick; window.initializeTextareaCounter = initializeTextareaCounter; diff --git a/resources/js/types/global.d.ts b/resources/js/types/global.d.ts index dff79d3cdc..074cc4c3f3 100644 --- a/resources/js/types/global.d.ts +++ b/resources/js/types/global.d.ts @@ -13,7 +13,7 @@ import type { handleLeaderboardTabClick as HandleLeaderboardTabClick } from '@/u import type { initializeTextareaCounter as InitializeTextareaCounter } from '@/utils/initializeTextareaCounter'; import type { injectShortcode as InjectShortcode } from '@/utils/injectShortcode'; import type { loadPostPreview as LoadPostPreview } from '@/utils/loadPostPreview'; -import type { setCookie as SetCookie } from '@/utils/cookie'; +import type { getCookie as GetCookie, setCookie as SetCookie } from '@/utils/cookie'; import type { toggleUserCompletedSetsVisibility as ToggleUserCompletedSetsVisibility } from '@/utils/toggleUserCompletedSetsVisibility'; declare global { @@ -24,6 +24,7 @@ declare global { var cfg: Record | undefined; var copyToClipboard: (text: string) => void; var fetcher: typeof Fetcher; + var getCookie: typeof GetCookie; var getStringByteCount: typeof GetStringByteCount; var handleLeaderboardTabClick: typeof HandleLeaderboardTabClick; var hideEarnedCheckboxComponent: typeof HideEarnedCheckboxComponent; diff --git a/resources/views/components/feature-flags.blade.php b/resources/views/components/feature-flags.blade.php index de9d9d005c..9fc9dd59c2 100644 --- a/resources/views/components/feature-flags.blade.php +++ b/resources/views/components/feature-flags.blade.php @@ -1,3 +1,21 @@ + + + +

    Beaten Games Player-facing UX

    @hasfeature("beat") @@ -6,3 +24,17 @@ Disabled @endhasfeature
    + +
    +

    Aggregate Queries

    + + + + @hasfeature("aggregate_queries") + Enabled (Source: {{ $aggregateQueriesSource }}) + @else + Disabled (Source: {{ $aggregateQueriesSource }}) + @endhasfeature +
    From d04e9864845d9482f8d5731ed376ff314d34df3b Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Sat, 14 Oct 2023 09:51:45 -0400 Subject: [PATCH 47/92] perf: temporarily disable calls to ScoreLeaderboardComponent() (#1914) --- public/achievementInfo.php | 3 ++- public/userInfo.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/public/achievementInfo.php b/public/achievementInfo.php index fcd8f08a4f..4e760a8ccc 100644 --- a/public/achievementInfo.php +++ b/public/achievementInfo.php @@ -467,7 +467,8 @@ function ResetProgress() {
    "; if ($user !== null && $user === $userPage) { - RenderScoreLeaderboardComponent($user, true); + // FIXME: https://discord.com/channels/476211979464343552/1026595325038833725/1162746245996093450 + // RenderScoreLeaderboardComponent($user, true); } ?> From 79abb6342ffaa9ab1de56b3245bbab21bf82067b Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 16:03:06 +0200 Subject: [PATCH 48/92] Move batched queues to their own supervisor --- config/horizon.php | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/config/horizon.php b/config/horizon.php index d6b8285193..8b1145ab53 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -197,11 +197,25 @@ 'player-game-metrics', 'game-metrics', 'developer-metrics', + ], + 'balance' => 'auto', + 'autoScalingStrategy' => 'time', + 'maxProcesses' => 10, + 'maxTime' => 0, + 'maxJobs' => 0, + 'memory' => 128, + 'tries' => 1, + 'timeout' => 300, // NOTE timeout should always be at least several seconds shorter than the queue config's retry_after configuration value + 'nice' => 0, + ], + 'supervisor-2' => [ + 'connection' => 'redis', + 'queue' => [ 'player-game-metrics-batch', ], 'balance' => 'auto', 'autoScalingStrategy' => 'time', - 'maxProcesses' => 1, + 'maxProcesses' => 15, 'maxTime' => 0, 'maxJobs' => 0, 'memory' => 128, @@ -214,7 +228,7 @@ 'environments' => [ 'production' => [ 'supervisor-1' => [ - 'maxProcesses' => 20, + 'maxProcesses' => 10, 'balanceMaxShift' => 1, 'balanceCooldown' => 3, ], @@ -222,7 +236,7 @@ 'stage' => [ 'supervisor-1' => [ - 'maxProcesses' => 20, + 'maxProcesses' => 10, 'balanceMaxShift' => 1, 'balanceCooldown' => 3, ], From 7519bd224eecfc9d6897a01eb7e93ca79df9ad51 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 21:46:47 +0200 Subject: [PATCH 49/92] increase supervisor-1 process count --- config/horizon.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/horizon.php b/config/horizon.php index 8b1145ab53..d673ba4650 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -200,7 +200,7 @@ ], 'balance' => 'auto', 'autoScalingStrategy' => 'time', - 'maxProcesses' => 10, + 'maxProcesses' => 1, 'maxTime' => 0, 'maxJobs' => 0, 'memory' => 128, @@ -228,7 +228,7 @@ 'environments' => [ 'production' => [ 'supervisor-1' => [ - 'maxProcesses' => 10, + 'maxProcesses' => 15, 'balanceMaxShift' => 1, 'balanceCooldown' => 3, ], @@ -236,7 +236,7 @@ 'stage' => [ 'supervisor-1' => [ - 'maxProcesses' => 10, + 'maxProcesses' => 15, 'balanceMaxShift' => 1, 'balanceCooldown' => 3, ], From e74be5b9e1c8c16edf920e934bb605cae9a03163 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 21:47:19 +0200 Subject: [PATCH 50/92] allow failures in job batches --- app/Platform/Actions/UpdateGameMetrics.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php index adf334a333..349b1bca3b 100644 --- a/app/Platform/Actions/UpdateGameMetrics.php +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -9,6 +9,7 @@ use App\Platform\Models\Game; use App\Platform\Models\PlayerGame; use Illuminate\Bus\Batch; +use Illuminate\Bus\BatchRepository; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Log; @@ -110,6 +111,13 @@ public function execute(Game $game): void ) ->onQueue('player-game-metrics-batch') ->name('player-game-metrics ' . $game->id . ' ' . $page) + ->allowFailures() + ->finally(function (Batch $batch) { + // mark batch as finished even if jobs failed + if (!$batch->finished()) { + resolve(BatchRepository::class)->markAsFinished($batch->id); + } + }) ->dispatch(); }); } From 0928bacfe39a4a0c957328e7b0d8ebf3256c69e5 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 23:43:33 +0200 Subject: [PATCH 51/92] fix job uniqueness --- .../Jobs/UpdateDeveloperContributionYieldJob.php | 7 +++++++ app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php | 9 ++++++++- app/Platform/Jobs/UpdateGameMetricsJob.php | 7 +++++++ app/Platform/Jobs/UpdatePlayerGameMetricsJob.php | 9 +++++++-- app/Platform/Jobs/UpdatePlayerMetricsJob.php | 7 +++++++ 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php b/app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php index afe8c73a90..405c877605 100644 --- a/app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php +++ b/app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php @@ -23,6 +23,13 @@ public function __construct( ) { } + public $uniqueFor = 3600; + + public function uniqueId(): string + { + return config('queue.default') === 'sync' ? '' : $this->userId; + } + public function handle(): void { app()->make(UpdateDeveloperContributionYield::class) diff --git a/app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php b/app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php index ce893927bb..2acdfe0612 100644 --- a/app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php +++ b/app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php @@ -5,11 +5,11 @@ use App\Platform\Actions\UpdateGameAchievementsMetrics; use App\Platform\Models\Game; use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use RectorPrefix202308\Illuminate\Contracts\Broadcasting\ShouldBeUnique; class UpdateGameAchievementsMetricsJob implements ShouldQueue, ShouldBeUnique { @@ -23,6 +23,13 @@ public function __construct( ) { } + public $uniqueFor = 3600; + + public function uniqueId(): string + { + return config('queue.default') === 'sync' ? '' : $this->gameId; + } + public function handle(): void { app()->make(UpdateGameAchievementsMetrics::class) diff --git a/app/Platform/Jobs/UpdateGameMetricsJob.php b/app/Platform/Jobs/UpdateGameMetricsJob.php index 241ccf6db3..0a80f27460 100644 --- a/app/Platform/Jobs/UpdateGameMetricsJob.php +++ b/app/Platform/Jobs/UpdateGameMetricsJob.php @@ -23,6 +23,13 @@ public function __construct( ) { } + public $uniqueFor = 3600; + + public function uniqueId(): string + { + return config('queue.default') === 'sync' ? '' : $this->gameId; + } + public function handle(): void { app()->make(UpdateGameMetrics::class) diff --git a/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php index 289077de13..d1c105d582 100644 --- a/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php +++ b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php @@ -3,9 +3,7 @@ namespace App\Platform\Jobs; use App\Platform\Actions\UpdatePlayerGameMetrics; -use App\Platform\Actions\UpdatePlayerMetrics; use App\Platform\Models\PlayerGame; -use App\Site\Models\User; use Illuminate\Bus\Batchable; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; @@ -28,6 +26,13 @@ public function __construct( ) { } + public $uniqueFor = 3600; + + public function uniqueId(): string + { + return config('queue.default') === 'sync' ? '' : $this->userId . '-' . $this->gameId; + } + public function handle(): void { if ($this->batch()?->cancelled()) { diff --git a/app/Platform/Jobs/UpdatePlayerMetricsJob.php b/app/Platform/Jobs/UpdatePlayerMetricsJob.php index 1dbe7763e8..f7b5ea6157 100644 --- a/app/Platform/Jobs/UpdatePlayerMetricsJob.php +++ b/app/Platform/Jobs/UpdatePlayerMetricsJob.php @@ -25,6 +25,13 @@ public function __construct( ) { } + public $uniqueFor = 3600; + + public function uniqueId(): string + { + return config('queue.default') === 'sync' ? '' : $this->userId; + } + public function handle(): void { if ($this->batch()?->cancelled()) { From 8ac05c863dc49106ac863768635be57681dc379f Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 23:54:24 +0200 Subject: [PATCH 52/92] add missing types --- app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php | 2 +- app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php | 2 +- app/Platform/Jobs/UpdateGameMetricsJob.php | 2 +- app/Platform/Jobs/UpdatePlayerGameMetricsJob.php | 2 +- app/Platform/Jobs/UpdatePlayerMetricsJob.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php b/app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php index 405c877605..91227338f0 100644 --- a/app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php +++ b/app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php @@ -23,7 +23,7 @@ public function __construct( ) { } - public $uniqueFor = 3600; + public int $uniqueFor = 3600; public function uniqueId(): string { diff --git a/app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php b/app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php index 2acdfe0612..2994cc0a66 100644 --- a/app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php +++ b/app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php @@ -23,7 +23,7 @@ public function __construct( ) { } - public $uniqueFor = 3600; + public int $uniqueFor = 3600; public function uniqueId(): string { diff --git a/app/Platform/Jobs/UpdateGameMetricsJob.php b/app/Platform/Jobs/UpdateGameMetricsJob.php index 0a80f27460..06fca7f214 100644 --- a/app/Platform/Jobs/UpdateGameMetricsJob.php +++ b/app/Platform/Jobs/UpdateGameMetricsJob.php @@ -23,7 +23,7 @@ public function __construct( ) { } - public $uniqueFor = 3600; + public int $uniqueFor = 3600; public function uniqueId(): string { diff --git a/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php index d1c105d582..9492a05f8f 100644 --- a/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php +++ b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php @@ -26,7 +26,7 @@ public function __construct( ) { } - public $uniqueFor = 3600; + public int $uniqueFor = 3600; public function uniqueId(): string { diff --git a/app/Platform/Jobs/UpdatePlayerMetricsJob.php b/app/Platform/Jobs/UpdatePlayerMetricsJob.php index f7b5ea6157..0a4175ee4b 100644 --- a/app/Platform/Jobs/UpdatePlayerMetricsJob.php +++ b/app/Platform/Jobs/UpdatePlayerMetricsJob.php @@ -25,7 +25,7 @@ public function __construct( ) { } - public $uniqueFor = 3600; + public int $uniqueFor = 3600; public function uniqueId(): string { From a7d909a21d123104e048a85a8fd8335402010766 Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Sat, 14 Oct 2023 16:11:02 -0600 Subject: [PATCH 53/92] allow aggregate query for followed users on game page (#1915) --- app/Helpers/database/user-relationship.php | 46 +++++++++++++--------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/app/Helpers/database/user-relationship.php b/app/Helpers/database/user-relationship.php index 82088841a2..b55412894f 100644 --- a/app/Helpers/database/user-relationship.php +++ b/app/Helpers/database/user-relationship.php @@ -116,25 +116,35 @@ function getAllFriendsProgress(string $user, int $gameID, ?array &$friendScoresO $friendSubquery = GetFriendsSubquery($user, false); // Less dependent subqueries :) - $query = "SELECT aw.User, ua.Motto, SUM( ach.Points ) AS TotalPoints, ua.RAPoints, ua.RichPresenceMsg, act.LastUpdate - FROM - ( - SELECT aw.User, aw.AchievementID, aw.Date - FROM Awarded AS aw - RIGHT JOIN + if (config('feature.aggregate_queries')) { + $query = "SELECT ua.User, ua.Motto, ua.RAPoints, ua.RichPresenceMsg, + pg.points_hardcore AS TotalPoints + FROM player_games pg + LEFT JOIN UserAccounts ua ON ua.ID = pg.user_id + WHERE ua.User IN ( $friendSubquery ) AND pg.game_id = $gameID + AND pg.points_hardcore > 0 + ORDER BY TotalPoints DESC, ua.User"; + } else { + $query = "SELECT aw.User, ua.Motto, SUM( ach.Points ) AS TotalPoints, ua.RAPoints, ua.RichPresenceMsg, act.LastUpdate + FROM ( - SELECT ID - FROM Achievements AS ach - WHERE ach.GameID = '$gameID' AND ach.Flags = 3 - ) AS Inner1 ON Inner1.ID = aw.AchievementID - WHERE aw.HardcoreMode = " . UnlockMode::Hardcore . " - ) AS aw - LEFT JOIN UserAccounts AS ua ON ua.User = aw.User - LEFT JOIN Achievements AS ach ON ach.ID = aw.AchievementID - LEFT JOIN Activity AS act ON act.ID = ua.LastActivityID - WHERE aw.User IN ( $friendSubquery ) - GROUP BY aw.User - ORDER BY TotalPoints DESC, aw.User"; + SELECT aw.User, aw.AchievementID, aw.Date + FROM Awarded AS aw + RIGHT JOIN + ( + SELECT ID + FROM Achievements AS ach + WHERE ach.GameID = '$gameID' AND ach.Flags = 3 + ) AS Inner1 ON Inner1.ID = aw.AchievementID + WHERE aw.HardcoreMode = " . UnlockMode::Hardcore . " + ) AS aw + LEFT JOIN UserAccounts AS ua ON ua.User = aw.User + LEFT JOIN Achievements AS ach ON ach.ID = aw.AchievementID + LEFT JOIN Activity AS act ON act.ID = ua.LastActivityID + WHERE aw.User IN ( $friendSubquery ) + GROUP BY aw.User + ORDER BY TotalPoints DESC, aw.User"; + } $dbResult = s_mysql_query($query); From 507d5c7a27b647d63604347ce8e277d081a783bc Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 15 Oct 2023 11:26:42 +0200 Subject: [PATCH 54/92] add player session listener jobs to their own queue --- app/Platform/Listeners/ResumePlayerSession.php | 2 ++ config/horizon.php | 1 + 2 files changed, 3 insertions(+) diff --git a/app/Platform/Listeners/ResumePlayerSession.php b/app/Platform/Listeners/ResumePlayerSession.php index c6984bd1f9..b42c9fc246 100644 --- a/app/Platform/Listeners/ResumePlayerSession.php +++ b/app/Platform/Listeners/ResumePlayerSession.php @@ -10,6 +10,8 @@ class ResumePlayerSession implements ShouldQueue { + public string $queue = 'player-sessions'; + public function handle(object $event): void { $user = null; diff --git a/config/horizon.php b/config/horizon.php index d673ba4650..1be3cd01bf 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -193,6 +193,7 @@ 'queue' => [ 'player-achievements', 'player-metrics', + 'player-sessions', 'default', 'player-game-metrics', 'game-metrics', From b3d02138dbe8392c0c2fe9690f023aa3d17f4fb8 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 15 Oct 2023 11:27:05 +0200 Subject: [PATCH 55/92] balance queues by size, not time --- config/horizon.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/horizon.php b/config/horizon.php index 1be3cd01bf..255057712b 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -200,7 +200,7 @@ 'developer-metrics', ], 'balance' => 'auto', - 'autoScalingStrategy' => 'time', + 'autoScalingStrategy' => 'size', 'maxProcesses' => 1, 'maxTime' => 0, 'maxJobs' => 0, From ca2b5546de2f931edfa1d0da77787649465bcd8a Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 15 Oct 2023 12:05:25 +0200 Subject: [PATCH 56/92] more workers for batch queue --- config/horizon.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/horizon.php b/config/horizon.php index 255057712b..3bd6e506b1 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -216,7 +216,7 @@ ], 'balance' => 'auto', 'autoScalingStrategy' => 'time', - 'maxProcesses' => 15, + 'maxProcesses' => 20, 'maxTime' => 0, 'maxJobs' => 0, 'memory' => 128, From 2e8cdbec8e9197b6b365c026322d6f2122a1abff Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 15 Oct 2023 12:31:45 +0200 Subject: [PATCH 57/92] update player game metrics after attaching --- app/Platform/EventServiceProvider.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Platform/EventServiceProvider.php b/app/Platform/EventServiceProvider.php index f3faaeb52f..0b8b02b5d1 100755 --- a/app/Platform/EventServiceProvider.php +++ b/app/Platform/EventServiceProvider.php @@ -14,6 +14,7 @@ use App\Platform\Events\PlayerAchievementUnlocked; use App\Platform\Events\PlayerBadgeAwarded; use App\Platform\Events\PlayerBadgeLost; +use App\Platform\Events\PlayerGameAttached; use App\Platform\Events\PlayerGameBeaten; use App\Platform\Events\PlayerGameCompleted; use App\Platform\Events\PlayerGameMetricsUpdated; @@ -67,6 +68,9 @@ class EventServiceProvider extends ServiceProvider PlayerBadgeLost::class => [ // TODO Notify player ], + PlayerGameAttached::class => [ + DispatchUpdatePlayerGameMetricsJob::class, // dispatches PlayerGameMetricsUpdated + ], PlayerGameBeaten::class => [ // TODO Refactor to AchievementSetBeaten // TODO Notify player @@ -84,7 +88,7 @@ class EventServiceProvider extends ServiceProvider PlayerMetricsUpdated::class => [ ], PlayerSessionHeartbeat::class => [ - ResumePlayerSession::class, + ResumePlayerSession::class, // dispatches PlayerGameAttached for new entries ], PlayerRankedStatusChanged::class => [ // TODO Update all affected games From d386cd6ab4eab05e66f6f04414f6f791114be43a Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 14 Oct 2023 23:45:25 +0200 Subject: [PATCH 58/92] add custom tags to jobs usually horizon detects eloquent models ion the constructor automatically and assigns them as tags. due to using just IDs in the constructors this has to be done manually. --- app/Platform/Jobs/UnlockPlayerAchievementJob.php | 12 ++++++++++++ .../Jobs/UpdateDeveloperContributionYieldJob.php | 10 ++++++++++ .../Jobs/UpdateGameAchievementsMetricsJob.php | 10 ++++++++++ app/Platform/Jobs/UpdateGameMetricsJob.php | 10 ++++++++++ app/Platform/Jobs/UpdatePlayerGameMetricsJob.php | 13 +++++++++++++ app/Platform/Jobs/UpdatePlayerMetricsJob.php | 10 ++++++++++ 6 files changed, 65 insertions(+) diff --git a/app/Platform/Jobs/UnlockPlayerAchievementJob.php b/app/Platform/Jobs/UnlockPlayerAchievementJob.php index 1f9bf0cca3..0c26a8f6e8 100644 --- a/app/Platform/Jobs/UnlockPlayerAchievementJob.php +++ b/app/Platform/Jobs/UnlockPlayerAchievementJob.php @@ -29,6 +29,18 @@ public function __construct( $this->timestamp ??= Carbon::now(); } + /** + * @return array + */ + public function tags(): array + { + return [ + User::class . ':' . $this->userId, + Achievement::class . ':' . $this->achievementId, + 'unlock:' . ($this->unlockedByUserId ? 'manual' : 'organic'), + ]; + } + public function handle(): void { app()->make(UnlockPlayerAchievement::class)->execute( diff --git a/app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php b/app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php index 91227338f0..14862c720f 100644 --- a/app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php +++ b/app/Platform/Jobs/UpdateDeveloperContributionYieldJob.php @@ -30,6 +30,16 @@ public function uniqueId(): string return config('queue.default') === 'sync' ? '' : $this->userId; } + /** + * @return array + */ + public function tags(): array + { + return [ + User::class . ':' . $this->userId, + ]; + } + public function handle(): void { app()->make(UpdateDeveloperContributionYield::class) diff --git a/app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php b/app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php index 2994cc0a66..a359dfd0c0 100644 --- a/app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php +++ b/app/Platform/Jobs/UpdateGameAchievementsMetricsJob.php @@ -30,6 +30,16 @@ public function uniqueId(): string return config('queue.default') === 'sync' ? '' : $this->gameId; } + /** + * @return array + */ + public function tags(): array + { + return [ + Game::class . ':' . $this->gameId, + ]; + } + public function handle(): void { app()->make(UpdateGameAchievementsMetrics::class) diff --git a/app/Platform/Jobs/UpdateGameMetricsJob.php b/app/Platform/Jobs/UpdateGameMetricsJob.php index 06fca7f214..0d266db90e 100644 --- a/app/Platform/Jobs/UpdateGameMetricsJob.php +++ b/app/Platform/Jobs/UpdateGameMetricsJob.php @@ -30,6 +30,16 @@ public function uniqueId(): string return config('queue.default') === 'sync' ? '' : $this->gameId; } + /** + * @return array + */ + public function tags(): array + { + return [ + Game::class . ':' . $this->gameId, + ]; + } + public function handle(): void { app()->make(UpdateGameMetrics::class) diff --git a/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php index 9492a05f8f..057c8f8a83 100644 --- a/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php +++ b/app/Platform/Jobs/UpdatePlayerGameMetricsJob.php @@ -3,7 +3,9 @@ namespace App\Platform\Jobs; use App\Platform\Actions\UpdatePlayerGameMetrics; +use App\Platform\Models\Game; use App\Platform\Models\PlayerGame; +use App\Site\Models\User; use Illuminate\Bus\Batchable; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; @@ -33,6 +35,17 @@ public function uniqueId(): string return config('queue.default') === 'sync' ? '' : $this->userId . '-' . $this->gameId; } + /** + * @return array + */ + public function tags(): array + { + return [ + User::class . ':' . $this->userId, + Game::class . ':' . $this->gameId, + ]; + } + public function handle(): void { if ($this->batch()?->cancelled()) { diff --git a/app/Platform/Jobs/UpdatePlayerMetricsJob.php b/app/Platform/Jobs/UpdatePlayerMetricsJob.php index 0a4175ee4b..7f6c2b4376 100644 --- a/app/Platform/Jobs/UpdatePlayerMetricsJob.php +++ b/app/Platform/Jobs/UpdatePlayerMetricsJob.php @@ -32,6 +32,16 @@ public function uniqueId(): string return config('queue.default') === 'sync' ? '' : $this->userId; } + /** + * @return array + */ + public function tags(): array + { + return [ + User::class . ':' . $this->userId, + ]; + } + public function handle(): void { if ($this->batch()?->cancelled()) { From ad50e98a35896ebb152603a0e81eef1fbfa00a71 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 15 Oct 2023 13:49:16 +0200 Subject: [PATCH 59/92] simplify player game attaching & prevent race condition errors --- app/Platform/Actions/AttachPlayerGame.php | 26 +++++++++++--------- app/Platform/Actions/ResumePlayerSession.php | 3 +-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/Platform/Actions/AttachPlayerGame.php b/app/Platform/Actions/AttachPlayerGame.php index 032d7b3256..b226920961 100644 --- a/app/Platform/Actions/AttachPlayerGame.php +++ b/app/Platform/Actions/AttachPlayerGame.php @@ -6,28 +6,30 @@ use App\Platform\Events\PlayerGameAttached; use App\Platform\Models\Game; +use App\Platform\Models\PlayerGame; use App\Site\Models\User; +use Exception; class AttachPlayerGame { - public function execute(User $user, Game $game): Game + public function execute(User $user, Game $game): PlayerGame { // upsert game attachment without running into unique constraints - /** @var ?Game $gameWithPivot */ - $gameWithPivot = $user->games()->find($game); - if ($gameWithPivot) { - return $gameWithPivot; + $playerGame = $user->playerGames()->firstWhere('game_id', $game->id); + if ($playerGame) { + return $playerGame; } - $user->games()->attach($game); + try { + $user->games()->attach($game); - /** @var Game $gameWithPivot */ - $gameWithPivot = $user->games()->find($game); - - // let everyone know that this user started this game for first time - PlayerGameAttached::dispatch($user, $gameWithPivot); + // let everyone know that this user started this game for first time + PlayerGameAttached::dispatch($user, $game); + } catch (Exception) { + // prevent race conditions where the game might've been attached by another job + } - return $gameWithPivot; + return $user->playerGames()->firstWhere('game_id', $game->id); } } diff --git a/app/Platform/Actions/ResumePlayerSession.php b/app/Platform/Actions/ResumePlayerSession.php index 3f361cd7a2..5d89f51169 100644 --- a/app/Platform/Actions/ResumePlayerSession.php +++ b/app/Platform/Actions/ResumePlayerSession.php @@ -23,8 +23,7 @@ public function execute( ): PlayerSession { // upsert player game and update last played date right away $attachPlayerGameAction = app()->make(AttachPlayerGame::class); - $game = $attachPlayerGameAction->execute($user, $game); - $playerGame = $game->pivot; + $playerGame = $attachPlayerGameAction->execute($user, $game); $playerGame->last_played_at = $timestamp; $playerGame->save(); From aac2a9f2a080621052d25db17003dcde56b28ed2 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 15 Oct 2023 13:56:26 +0200 Subject: [PATCH 60/92] use existing relation for palyer game lookup --- app/Platform/Actions/AttachPlayerGame.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Platform/Actions/AttachPlayerGame.php b/app/Platform/Actions/AttachPlayerGame.php index b226920961..36d680a015 100644 --- a/app/Platform/Actions/AttachPlayerGame.php +++ b/app/Platform/Actions/AttachPlayerGame.php @@ -16,7 +16,7 @@ public function execute(User $user, Game $game): PlayerGame { // upsert game attachment without running into unique constraints - $playerGame = $user->playerGames()->firstWhere('game_id', $game->id); + $playerGame = $user->playerGame($game); if ($playerGame) { return $playerGame; } @@ -30,6 +30,6 @@ public function execute(User $user, Game $game): PlayerGame // prevent race conditions where the game might've been attached by another job } - return $user->playerGames()->firstWhere('game_id', $game->id); + return $user->playerGame($game); } } From 8b8e129326da4a3ab3c3dc7dc0a57a0acf2fa024 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 15 Oct 2023 15:59:04 +0200 Subject: [PATCH 61/92] feat: add awarded badges at the end of the list (#1916) --- .../Actions/RevalidateAchievementSetBadgeEligibility.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/Platform/Actions/RevalidateAchievementSetBadgeEligibility.php b/app/Platform/Actions/RevalidateAchievementSetBadgeEligibility.php index 6efd60cded..fad0783f98 100644 --- a/app/Platform/Actions/RevalidateAchievementSetBadgeEligibility.php +++ b/app/Platform/Actions/RevalidateAchievementSetBadgeEligibility.php @@ -54,7 +54,6 @@ private function revalidateBeatenBadgeEligibility(PlayerGame $playerGame): void $playerGame->game->id, UnlockMode::Softcore, $playerGame->beaten_at, - displayOrder: 0 ); PlayerBadgeAwarded::dispatch($badge); PlayerGameBeaten::dispatch($playerGame->user, $playerGame->game); @@ -69,7 +68,6 @@ private function revalidateBeatenBadgeEligibility(PlayerGame $playerGame): void $playerGame->game->id, UnlockMode::Hardcore, $playerGame->beaten_hardcore_at, - displayOrder: 0 ); PlayerBadgeAwarded::dispatch($badge); PlayerGameBeaten::dispatch($playerGame->user, $playerGame->game, true); @@ -128,7 +126,6 @@ private function revalidateCompletionBadgeEligibility(PlayerGame $playerGame): v $playerGame->game->id, UnlockMode::Softcore, $playerGame->completed_at, - displayOrder: 0 ); PlayerBadgeAwarded::dispatch($badge); PlayerGameCompleted::dispatch($playerGame->user, $playerGame->game); @@ -154,7 +151,6 @@ private function revalidateCompletionBadgeEligibility(PlayerGame $playerGame): v $playerGame->game->id, UnlockMode::Hardcore, $playerGame->completed_hardcore_at, - displayOrder: 0 ); PlayerBadgeAwarded::dispatch($badge); PlayerGameCompleted::dispatch($playerGame->user, $playerGame->game, true); From f3e243d3f68ca3209d9ac78686c9689434ef811b Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 15 Oct 2023 16:49:01 +0200 Subject: [PATCH 62/92] feat: allow for user id or username command parameters (#1917) --- app/Platform/Commands/UnlockPlayerAchievement.php | 11 +++++++---- app/Platform/Commands/UpdatePlayerGameMetrics.php | 10 ++++++++-- app/Platform/Commands/UpdatePlayerMetrics.php | 12 +++++++++--- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/app/Platform/Commands/UnlockPlayerAchievement.php b/app/Platform/Commands/UnlockPlayerAchievement.php index 08db5cfe60..f7f2d42072 100644 --- a/app/Platform/Commands/UnlockPlayerAchievement.php +++ b/app/Platform/Commands/UnlockPlayerAchievement.php @@ -13,7 +13,7 @@ class UnlockPlayerAchievement extends Command { protected $signature = 'ra:platform:player:unlock-achievement - {username} + {userId : User ID or username. Usernames containing only numbers are ambiguous and must be referenced by user ID} {achievementIds : Comma-separated list of achievement IDs} {--hardcore}'; protected $description = 'Unlock achievement(s) for user'; @@ -29,16 +29,18 @@ public function __construct( */ public function handle(): void { - $username = $this->argument('username'); + $userId = $this->argument('userId'); $achievementIds = collect(explode(',', $this->argument('achievementIds'))) ->map(fn ($id) => (int) $id); $hardcore = (bool) $this->option('hardcore'); - $user = User::where('User', $this->argument('username'))->firstOrFail(); + $user = is_numeric($userId) + ? User::findOrFail($userId) + : User::where('User', $userId)->firstOrFail(); $achievements = Achievement::whereIn('id', $achievementIds)->get(); - $this->info('Unlocking ' . $achievements->count() . ' [' . ($hardcore ? 'hardcore' : 'softcore') . '] ' . __res('achievement', $achievements->count()) . ' for user [' . $username . '] [' . $user->id . ']'); + $this->info('Unlocking ' . $achievements->count() . ' [' . ($hardcore ? 'hardcore' : 'softcore') . '] ' . __res('achievement', $achievements->count()) . ' for user [' . $user->id . ':' . $user->username . ']'); $progressBar = $this->output->createProgressBar($achievements->count()); $progressBar->start(); @@ -53,5 +55,6 @@ public function handle(): void } $progressBar->finish(); + $this->line(PHP_EOL); } } diff --git a/app/Platform/Commands/UpdatePlayerGameMetrics.php b/app/Platform/Commands/UpdatePlayerGameMetrics.php index c3a8d24089..cfc7213000 100644 --- a/app/Platform/Commands/UpdatePlayerGameMetrics.php +++ b/app/Platform/Commands/UpdatePlayerGameMetrics.php @@ -13,7 +13,7 @@ class UpdatePlayerGameMetrics extends Command { protected $signature = 'ra:platform:player:update-game-metrics - {username} + {userId : User ID or username. Usernames containing only numbers are ambiguous and must be referenced by user ID} {gameIds? : Comma-separated list of game IDs. Leave empty to update all games in player library} {--outdated}'; protected $description = 'Update player game(s) metrics'; @@ -27,13 +27,16 @@ public function __construct( public function handle(): void { + $userId = $this->argument('userId'); $outdated = $this->option('outdated'); $gameIds = collect(explode(',', $this->argument('gameIds') ?? '')) ->filter() ->map(fn ($id) => (int) $id); - $user = User::where('User', $this->argument('username'))->firstOrFail(); + $user = is_numeric($userId) + ? User::findOrFail($userId) + : User::where('User', $userId)->firstOrFail(); $query = $user->playerGames() ->with(['user', 'game']); @@ -48,6 +51,8 @@ public function handle(): void } $playerGames = $query->get(); + $this->info('Updating ' . $playerGames->count() . ' ' . __res('game', $playerGames->count()) . ' for user [' . $user->id . ':' . $user->username . ']'); + $progressBar = $this->output->createProgressBar($playerGames->count()); $progressBar->start(); @@ -65,5 +70,6 @@ public function handle(): void } $progressBar->finish(); + $this->line(PHP_EOL); } } diff --git a/app/Platform/Commands/UpdatePlayerMetrics.php b/app/Platform/Commands/UpdatePlayerMetrics.php index 82d5fa0060..dbef8d880c 100644 --- a/app/Platform/Commands/UpdatePlayerMetrics.php +++ b/app/Platform/Commands/UpdatePlayerMetrics.php @@ -10,7 +10,8 @@ class UpdatePlayerMetrics extends Command { - protected $signature = 'ra:platform:player:update-metrics {username}'; + protected $signature = 'ra:platform:player:update-metrics + {userId : User ID or username. Usernames containing only numbers are ambiguous and must be referenced by user ID}'; protected $description = 'Update player metrics'; public function __construct( @@ -21,9 +22,14 @@ public function __construct( public function handle(): void { - $user = User::where('User', $this->argument('username'))->firstOrFail(); + $userId = $this->argument('userId'); + + $user = is_numeric($userId) + ? User::findOrFail($userId) + : User::where('User', $userId)->firstOrFail(); + + $this->info('Updating metrics for player [' . $user->id . ':' . $user->username . ']'); - $this->info('Update metrics for player ' . $user->username . ' [' . $user->id . ']'); $this->updatePlayerMetrics->execute($user); } } From 1b6973aa2cf118c29fe36be4b8876a1cb8e76a4a Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 15 Oct 2023 18:30:36 +0200 Subject: [PATCH 63/92] fix: PlayerGameAttached events not dispatching UpdatePlayerGameMetrics --- app/Platform/Actions/ResumePlayerSession.php | 4 ++-- app/Platform/Actions/UnlockPlayerAchievement.php | 4 ++-- .../Listeners/DispatchUpdatePlayerGameMetricsJob.php | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/Platform/Actions/ResumePlayerSession.php b/app/Platform/Actions/ResumePlayerSession.php index 5d89f51169..191ccd8a9a 100644 --- a/app/Platform/Actions/ResumePlayerSession.php +++ b/app/Platform/Actions/ResumePlayerSession.php @@ -22,8 +22,8 @@ public function execute( ?Carbon $timestamp = null, ): PlayerSession { // upsert player game and update last played date right away - $attachPlayerGameAction = app()->make(AttachPlayerGame::class); - $playerGame = $attachPlayerGameAction->execute($user, $game); + $playerGame = app()->make(AttachPlayerGame::class) + ->execute($user, $game); $playerGame->last_played_at = $timestamp; $playerGame->save(); diff --git a/app/Platform/Actions/UnlockPlayerAchievement.php b/app/Platform/Actions/UnlockPlayerAchievement.php index 860aa42408..51d77cc211 100644 --- a/app/Platform/Actions/UnlockPlayerAchievement.php +++ b/app/Platform/Actions/UnlockPlayerAchievement.php @@ -28,8 +28,8 @@ public function execute( if ($unlockedBy) { // only attach the game if it's a manual unlock - $attachPlayerGameAction = app()->make(AttachPlayerGame::class); - $attachPlayerGameAction->execute($user, $achievement->game); + app()->make(AttachPlayerGame::class) + ->execute($user, $achievement->game); } else { // make sure to resume the player session which will attach the game to the player, too $playerSession = app()->make(ResumePlayerSession::class) diff --git a/app/Platform/Listeners/DispatchUpdatePlayerGameMetricsJob.php b/app/Platform/Listeners/DispatchUpdatePlayerGameMetricsJob.php index 1a370ea67e..43f99f35a4 100644 --- a/app/Platform/Listeners/DispatchUpdatePlayerGameMetricsJob.php +++ b/app/Platform/Listeners/DispatchUpdatePlayerGameMetricsJob.php @@ -3,6 +3,7 @@ namespace App\Platform\Listeners; use App\Platform\Events\PlayerAchievementUnlocked; +use App\Platform\Events\PlayerGameAttached; use App\Platform\Jobs\UpdatePlayerGameMetricsJob; use App\Platform\Models\Game; use App\Site\Models\User; @@ -24,6 +25,10 @@ public function handle(object $event): void $game = $achievement->game; $hardcore = $event->hardcore; break; + case PlayerGameAttached::class: + $user = $event->user; + $game = $event->game; + break; } if (!$user instanceof User) { From c0fa53753c0c1a16ce9418c17af8c72f23af1233 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sun, 15 Oct 2023 18:35:56 +0200 Subject: [PATCH 64/92] fix: ResetPlayerProgressTest --- tests/Feature/Platform/Action/ResetPlayerProgressTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Feature/Platform/Action/ResetPlayerProgressTest.php b/tests/Feature/Platform/Action/ResetPlayerProgressTest.php index dcffac5859..f89d9b0b85 100755 --- a/tests/Feature/Platform/Action/ResetPlayerProgressTest.php +++ b/tests/Feature/Platform/Action/ResetPlayerProgressTest.php @@ -70,6 +70,7 @@ public function testResetHardcore(): void $achievement = Achievement::factory()->published()->create(['GameID' => $game->id, 'Points' => 5, 'TrueRatio' => 7, 'Author' => $author->User]); $this->addHardcoreUnlock($user, $achievement); + $achievement->refresh(); $this->assertHasSoftcoreUnlock($user, $achievement); $this->assertHasHardcoreUnlock($user, $achievement); @@ -108,6 +109,7 @@ public function testResetAuthoredAchievement(): void $achievement = Achievement::factory()->published()->create(['GameID' => $game->id, 'Points' => 5, 'TrueRatio' => 7, 'Author' => $user->User]); $this->addHardcoreUnlock($user, $achievement); + $achievement->refresh(); $this->assertHasSoftcoreUnlock($user, $achievement); $this->assertHasHardcoreUnlock($user, $achievement); From 682ece0dbf67098d87107a228d84c01967b02856 Mon Sep 17 00:00:00 2001 From: luchaos Date: Thu, 19 Oct 2023 01:33:50 +0200 Subject: [PATCH 65/92] fix: do not delete previously awarded badges for games with less than minimum published achievement counts --- .../Actions/RevalidateAchievementSetBadgeEligibility.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/Platform/Actions/RevalidateAchievementSetBadgeEligibility.php b/app/Platform/Actions/RevalidateAchievementSetBadgeEligibility.php index fad0783f98..2539da28b6 100644 --- a/app/Platform/Actions/RevalidateAchievementSetBadgeEligibility.php +++ b/app/Platform/Actions/RevalidateAchievementSetBadgeEligibility.php @@ -107,15 +107,6 @@ private function revalidateCompletionBadgeEligibility(PlayerGame $playerGame): v } if ($playerGame->achievements_total < PlayerBadge::MINIMUM_ACHIEVEMENTS_COUNT_FOR_MASTERY) { - if ($softcoreBadge->exists()) { - PlayerBadgeLost::dispatch($softcoreBadge->first()); - $softcoreBadge->delete(); - } - if ($hardcoreBadge->exists()) { - PlayerBadgeLost::dispatch($hardcoreBadge->first()); - $hardcoreBadge->delete(); - } - return; } From 7b88b318b53dbea23537ba1b32f38c68f8aa1ea2 Mon Sep 17 00:00:00 2001 From: luchaos Date: Thu, 19 Oct 2023 13:16:02 +0200 Subject: [PATCH 66/92] feat: move update for outdated player games to separate job, command and action --- app/Platform/Actions/UpdateGameMetrics.php | 46 +++-------------- .../UpdateOutdatedPlayerGameMetrics.php | 51 +++++++++++++++++++ .../UpdateOutdatedPlayerGameMetrics.php | 40 +++++++++++++++ .../UpdateOutdatedPlayerGameMetricsJob.php | 48 +++++++++++++++++ config/horizon.php | 13 ++--- config/queue.php | 2 +- 6 files changed, 152 insertions(+), 48 deletions(-) create mode 100644 app/Platform/Actions/UpdateOutdatedPlayerGameMetrics.php create mode 100644 app/Platform/Commands/UpdateOutdatedPlayerGameMetrics.php create mode 100644 app/Platform/Jobs/UpdateOutdatedPlayerGameMetricsJob.php diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php index 349b1bca3b..3037ee4aab 100644 --- a/app/Platform/Actions/UpdateGameMetrics.php +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -5,13 +5,8 @@ namespace App\Platform\Actions; use App\Platform\Events\GameMetricsUpdated; -use App\Platform\Jobs\UpdatePlayerGameMetricsJob; +use App\Platform\Jobs\UpdateOutdatedPlayerGameMetricsJob; use App\Platform\Models\Game; -use App\Platform\Models\PlayerGame; -use Illuminate\Bus\Batch; -use Illuminate\Bus\BatchRepository; -use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Log; class UpdateGameMetrics @@ -75,17 +70,13 @@ public function execute(Game $game): void app()->make(UpdateGameAchievementsMetrics::class) ->execute($game); + $game->refresh(); + $pointsWeightedChange = $game->TotalTruePoints - $pointsWeightedBeforeUpdate; GameMetricsUpdated::dispatch($game); - if (!$achievementSetVersionChanged) { - return; - } - - Log::info('Achievement set version changed for ' . $game->id . '. Queueing all outdated player games.'); - // TODO dispatch events for achievement set and game metrics changes $tmp = $achievementsPublishedChange; $tmp = $pointsTotalChange; @@ -93,33 +84,10 @@ public function execute(Game $game): void $tmp = $playersTotalChange; $tmp = $playersHardcoreChange; - // Ad-hoc updates for player games metrics and player metrics after achievement set version changes - // Note: this might dispatch multiple thousands of jobs depending on a game's players count - // add all affected player games to the update queue in batches - if (config('queue.default') !== 'sync') { - $game->playerGames() - ->where(function ($query) use ($game) { - $query->whereNot('achievement_set_version_hash', '=', $game->achievement_set_version_hash) - ->orWhereNull('achievement_set_version_hash'); - }) - ->chunkById(1000, function (Collection $chunk, $page) use ($game) { - // map and dispatch this chunk as a batch of jobs - Bus::batch( - $chunk->map( - fn (PlayerGame $playerGame) => new UpdatePlayerGameMetricsJob($playerGame->user_id, $playerGame->game_id) - ) - ) - ->onQueue('player-game-metrics-batch') - ->name('player-game-metrics ' . $game->id . ' ' . $page) - ->allowFailures() - ->finally(function (Batch $batch) { - // mark batch as finished even if jobs failed - if (!$batch->finished()) { - resolve(BatchRepository::class)->markAsFinished($batch->id); - } - }) - ->dispatch(); - }); + if ($achievementSetVersionChanged) { + Log::info('Achievement set version changed for ' . $game->id . '. Queueing all outdated player games.'); + dispatch(new UpdateOutdatedPlayerGameMetricsJob($game->id)) + ->onQueue('game-outdated-player-games'); } } } diff --git a/app/Platform/Actions/UpdateOutdatedPlayerGameMetrics.php b/app/Platform/Actions/UpdateOutdatedPlayerGameMetrics.php new file mode 100644 index 0000000000..2f19fed1c8 --- /dev/null +++ b/app/Platform/Actions/UpdateOutdatedPlayerGameMetrics.php @@ -0,0 +1,51 @@ +playerGames() + ->where(function ($query) use ($game) { + $query->whereNot('achievement_set_version_hash', '=', $game->achievement_set_version_hash) + ->orWhereNull('achievement_set_version_hash'); + }) + ->chunkById(1000, function (Collection $chunk, $page) use ($game) { + // map and dispatch this chunk as a batch of jobs + Bus::batch( + $chunk->map( + fn (PlayerGame $playerGame) => new UpdatePlayerGameMetricsJob($playerGame->user_id, $playerGame->game_id) + ) + ) + ->onQueue('player-game-metrics-batch') + ->name('player-game-metrics ' . $game->id . ' ' . $page) + ->allowFailures() + ->finally(function (Batch $batch) { + // mark batch as finished even if jobs failed + if (!$batch->finished()) { + resolve(BatchRepository::class)->markAsFinished($batch->id); + } + }) + ->dispatch(); + }); + } +} diff --git a/app/Platform/Commands/UpdateOutdatedPlayerGameMetrics.php b/app/Platform/Commands/UpdateOutdatedPlayerGameMetrics.php new file mode 100644 index 0000000000..367c375e2c --- /dev/null +++ b/app/Platform/Commands/UpdateOutdatedPlayerGameMetrics.php @@ -0,0 +1,40 @@ +argument('gameIds'))) + ->map(fn ($id) => (int) $id); + + $games = Game::whereIn('id', $gameIds)->get(); + + $progressBar = $this->output->createProgressBar($games->count()); + $progressBar->start(); + + foreach ($games as $game) { + $this->updateOutdatedPlayerGameMetrics->execute($game); + $progressBar->advance(); + } + + $progressBar->finish(); + } +} diff --git a/app/Platform/Jobs/UpdateOutdatedPlayerGameMetricsJob.php b/app/Platform/Jobs/UpdateOutdatedPlayerGameMetricsJob.php new file mode 100644 index 0000000000..bb20055aa1 --- /dev/null +++ b/app/Platform/Jobs/UpdateOutdatedPlayerGameMetricsJob.php @@ -0,0 +1,48 @@ +gameId; + } + + /** + * @return array + */ + public function tags(): array + { + return [ + Game::class . ':' . $this->gameId, + ]; + } + + public function handle(): void + { + app()->make(UpdateOutdatedPlayerGameMetrics::class) + ->execute(Game::findOrFail($this->gameId)); + } +} diff --git a/config/horizon.php b/config/horizon.php index 3bd6e506b1..86f5160cc2 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -201,7 +201,9 @@ ], 'balance' => 'auto', 'autoScalingStrategy' => 'size', - 'maxProcesses' => 1, + 'maxProcesses' => 15, + 'balanceMaxShift' => 1, + 'balanceCooldown' => 3, 'maxTime' => 0, 'maxJobs' => 0, 'memory' => 128, @@ -213,6 +215,7 @@ 'connection' => 'redis', 'queue' => [ 'player-game-metrics-batch', + 'game-outdated-player-games', ], 'balance' => 'auto', 'autoScalingStrategy' => 'time', @@ -221,7 +224,7 @@ 'maxJobs' => 0, 'memory' => 128, 'tries' => 1, - 'timeout' => 300, // NOTE timeout should always be at least several seconds shorter than the queue config's retry_after configuration value + 'timeout' => 600, // NOTE timeout should always be at least several seconds shorter than the queue config's retry_after configuration value 'nice' => 0, ], ], @@ -229,17 +232,11 @@ 'environments' => [ 'production' => [ 'supervisor-1' => [ - 'maxProcesses' => 15, - 'balanceMaxShift' => 1, - 'balanceCooldown' => 3, ], ], 'stage' => [ 'supervisor-1' => [ - 'maxProcesses' => 15, - 'balanceMaxShift' => 1, - 'balanceCooldown' => 3, ], ], diff --git a/config/queue.php b/config/queue.php index 3b2f5f1185..089e9c1582 100755 --- a/config/queue.php +++ b/config/queue.php @@ -63,7 +63,7 @@ 'driver' => 'redis', 'connection' => 'queue', 'queue' => env('REDIS_QUEUE', 'default'), - 'retry_after' => 305, // NOTE this should be longer than horizon config's timeout + 'retry_after' => 3600, // NOTE this should be longer than horizon config's timeout - setting very high to not run into it 'block_for' => null, ], From 5a2fb59452a6a26768d2f4fd9862e778f30b6e94 Mon Sep 17 00:00:00 2001 From: luchaos Date: Thu, 19 Oct 2023 13:23:52 +0200 Subject: [PATCH 67/92] fix: update game player games naming --- app/Platform/Actions/UpdateGameMetrics.php | 4 ++-- ...datedPlayerGameMetrics.php => UpdateGamePlayerGames.php} | 2 +- app/Platform/AppServiceProvider.php | 2 ++ ...datedPlayerGameMetrics.php => UpdateGamePlayerGames.php} | 6 +++--- ...layerGameMetricsJob.php => UpdateGamePlayerGamesJob.php} | 6 +++--- 5 files changed, 11 insertions(+), 9 deletions(-) rename app/Platform/Actions/{UpdateOutdatedPlayerGameMetrics.php => UpdateGamePlayerGames.php} (98%) rename app/Platform/Commands/{UpdateOutdatedPlayerGameMetrics.php => UpdateGamePlayerGames.php} (80%) rename app/Platform/Jobs/{UpdateOutdatedPlayerGameMetricsJob.php => UpdateGamePlayerGamesJob.php} (81%) diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php index 3037ee4aab..7683c45ecf 100644 --- a/app/Platform/Actions/UpdateGameMetrics.php +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -5,7 +5,7 @@ namespace App\Platform\Actions; use App\Platform\Events\GameMetricsUpdated; -use App\Platform\Jobs\UpdateOutdatedPlayerGameMetricsJob; +use App\Platform\Jobs\UpdateGamePlayerGamesJob; use App\Platform\Models\Game; use Illuminate\Support\Facades\Log; @@ -86,7 +86,7 @@ public function execute(Game $game): void if ($achievementSetVersionChanged) { Log::info('Achievement set version changed for ' . $game->id . '. Queueing all outdated player games.'); - dispatch(new UpdateOutdatedPlayerGameMetricsJob($game->id)) + dispatch(new UpdateGamePlayerGamesJob($game->id)) ->onQueue('game-outdated-player-games'); } } diff --git a/app/Platform/Actions/UpdateOutdatedPlayerGameMetrics.php b/app/Platform/Actions/UpdateGamePlayerGames.php similarity index 98% rename from app/Platform/Actions/UpdateOutdatedPlayerGameMetrics.php rename to app/Platform/Actions/UpdateGamePlayerGames.php index 2f19fed1c8..7118de9732 100644 --- a/app/Platform/Actions/UpdateOutdatedPlayerGameMetrics.php +++ b/app/Platform/Actions/UpdateGamePlayerGames.php @@ -12,7 +12,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Bus; -class UpdateOutdatedPlayerGameMetrics +class UpdateGamePlayerGames { public function execute(Game $game): void { diff --git a/app/Platform/AppServiceProvider.php b/app/Platform/AppServiceProvider.php index 931269ad43..ad5b7cfcf5 100644 --- a/app/Platform/AppServiceProvider.php +++ b/app/Platform/AppServiceProvider.php @@ -23,6 +23,7 @@ use App\Platform\Commands\UpdateDeveloperContributionYield; use App\Platform\Commands\UpdateGameAchievementsMetrics; use App\Platform\Commands\UpdateGameMetrics; +use App\Platform\Commands\UpdateGamePlayerGames; use App\Platform\Commands\UpdateLeaderboardMetrics; use App\Platform\Commands\UpdatePlayerGameMetrics; use App\Platform\Commands\UpdatePlayerMetrics; @@ -62,6 +63,7 @@ public function boot(): void // Games UpdateGameMetrics::class, UpdateGameAchievementsMetrics::class, + UpdateGamePlayerGames::class, // Game Hashes NoIntroImport::class, diff --git a/app/Platform/Commands/UpdateOutdatedPlayerGameMetrics.php b/app/Platform/Commands/UpdateGamePlayerGames.php similarity index 80% rename from app/Platform/Commands/UpdateOutdatedPlayerGameMetrics.php rename to app/Platform/Commands/UpdateGamePlayerGames.php index 367c375e2c..931dfa9ee0 100644 --- a/app/Platform/Commands/UpdateOutdatedPlayerGameMetrics.php +++ b/app/Platform/Commands/UpdateGamePlayerGames.php @@ -4,13 +4,13 @@ namespace App\Platform\Commands; -use App\Platform\Actions\UpdateOutdatedPlayerGameMetrics as UpdateOutdatedPlayerGameMetricsAction; +use App\Platform\Actions\UpdateGamePlayerGames as UpdateOutdatedPlayerGameMetricsAction; use App\Platform\Models\Game; use Illuminate\Console\Command; -class UpdateOutdatedPlayerGameMetrics extends Command +class UpdateGamePlayerGames extends Command { - protected $signature = 'ra:platform:game:update-outdated-player-metrics + protected $signature = 'ra:platform:game:update-player-games {gameIds : Comma-separated list of game IDs}'; protected $description = "Update game(s) outdated player game metrics"; diff --git a/app/Platform/Jobs/UpdateOutdatedPlayerGameMetricsJob.php b/app/Platform/Jobs/UpdateGamePlayerGamesJob.php similarity index 81% rename from app/Platform/Jobs/UpdateOutdatedPlayerGameMetricsJob.php rename to app/Platform/Jobs/UpdateGamePlayerGamesJob.php index bb20055aa1..f8663a9492 100644 --- a/app/Platform/Jobs/UpdateOutdatedPlayerGameMetricsJob.php +++ b/app/Platform/Jobs/UpdateGamePlayerGamesJob.php @@ -2,7 +2,7 @@ namespace App\Platform\Jobs; -use App\Platform\Actions\UpdateOutdatedPlayerGameMetrics; +use App\Platform\Actions\UpdateGamePlayerGames; use App\Platform\Models\Game; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; @@ -11,7 +11,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class UpdateOutdatedPlayerGameMetricsJob implements ShouldQueue, ShouldBeUniqueUntilProcessing +class UpdateGamePlayerGamesJob implements ShouldQueue, ShouldBeUniqueUntilProcessing { use Dispatchable; use InteractsWithQueue; @@ -42,7 +42,7 @@ public function tags(): array public function handle(): void { - app()->make(UpdateOutdatedPlayerGameMetrics::class) + app()->make(UpdateGamePlayerGames::class) ->execute(Game::findOrFail($this->gameId)); } } From 6bcb7468ef45f31e338563fe158a8b61aad4f914 Mon Sep 17 00:00:00 2001 From: luchaos Date: Thu, 19 Oct 2023 13:26:58 +0200 Subject: [PATCH 68/92] fix: queue name --- app/Platform/Actions/UpdateGameMetrics.php | 2 +- config/horizon.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Platform/Actions/UpdateGameMetrics.php b/app/Platform/Actions/UpdateGameMetrics.php index 7683c45ecf..ab4155372c 100644 --- a/app/Platform/Actions/UpdateGameMetrics.php +++ b/app/Platform/Actions/UpdateGameMetrics.php @@ -87,7 +87,7 @@ public function execute(Game $game): void if ($achievementSetVersionChanged) { Log::info('Achievement set version changed for ' . $game->id . '. Queueing all outdated player games.'); dispatch(new UpdateGamePlayerGamesJob($game->id)) - ->onQueue('game-outdated-player-games'); + ->onQueue('game-player-games'); } } } diff --git a/config/horizon.php b/config/horizon.php index 86f5160cc2..47e02ac853 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -215,7 +215,7 @@ 'connection' => 'redis', 'queue' => [ 'player-game-metrics-batch', - 'game-outdated-player-games', + 'game-player-games', ], 'balance' => 'auto', 'autoScalingStrategy' => 'time', From 0087e6c17a55018ae8ae52f5fb25fd06834c0beb Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Sat, 21 Oct 2023 04:39:50 -0600 Subject: [PATCH 69/92] use aggregate_queries feature for high scores on game page (#1918) * use aggregate_queries feature for high scores on game page * address feedback * use INNER JOIN --- app/Helpers/database/player-game.php | 34 ++++++++++++++++++---------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/app/Helpers/database/player-game.php b/app/Helpers/database/player-game.php index 2054c8b19e..11976ea63d 100644 --- a/app/Helpers/database/player-game.php +++ b/app/Helpers/database/player-game.php @@ -747,18 +747,28 @@ function getGameTopAchievers(int $gameID): array $numAchievementsInSet = $data['NumAchievementsInSet']; } - // TODO config('feature.aggregate_queries') slow query (17) - $query = "SELECT aw.User, COUNT(*) AS NumAchievements, SUM(ach.points) AS TotalScore, MAX(aw.Date) AS LastAward - FROM Awarded AS aw - LEFT JOIN Achievements AS ach ON ach.ID = aw.AchievementID - LEFT JOIN GameData AS gd ON gd.ID = ach.GameID - LEFT JOIN UserAccounts AS ua ON ua.User = aw.User - WHERE NOT ua.Untracked - AND ach.Flags = " . AchievementFlag::OfficialCore . " - AND gd.ID = $gameID - AND aw.HardcoreMode = " . UnlockMode::Hardcore . " - GROUP BY aw.User - ORDER BY TotalScore DESC, NumAchievements DESC, LastAward"; + if (config('feature.aggregate_queries')) { + $query = "SELECT ua.User, pg.achievements_unlocked_hardcore AS NumAchievements, + pg.points_hardcore AS TotalScore, pg.last_unlock_hardcore_at AS LastAward + FROM player_games pg + INNER JOIN UserAccounts ua ON ua.ID = pg.user_id + WHERE ua.Untracked = 0 + AND pg.game_id = $gameID + AND pg.achievements_unlocked_hardcore > 0 + ORDER BY TotalScore DESC, NumAchievements DESC, LastAward"; + } else { + $query = "SELECT aw.User, COUNT(*) AS NumAchievements, SUM(ach.points) AS TotalScore, MAX(aw.Date) AS LastAward + FROM Awarded AS aw + LEFT JOIN Achievements AS ach ON ach.ID = aw.AchievementID + LEFT JOIN GameData AS gd ON gd.ID = ach.GameID + LEFT JOIN UserAccounts AS ua ON ua.User = aw.User + WHERE NOT ua.Untracked + AND ach.Flags = " . AchievementFlag::OfficialCore . " + AND gd.ID = $gameID + AND aw.HardcoreMode = " . UnlockMode::Hardcore . " + GROUP BY aw.User + ORDER BY TotalScore DESC, NumAchievements DESC, LastAward"; + } $mastersCounter = 0; foreach (legacyDbFetchAll($query) as $data) { From ed1016e51b640b2eae3e2af353cd123c4a363cf9 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 21 Oct 2023 12:42:55 +0200 Subject: [PATCH 70/92] chore: add PHP_EOL to commands --- app/Platform/Commands/UpdateDeveloperContributionYield.php | 1 + app/Platform/Commands/UpdateGameAchievementsMetrics.php | 1 + app/Platform/Commands/UpdateGameMetrics.php | 1 + app/Platform/Commands/UpdateGamePlayerGames.php | 1 + 4 files changed, 4 insertions(+) diff --git a/app/Platform/Commands/UpdateDeveloperContributionYield.php b/app/Platform/Commands/UpdateDeveloperContributionYield.php index ecd8726a54..d94ed82e8a 100644 --- a/app/Platform/Commands/UpdateDeveloperContributionYield.php +++ b/app/Platform/Commands/UpdateDeveloperContributionYield.php @@ -43,5 +43,6 @@ public function handle(): void } $progressBar->finish(); + $this->line(PHP_EOL); } } diff --git a/app/Platform/Commands/UpdateGameAchievementsMetrics.php b/app/Platform/Commands/UpdateGameAchievementsMetrics.php index 44be564b1c..b1d6ee9c97 100644 --- a/app/Platform/Commands/UpdateGameAchievementsMetrics.php +++ b/app/Platform/Commands/UpdateGameAchievementsMetrics.php @@ -36,5 +36,6 @@ public function handle(): void } $progressBar->finish(); + $this->line(PHP_EOL); } } diff --git a/app/Platform/Commands/UpdateGameMetrics.php b/app/Platform/Commands/UpdateGameMetrics.php index de68f6b12d..5fc22340b9 100644 --- a/app/Platform/Commands/UpdateGameMetrics.php +++ b/app/Platform/Commands/UpdateGameMetrics.php @@ -36,5 +36,6 @@ public function handle(): void } $progressBar->finish(); + $this->line(PHP_EOL); } } diff --git a/app/Platform/Commands/UpdateGamePlayerGames.php b/app/Platform/Commands/UpdateGamePlayerGames.php index 931dfa9ee0..1fd303d0e4 100644 --- a/app/Platform/Commands/UpdateGamePlayerGames.php +++ b/app/Platform/Commands/UpdateGamePlayerGames.php @@ -36,5 +36,6 @@ public function handle(): void } $progressBar->finish(); + $this->line(PHP_EOL); } } From 2d2bd81b44922d60dbf0d639f4dd42e4d2dd8f5f Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Sat, 21 Oct 2023 08:48:48 -0600 Subject: [PATCH 71/92] fix client error retrieving softcore unlocks with aggregate queries on (#1919) --- app/Helpers/database/player-game.php | 17 ++++++++++++----- tests/Feature/Connect/AwardAchievementTest.php | 6 +++--- tests/Feature/Connect/BootstrapsConnect.php | 2 ++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/Helpers/database/player-game.php b/app/Helpers/database/player-game.php index 11976ea63d..a8617fba81 100644 --- a/app/Helpers/database/player-game.php +++ b/app/Helpers/database/player-game.php @@ -322,11 +322,18 @@ function getUserAchievementUnlocksForGame(string $username, int $gameID, int $fl 'unlocked_hardcore_at', ]) ->mapWithKeys(function (PlayerAchievement $unlock, int $key) { - return [$unlock->achievement_id => [ - // TODO move this transformation to where it's needed (web api) and use models everywhere else - 'DateEarned' => $unlock->unlocked_at?->format('Y-m-d H:i:s'), - 'DateEarnedHardcore' => $unlock->unlocked_hardcore_at?->format('Y-m-d H:i:s'), - ]]; + $result = []; + + // TODO move this transformation to where it's needed (web api) and use models everywhere else + if ($unlock->unlocked_at) { + $result['DateEarned'] = $unlock->unlocked_at->format('Y-m-d H:i:s'); + } + + if ($unlock->unlocked_hardcore_at) { + $result['DateEarnedHardcore'] = $unlock->unlocked_hardcore_at->format('Y-m-d H:i:s'); + } + + return [$unlock->achievement_id => $result]; }); return $playerAchievements->toArray(); diff --git a/tests/Feature/Connect/AwardAchievementTest.php b/tests/Feature/Connect/AwardAchievementTest.php index 21d5c38937..5d1a8694fb 100644 --- a/tests/Feature/Connect/AwardAchievementTest.php +++ b/tests/Feature/Connect/AwardAchievementTest.php @@ -125,7 +125,7 @@ public function testHardcoreUnlock(): void // make sure the unlock cache was updated $unlocks = getUserAchievementUnlocksForGame($this->user->User, $game->ID); - $this->assertEquals([$achievement1->ID, $achievement5->ID, $achievement6->ID, $achievement3->ID], array_keys($unlocks)); + $this->assertEqualsCanonicalizing([$achievement1->ID, $achievement5->ID, $achievement6->ID, $achievement3->ID], array_keys($unlocks)); $this->assertEquals($now, $unlocks[$achievement3->ID]['DateEarnedHardcore']); $this->assertEquals($now, $unlocks[$achievement3->ID]['DateEarned']); @@ -296,7 +296,7 @@ public function testSoftcoreUnlockPromotedToHardcore(): void // make sure the unlock cache was updated $unlocks = getUserAchievementUnlocksForGame($this->user->User, $game->ID); - $this->assertEquals([$achievement1->ID, $achievement5->ID, $achievement6->ID, $achievement3->ID], array_keys($unlocks)); + $this->assertEqualsCanonicalizing([$achievement1->ID, $achievement5->ID, $achievement6->ID, $achievement3->ID], array_keys($unlocks)); $this->assertEquals($now, $unlocks[$achievement3->ID]['DateEarned']); $this->assertArrayNotHasKey('DateEarnedHardcore', $unlocks[$achievement3->ID]); @@ -366,7 +366,7 @@ public function testSoftcoreUnlockPromotedToHardcore(): void // make sure the unlock cache was updated $unlocks = getUserAchievementUnlocksForGame($this->user->User, $game->ID); - $this->assertEquals([$achievement1->ID, $achievement5->ID, $achievement6->ID, $achievement3->ID], array_keys($unlocks)); + $this->assertEqualsCanonicalizing([$achievement1->ID, $achievement5->ID, $achievement6->ID, $achievement3->ID], array_keys($unlocks)); $this->assertEquals($now, $unlocks[$achievement3->ID]['DateEarned']); $this->assertEquals($newNow, $unlocks[$achievement3->ID]['DateEarnedHardcore']); diff --git a/tests/Feature/Connect/BootstrapsConnect.php b/tests/Feature/Connect/BootstrapsConnect.php index f8f275fb86..d16a1b0cb0 100644 --- a/tests/Feature/Connect/BootstrapsConnect.php +++ b/tests/Feature/Connect/BootstrapsConnect.php @@ -15,6 +15,8 @@ protected function setUp(): void { parent::setUp(); + // config(['feature.aggregate_queries' => true]); + /** @var User $user */ $user = User::factory()->create(['appToken' => Str::random(16)]); $this->user = $user; From b4e1dc15a74627b6dbf83e5d9e3e8d186cb006c1 Mon Sep 17 00:00:00 2001 From: luchaos Date: Sat, 21 Oct 2023 17:22:49 +0200 Subject: [PATCH 72/92] fix(connect): do not record ping requests without a game ID (#1920) --- public/dorequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/dorequest.php b/public/dorequest.php index 4040a32b09..94088a3fd8 100644 --- a/public/dorequest.php +++ b/public/dorequest.php @@ -230,7 +230,7 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) */ case "ping": - if ($user === null) { + if ($user === null || !$gameID) { $response['Success'] = false; } else { $activityMessage = request()->post('m'); From 3b4b6e36419ecbf322960a79b34b78a00fe14877 Mon Sep 17 00:00:00 2001 From: luchaos Date: Mon, 23 Oct 2023 20:48:34 +0200 Subject: [PATCH 73/92] fix: achievement promote/demote do not dispatch events --- app/Helpers/database/achievement.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Helpers/database/achievement.php b/app/Helpers/database/achievement.php index 56082d5369..efb5d8f900 100644 --- a/app/Helpers/database/achievement.php +++ b/app/Helpers/database/achievement.php @@ -495,7 +495,7 @@ function updateAchievementFlag(int|string|array $achID, int $newFlag): bool { $achievementIDs = is_array($achID) ? implode(', ', $achID) : $achID; - sanitize_sql_inputs($achievementIDs, $newFlag); + sanitize_sql_inputs($achievementIDs); $query = "SELECT ID, Author, Points FROM Achievements WHERE ID IN ($achievementIDs) AND Flags != $newFlag"; $dbResult = s_mysql_query($query); From 9098e67605cf3d7f344e8cba26e66f9c60da76c6 Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Mon, 23 Oct 2023 12:59:42 -0600 Subject: [PATCH 74/92] fix error viewing unofficial achievements for game with soft-deleted achievements (#1921) --- app/Helpers/database/game.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Helpers/database/game.php b/app/Helpers/database/game.php index b96ea0fdc4..2127dfef45 100644 --- a/app/Helpers/database/game.php +++ b/app/Helpers/database/game.php @@ -206,7 +206,7 @@ function getGameMetadata( ach.type FROM Achievements AS ach $metricsJoin - WHERE ach.GameID = :gameId AND ach.Flags = :achievementFlag + WHERE ach.GameID = :gameId AND ach.Flags = :achievementFlag AND ach.deleted_at IS NULL $orderBy"; $achievementDataOut = legacyDbFetchAll($query, array_merge([ From 1cf8dc5291a54332e1f40788a100c646dd7d3c0d Mon Sep 17 00:00:00 2001 From: luchaos Date: Mon, 23 Oct 2023 22:15:08 +0200 Subject: [PATCH 75/92] chore: do not sanitize integers (#1922) --- app/Helpers/database/code-note.php | 4 --- app/Helpers/database/forum.php | 6 +--- app/Helpers/database/game.php | 40 ++++++++++++-------------- app/Helpers/database/message.php | 6 ++-- app/Helpers/database/set-claim.php | 4 +-- app/Helpers/database/set-request.php | 5 ---- app/Helpers/database/user-activity.php | 6 ---- app/Helpers/database/user.php | 6 ++-- 8 files changed, 26 insertions(+), 51 deletions(-) diff --git a/app/Helpers/database/code-note.php b/app/Helpers/database/code-note.php index 10977166fb..36a09fb7f7 100644 --- a/app/Helpers/database/code-note.php +++ b/app/Helpers/database/code-note.php @@ -8,8 +8,6 @@ function getCodeNotesData(int $gameID): array { $codeNotesOut = []; - sanitize_sql_inputs($gameID); - $query = "SELECT ua.User, cn.Address, cn.Note FROM CodeNotes AS cn LEFT JOIN UserAccounts AS ua ON ua.ID = cn.AuthorID @@ -32,8 +30,6 @@ function getCodeNotesData(int $gameID): array function getCodeNotes(int $gameID, ?array &$codeNotesOut): bool { - sanitize_sql_inputs($gameID); - $query = "SELECT ua.User, cn.Address, cn.Note FROM CodeNotes AS cn LEFT JOIN UserAccounts AS ua ON ua.ID = cn.AuthorID diff --git a/app/Helpers/database/forum.php b/app/Helpers/database/forum.php index 0145f6a9d5..63bc0cc6fe 100644 --- a/app/Helpers/database/forum.php +++ b/app/Helpers/database/forum.php @@ -136,8 +136,6 @@ function getTopicDetails(int $topicID, ?array &$topicDataOut = []): bool function getTopicComments(int $topicID, int $offset, int $count, ?int &$maxCountOut): ?array { - sanitize_sql_inputs($topicID); - $query = " SELECT COUNT(*) FROM ForumTopicComment AS ftc WHERE ftc.ForumTopicID = $topicID "; @@ -349,7 +347,7 @@ function submitTopicComment( function notifyUsersAboutForumActivity(int $topicID, string $topicTitle, string $author, int $commentID): void { - sanitize_sql_inputs($topicID, $author, $commentID); + sanitize_sql_inputs($author); // $author has made a post in the topic $topicID // Find all people involved in this forum topic, and if they are not the author and prefer to @@ -383,8 +381,6 @@ function getTopicCommentCommentOffset(int $forumTopicID, int $commentID, int $co $commentID = 99_999_999; } - sanitize_sql_inputs($forumTopicID, $commentID); - $query = "SELECT COUNT(ID) AS CommentOffset FROM ForumTopicComment WHERE DateCreated < (SELECT DateCreated FROM ForumTopicComment WHERE ID = $commentID) diff --git a/app/Helpers/database/game.php b/app/Helpers/database/game.php index 2127dfef45..71d625b828 100644 --- a/app/Helpers/database/game.php +++ b/app/Helpers/database/game.php @@ -69,24 +69,24 @@ function getGameMetadata( 5 => "ORDER BY ach.Title, ach.ID ASC ", 15 => "ORDER BY ach.Title DESC, ach.ID DESC ", - 6 => "ORDER BY - CASE - WHEN ach.type = 'progression' THEN 0 - WHEN ach.type = 'win_condition' THEN 1 - WHEN ach.type IS NULL THEN 2 - ELSE 3 - END, - ach.DisplayOrder, + 6 => "ORDER BY + CASE + WHEN ach.type = 'progression' THEN 0 + WHEN ach.type = 'win_condition' THEN 1 + WHEN ach.type IS NULL THEN 2 + ELSE 3 + END, + ach.DisplayOrder, ach.ID ASC ", - 16 => "ORDER BY - CASE - WHEN ach.type = 'progression' THEN 0 - WHEN ach.type = 'win_condition' THEN 1 - WHEN ach.type IS NULL THEN 2 - ELSE 3 - END DESC, - ach.DisplayOrder DESC, + 16 => "ORDER BY + CASE + WHEN ach.type = 'progression' THEN 0 + WHEN ach.type = 'win_condition' THEN 1 + WHEN ach.type IS NULL THEN 2 + ELSE 3 + END DESC, + ach.DisplayOrder DESC, ach.ID DESC ", // 1 @@ -576,7 +576,7 @@ function modifyGameData( return true; } - sanitize_sql_inputs($gameID, $developer, $publisher, $genre, $released, $guideURL); + sanitize_sql_inputs($developer, $publisher, $genre, $released, $guideURL); $query = "UPDATE GameData AS gd SET gd.Developer = '$developer', gd.Publisher = '$publisher', gd.Genre = '$genre', gd.Released = '$released', gd.GuideURL = '$guideURL' @@ -681,8 +681,6 @@ function modifyGameAlternatives(string $user, int $gameID, int|string|null $toAd function modifyGameForumTopic(string $user, int $gameID, int $newForumTopic): bool { - sanitize_sql_inputs($gameID, $newForumTopic); - if ($gameID == 0 || $newForumTopic == 0) { return false; } @@ -740,7 +738,7 @@ function getGameListSearch(int $offset, int $count, int $method, ?int $consoleID function createNewGame(string $title, int $consoleID): ?array { - sanitize_sql_inputs($title, $consoleID); + sanitize_sql_inputs($title); // $title = str_replace( "--", "-", $title ); // subtle non-comment breaker $query = "INSERT INTO GameData (Title, ConsoleID, ForumTopicID, Flags, ImageIcon, ImageTitle, ImageIngame, ImageBoxArt, Publisher, Developer, Genre, Released, IsFinal, RichPresencePatch, TotalTruePoints) @@ -872,7 +870,7 @@ function modifyGameRichPresence(string $user, int $gameID, string $dataIn): bool return true; } - sanitize_sql_inputs($gameID, $dataIn); + sanitize_sql_inputs($dataIn); $query = "UPDATE GameData SET RichPresencePatch='$dataIn' WHERE ID=$gameID"; $db = getMysqliConnection(); diff --git a/app/Helpers/database/message.php b/app/Helpers/database/message.php index 53af59b469..18bf97a02c 100644 --- a/app/Helpers/database/message.php +++ b/app/Helpers/database/message.php @@ -86,7 +86,7 @@ function GetSentMessageCount(string $user): int function GetMessage(string $user, int $id): ?array { - sanitize_sql_inputs($user, $id); + sanitize_sql_inputs($user); $query = "SELECT * FROM Messages AS msg WHERE msg.ID='$id' AND msg.UserTo='$user'"; @@ -107,7 +107,7 @@ function GetMessage(string $user, int $id): ?array function GetAllMessages(string $user, int $offset, int $count, bool $unreadOnly): array { - sanitize_sql_inputs($user, $offset, $count); + sanitize_sql_inputs($user); $retval = []; @@ -136,7 +136,7 @@ function GetAllMessages(string $user, int $offset, int $count, bool $unreadOnly) function GetSentMessages(string $user, int $offset, int $count): array { - sanitize_sql_inputs($user, $offset, $count); + sanitize_sql_inputs($user); $retval = []; diff --git a/app/Helpers/database/set-claim.php b/app/Helpers/database/set-claim.php index 5ca37977f7..648116a576 100644 --- a/app/Helpers/database/set-claim.php +++ b/app/Helpers/database/set-claim.php @@ -145,8 +145,6 @@ function dropClaim(string $user, int $gameID): bool */ function extendClaim(string $user, int $gameID): bool { - sanitize_sql_inputs($gameID); - if (hasSetClaimed($user, $gameID, true)) { $query = " UPDATE @@ -442,7 +440,7 @@ function getActiveClaimCount(?string $user = null, bool $countCollaboration = tr */ function updateClaim(int $claimID, int $claimType, int $setType, int $status, int $special, string $claimDate, string $finishedDate): bool { - sanitize_sql_inputs($claimID, $claimType, $setType, $status, $special, $claimDate, $finishedDate); + sanitize_sql_inputs($claimDate, $finishedDate); $query = " UPDATE diff --git a/app/Helpers/database/set-request.php b/app/Helpers/database/set-request.php index 09e3e47f62..04d57d0537 100644 --- a/app/Helpers/database/set-request.php +++ b/app/Helpers/database/set-request.php @@ -112,7 +112,6 @@ function getUserRequestsInformation(string $user, array $list, int $gameID = -1) */ function getSetRequestCount(int $gameID): int { - sanitize_sql_inputs($gameID); if ($gameID < 1) { return 0; } @@ -136,8 +135,6 @@ function getSetRequestCount(int $gameID): int */ function getSetRequestorsList(int $gameID, bool $getEmailInfo = false): array { - sanitize_sql_inputs($gameID); - $retVal = []; if ($gameID < 1) { @@ -187,8 +184,6 @@ function getSetRequestorsList(int $gameID, bool $getEmailInfo = false): array */ function getMostRequestedSetsList(array|int|null $console, int $offset, int $count, int $requestStatus = RequestStatus::Any): array { - sanitize_sql_inputs($offset, $count); - $retVal = []; $query = " diff --git a/app/Helpers/database/user-activity.php b/app/Helpers/database/user-activity.php index ba6b338fb3..b08ec00139 100644 --- a/app/Helpers/database/user-activity.php +++ b/app/Helpers/database/user-activity.php @@ -416,8 +416,6 @@ function getArticleComments( ?array &$dataOut, bool $recent = false ): int { - sanitize_sql_inputs($articleTypeID, $articleID, $offset, $count); - $dataOut = []; $numArticleComments = 0; $order = $recent ? ' DESC' : ''; @@ -500,8 +498,6 @@ function getLatestRichPresenceUpdates(): array function getLatestNewAchievements(int $numToFetch, ?array &$dataOut): int { - sanitize_sql_inputs($numToFetch); - $numFound = 0; $query = "SELECT ach.ID, ach.GameID, ach.Title, ach.Description, ach.Points, gd.Title AS GameTitle, gd.ImageIcon as GameIcon, ach.DateCreated, UNIX_TIMESTAMP(ach.DateCreated) AS timestamp, ach.BadgeName, c.Name AS ConsoleName @@ -527,8 +523,6 @@ function getLatestNewAchievements(int $numToFetch, ?array &$dataOut): int function GetMostPopularTitles(int $daysRange = 7, int $offset = 0, int $count = 10): array { - sanitize_sql_inputs($daysRange, $offset, $count); - $data = []; $query = "SELECT COUNT(*) as PlayedCount, gd.ID, gd.Title, gd.ImageIcon, c.Name as ConsoleName diff --git a/app/Helpers/database/user.php b/app/Helpers/database/user.php index a419af5d96..d6a1eca54a 100644 --- a/app/Helpers/database/user.php +++ b/app/Helpers/database/user.php @@ -62,8 +62,6 @@ function getUserIDFromUser(?string $user): int function getUserMetadataFromID(int $userID): ?array { - sanitize_sql_inputs($userID); - $query = "SELECT * FROM UserAccounts WHERE ID ='$userID'"; $dbResult = s_mysql_query($query); @@ -76,7 +74,7 @@ function getUserMetadataFromID(int $userID): ?array function getUserUnlockDates(string $user, int $gameID, ?array &$dataOut): int { - sanitize_sql_inputs($user, $gameID); + sanitize_sql_inputs($user); $query = "SELECT ach.ID, ach.Title, ach.Description, ach.Points, ach.BadgeName, aw.HardcoreMode, aw.Date FROM Achievements ach @@ -111,7 +109,7 @@ function getUserUnlockDates(string $user, int $gameID, ?array &$dataOut): int */ function getUserUnlocksDetailed(string $user, int $gameID, ?array &$dataOut): int { - sanitize_sql_inputs($user, $gameID); + sanitize_sql_inputs($user); $query = "SELECT ach.Title, ach.ID, ach.Points, aw.HardcoreMode FROM Achievements AS ach From 14bc77c67cb8588c6877af8badb9143d4b92cb68 Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Mon, 23 Oct 2023 15:09:28 -0600 Subject: [PATCH 76/92] avoid activity table for startsession requests (#1923) --- app/Helpers/database/user-activity.php | 107 +-------------------- public/dorequest.php | 53 ++++++++-- tests/Feature/Connect/PingTest.php | 3 + tests/Feature/Connect/PostActivityTest.php | 24 +++-- tests/Feature/Connect/StartSessionTest.php | 55 ++++++++--- 5 files changed, 107 insertions(+), 135 deletions(-) diff --git a/app/Helpers/database/user-activity.php b/app/Helpers/database/user-activity.php index b08ec00139..cc1aa941b8 100644 --- a/app/Helpers/database/user-activity.php +++ b/app/Helpers/database/user-activity.php @@ -11,39 +11,6 @@ use Carbon\Carbon; use Illuminate\Support\Facades\Cache; -/** - * @deprecated see UserActivity model - */ -function getMostRecentActivity(string $user, ?int $type = null, ?int $data = null): ?array -{ - $innerClause = "Activity.user = :user"; - if (isset($type)) { - $innerClause .= " AND Activity.activityType = $type"; - } - if (isset($data)) { - $innerClause .= " AND Activity.data = $data"; - } - - $query = "SELECT * FROM Activity AS act - WHERE act.ID = - ( SELECT MAX(Activity.ID) FROM Activity WHERE $innerClause ) "; - - return legacyDbFetch($query, ['user' => $user]); -} - -/** - * @deprecated see WriteUserActivity listener - */ -function updateActivity(int $activityID): void -{ - // Update the last update value of given activity - $query = "UPDATE Activity - SET Activity.lastupdate = NOW() - WHERE Activity.ID = $activityID "; - - legacyDbStatement($query); -} - /** * @deprecated see WriteUserActivity listener */ @@ -91,67 +58,13 @@ function postActivity(string|User $userIn, int $type, ?int $data = null, ?int $d if ($data === null) { return false; } - $gameID = $data; - /* - * Switch the rich presence to the new game immediately - */ - $game = getGameData($gameID); + $game = getGameData($data); if (!$game) { return false; } - UpdateUserRichPresence($user, $gameID, "Playing {$game['Title']}"); - - /** - * Check for recent duplicate (check cache first, then query DB) - */ - $lastPlayedTimestamp = null; - $activityID = null; - $recentlyPlayedGamesCacheKey = CacheKey::buildUserRecentGamesCacheKey($user->User); - $recentlyPlayedGames = Cache::get($recentlyPlayedGamesCacheKey); - if (!empty($recentlyPlayedGames)) { - foreach ($recentlyPlayedGames as $recentlyPlayedGame) { - if ($recentlyPlayedGame['GameID'] == $gameID) { - $activityID = $recentlyPlayedGame['ActivityID']; - $lastPlayedTimestamp = strtotime($recentlyPlayedGame['LastPlayed']); - break; - } - } - } - - if ($activityID === null) { - // not in recent activity, look back farther - $lastPlayedActivityData = getMostRecentActivity($user->User, $type, $gameID); - if (isset($lastPlayedActivityData)) { - $lastPlayedTimestamp = strtotime($lastPlayedActivityData['timestamp']); - $activityID = $lastPlayedActivityData['ID']; - } - } - - if ($activityID !== null) { - $diff = time() - $lastPlayedTimestamp; - - /* - * record game session activity only every 12 hours - */ - if ($diff < 60 * 60 * 12) { - /* - * new playing $gameTitle activity from $user, duplicate of recent activity " . ($diff/60) . " mins ago - * Updating db, but not posting! - */ - updateActivity($activityID); - expireRecentlyPlayedGames($user->User); - - return true; - } - /* - * recognises that $user has played $gameTitle recently, but longer than 12 hours ago (" . ($diff/60) . " mins) so still posting activity! - * $nowTimestamp - $lastPlayedTimestamp = $diff - */ - } - - $activity->data = (string) $gameID; + $activity->data = (string) $data; break; case ActivityType::UploadAchievement: @@ -171,12 +84,6 @@ function postActivity(string|User $userIn, int $type, ?int $data = null, ?int $d $activity->save(); - if ($type == ActivityType::StartedPlaying) { - // have to do this after the activity is saved to prevent a race condition where - // it may get re-cached before the activity is committed. - expireRecentlyPlayedGames($user->User); - } - // update UserAccount $user->LastLogin = Carbon::now(); $user->LastActivityID = $activity->ID; @@ -186,16 +93,6 @@ function postActivity(string|User $userIn, int $type, ?int $data = null, ?int $d return true; } -/** - * @deprecated see ResumePlayerSessionAction - */ -function UpdateUserRichPresence(User $user, int $gameID, string $presenceMsg): void -{ - $user->RichPresenceMsg = utf8_sanitize($presenceMsg); - $user->LastGameID = $gameID; - $user->RichPresenceMsgDate = Carbon::now(); -} - /** * @deprecated see UserActivity model */ diff --git a/public/dorequest.php b/public/dorequest.php index 94088a3fd8..e01e82acef 100644 --- a/public/dorequest.php +++ b/public/dorequest.php @@ -236,11 +236,13 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) $activityMessage = request()->post('m'); PlayerSessionHeartbeat::dispatch($user, Game::find($gameID), $activityMessage); - // TODO remove double-writes below - if (isset($activityMessage)) { - UpdateUserRichPresence($user, $gameID, $activityMessage); + // legacy rich presence support (deprecated - see ResumePlayerSession) + if (isset($activityMessage) && $user->LastGameID == $gameID) { + $user->RichPresenceMsg = utf8_sanitize($activityMessage); + $user->RichPresenceMsgDate = Carbon::now(); } + $user->LastLogin = Carbon::now(); $user->save(); @@ -301,8 +303,30 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) case "postactivity": $activityType = (int) request()->input('a'); - $activityMessage = (int) request()->input('m'); - $response['Success'] = postActivity($username, $activityType, $activityMessage); + if ($activityType != ActivityType::StartedPlaying) { + return DoRequestError("You do not have permission to do that.", 403, 'access_denied'); + } + + $gameID = (int) request()->input('m'); + $game = Game::find($gameID); + if (!$game) { + return DoRequestError("Unknown game"); + } + + if ($user->LastGameID != $gameID) { + expireRecentlyPlayedGames($user->User); + $user->LastGameID = $gameID; + } + + // legacy rich presence support (deprecated - see ResumePlayerSession) + $user->RichPresenceMsg = "Playing {$game->Title}"; + $user->RichPresenceMsgDate = Carbon::now(); + + $user->LastLogin = Carbon::now(); + $user->save(); + + PlayerSessionHeartbeat::dispatch($user, $game); + $response['Success'] = true; break; case "richpresencepatch": @@ -311,13 +335,24 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) break; case "startsession": - // TODO replace game existence with validation - if (!postActivity($username, ActivityType::StartedPlaying, $gameID)) { + $game = Game::find($gameID); + if (!$game) { return DoRequestError("Unknown game"); } - // TODO remove postActivity() above - handled by ResumePlayerSessionAction - PlayerSessionHeartbeat::dispatch($user, Game::find($gameID)); + if ($user->LastGameID != $gameID) { + expireRecentlyPlayedGames($user->User); + $user->LastGameID = $gameID; + } + + // legacy rich presence support (deprecated - see ResumePlayerSession) + $user->RichPresenceMsg = "Playing {$game->Title}"; + $user->RichPresenceMsgDate = Carbon::now(); + + $user->LastLogin = Carbon::now(); + $user->save(); + + PlayerSessionHeartbeat::dispatch($user, $game); $response['Success'] = true; $userUnlocks = getUserAchievementUnlocksForGame($username, $gameID); diff --git a/tests/Feature/Connect/PingTest.php b/tests/Feature/Connect/PingTest.php index 4a39721862..bd080b80c4 100644 --- a/tests/Feature/Connect/PingTest.php +++ b/tests/Feature/Connect/PingTest.php @@ -30,6 +30,9 @@ public function testPing(): void /** @var Game $game */ $game = Game::factory()->create(['ConsoleID' => $system->ID]); + $this->user->LastGameID = $game->ID; + $this->user->save(); + // this API requires POST $this->post('dorequest.php', $this->apiParams('ping', ['g' => $game->ID, 'm' => 'Doing good'])) ->assertStatus(200) diff --git a/tests/Feature/Connect/PostActivityTest.php b/tests/Feature/Connect/PostActivityTest.php index c038167132..74573483f6 100644 --- a/tests/Feature/Connect/PostActivityTest.php +++ b/tests/Feature/Connect/PostActivityTest.php @@ -5,8 +5,8 @@ namespace Tests\Feature\Connect; use App\Community\Enums\ActivityType; -use App\Community\Models\UserActivityLegacy; use App\Platform\Models\Game; +use App\Platform\Models\PlayerSession; use App\Platform\Models\System; use App\Site\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -35,15 +35,27 @@ public function testPostActivity(): void 'Success' => true, ]); - /** @var UserActivityLegacy $activity */ - $activity = UserActivityLegacy::latest()->first(); - $this->assertNotNull($activity); - $this->assertEquals(ActivityType::StartedPlaying, $activity->activitytype); - $this->assertEquals($game->ID, $activity->data); + // player session created + $playerSession = PlayerSession::where([ + 'user_id' => $this->user->id, + 'game_id' => $game->ID, + ])->first(); + $this->assertModelExists($playerSession); + $this->assertEquals(1, $playerSession->duration); + $this->assertEquals('Playing ' . $game->title, $playerSession->rich_presence); /** @var User $user1 */ $user1 = User::firstWhere('User', $this->user->User); $this->assertEquals($game->ID, $user1->LastGameID); $this->assertEquals("Playing " . $game->Title, $user1->RichPresenceMsg); + + // disallow anything other than StartedPlaying messages + $this->get($this->apiUrl('postactivity', ['a' => ActivityType::CompleteGame, 'm' => $game->ID])) + ->assertExactJson([ + "Success" => false, + "Error" => "You do not have permission to do that.", + "Code" => "access_denied", + "Status" => 403, + ]); } } diff --git a/tests/Feature/Connect/StartSessionTest.php b/tests/Feature/Connect/StartSessionTest.php index 29dd651061..2c9ac96ccc 100644 --- a/tests/Feature/Connect/StartSessionTest.php +++ b/tests/Feature/Connect/StartSessionTest.php @@ -4,8 +4,6 @@ namespace Tests\Feature\Connect; -use App\Community\Enums\ActivityType; -use App\Community\Models\UserActivityLegacy; use App\Platform\Models\Achievement; use App\Platform\Models\Game; use App\Platform\Models\PlayerSession; @@ -71,7 +69,7 @@ public function testStartSession(): void 'ServerNow' => Carbon::now()->timestamp, ]); - // player session resumed + // player session created $playerSession = PlayerSession::where([ 'user_id' => $this->user->id, 'game_id' => $achievement3->game_id, @@ -80,12 +78,6 @@ public function testStartSession(): void $this->assertEquals(1, $playerSession->duration); $this->assertEquals('Playing ' . $game->title, $playerSession->rich_presence); - /** @var UserActivityLegacy $activity */ - $activity = UserActivityLegacy::latest()->first(); - $this->assertNotNull($activity); - $this->assertEquals(ActivityType::StartedPlaying, $activity->activitytype); - $this->assertEquals($game->ID, $activity->data); - /** @var User $user1 */ $user1 = User::firstWhere('User', $this->user->User); $this->assertEquals($game->ID, $user1->LastGameID); @@ -118,7 +110,7 @@ public function testStartSession(): void 'ServerNow' => Carbon::now()->timestamp, ]); - // player session resumed + // player session created $playerSession = PlayerSession::where([ 'user_id' => $this->user->id, 'game_id' => $game2->id, @@ -127,13 +119,46 @@ public function testStartSession(): void $this->assertEquals(1, $playerSession->duration); $this->assertEquals('Playing ' . $game2->title, $playerSession->rich_presence); - $activity = UserActivityLegacy::latest()->first(); - $this->assertNotNull($activity); - $this->assertEquals(ActivityType::StartedPlaying, $activity->activitytype); - $this->assertEquals($game2->ID, $activity->data); - $user1 = User::firstWhere('User', $this->user->User); $this->assertEquals($game2->ID, $user1->LastGameID); $this->assertEquals("Playing " . $game2->Title, $user1->RichPresenceMsg); + + // ---------------------------- + // recently active session is extended + Carbon::setTestNow($now->addMinutes(8)); + $this->get($this->apiUrl('startsession', ['g' => $game2->ID])) + ->assertExactJson([ + 'Success' => true, + 'ServerNow' => Carbon::now()->timestamp, + ]); + + // player session created + $playerSession2 = PlayerSession::where([ + 'user_id' => $this->user->id, + 'game_id' => $game2->id, + ])->orderByDesc('id')->first(); + $this->assertModelExists($playerSession2); + $this->assertEquals($playerSession->id, $playerSession2->id); + $this->assertEquals(8, $playerSession2->duration); + $this->assertEquals('Playing ' . $game2->title, $playerSession2->rich_presence); + + // ---------------------------- + // new session created after long absence + Carbon::setTestNow($now->addHours(4)); + $this->get($this->apiUrl('startsession', ['g' => $game2->ID])) + ->assertExactJson([ + 'Success' => true, + 'ServerNow' => Carbon::now()->timestamp, + ]); + + // player session created + $playerSession2 = PlayerSession::where([ + 'user_id' => $this->user->id, + 'game_id' => $game2->id, + ])->orderByDesc('id')->first(); + $this->assertModelExists($playerSession2); + $this->assertNotEquals($playerSession->id, $playerSession2->id); + $this->assertEquals(1, $playerSession2->duration); + $this->assertEquals('Playing ' . $game2->title, $playerSession2->rich_presence); } } From 2265ac737271c07bafb2016188a4b4b632897840 Mon Sep 17 00:00:00 2001 From: luchaos Date: Tue, 24 Oct 2023 00:10:07 +0200 Subject: [PATCH 77/92] feat: use pre-aggregated data for unlocks on user lists (#1924) --- app/Helpers/database/user.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Helpers/database/user.php b/app/Helpers/database/user.php index d6a1eca54a..814cea4b62 100644 --- a/app/Helpers/database/user.php +++ b/app/Helpers/database/user.php @@ -284,9 +284,8 @@ function getUserListByPerms(int $sortBy, int $offset, int $count, ?array &$dataO default => "ua.User ASC ", }; - // TODO slow query (70) when ordering by NumAwarded $query = "SELECT ua.ID, ua.User, ua.RAPoints, ua.TrueRAPoints, ua.LastLogin, - (SELECT COUNT(*) AS NumAwarded FROM Awarded AS aw WHERE aw.User = ua.User) NumAwarded + ua.achievements_unlocked NumAwarded FROM UserAccounts AS ua $whereQuery ORDER BY $orderBy From 79ef90603e7ca8d90766b300cbfee6ae97367624 Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Thu, 26 Oct 2023 12:44:24 -0600 Subject: [PATCH 78/92] prevent exception comparing to non-existant user (#1926) --- app/Helpers/database/player-game.php | 3 +++ composer.json | 2 +- public/gamecompare.php | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/Helpers/database/player-game.php b/app/Helpers/database/player-game.php index a8617fba81..adfbb62e93 100644 --- a/app/Helpers/database/player-game.php +++ b/app/Helpers/database/player-game.php @@ -311,6 +311,9 @@ function getUserAchievementUnlocksForGame(string $username, int $gameID, int $fl { if (config('feature.aggregate_queries')) { $user = User::firstWhere('User', $username); + if (!$user) { + return []; + } $achievementIds = Achievement::where('GameID', $gameID) ->where('Flags', $flag) ->pluck('ID'); diff --git a/composer.json b/composer.json index a99e4c6e9b..54f58dff27 100644 --- a/composer.json +++ b/composer.json @@ -156,7 +156,7 @@ "@php -r \"copy('storage/app/releases.dist.php', 'storage/app/releases.php');\"", "@php -r \"file_exists('storage/app/settings.json') || copy('storage/app/settings.dist.json', 'storage/app/settings.json');\"" ], - "stan": "vendor/bin/phpstan analyse --memory-limit 512M --ansi", + "stan": "vendor/bin/phpstan analyse --memory-limit 768M --ansi", "stan-clear": "vendor/bin/phpstan clear-result-cache", "start": "php artisan serve --port=64000", "test": "php artisan test" diff --git a/public/gamecompare.php b/public/gamecompare.php index 1d58e1f862..10d5e69072 100644 --- a/public/gamecompare.php +++ b/public/gamecompare.php @@ -1,6 +1,7 @@ exists()) { + abort(404); +} + $totalFriends = getAllFriendsProgress($user, $gameID, $friendScores); $numAchievements = getGameMetadata($gameID, $user, $achievementData, $gameData, 0, $user2); From ea54e15890da300b8e62c21d8b54e0fb4acd0213 Mon Sep 17 00:00:00 2001 From: luchaos Date: Fri, 27 Oct 2023 02:34:24 +0200 Subject: [PATCH 79/92] feat: switch to mariadb and convert to utf8mb4 (#1930) --- .env.example | 3 +++ .gitignore | 1 + app/Site/AppServiceProvider.php | 2 +- config/database.php | 7 ++----- docker-compose.yml | 27 +++++++++++++++---------- docker/mysql/Dockerfile | 5 ++--- docker/mysql/create-testing-database.sh | 6 ------ docker/mysql/mysql.cnf | 9 +++++++++ 8 files changed, 34 insertions(+), 26 deletions(-) delete mode 100644 docker/mysql/create-testing-database.sh diff --git a/.env.example b/.env.example index 8f7f9d4527..b5cda929a5 100644 --- a/.env.example +++ b/.env.example @@ -50,6 +50,9 @@ DB_PORT=3306 DB_DATABASE=retroachievements-web DB_USERNAME=retroachievements DB_PASSWORD="${DB_USERNAME}" +# TODO remove after utf8mb4 conversion +#DB_CHARSET=latin1 +#DB_COLLATION=latin1_general_ci #LEGACY_MEDIA_PATH= diff --git a/.gitignore b/.gitignore index 8830082a41..c6e841d873 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/database/*.sql /docker/nginx/logs /docs/dist /node_modules diff --git a/app/Site/AppServiceProvider.php b/app/Site/AppServiceProvider.php index d5e93beafa..f91b7687a5 100644 --- a/app/Site/AppServiceProvider.php +++ b/app/Site/AppServiceProvider.php @@ -114,7 +114,7 @@ public function boot(): void if (!$db) { throw new Exception('Could not connect to database. Please try again later.'); } - mysqli_set_charset($db, 'latin1'); + mysqli_set_charset($db, config('database.connections.mysql.charset')); mysqli_query($db, "SET sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''));"); return $db; diff --git a/config/database.php b/config/database.php index 96f2650258..e5264525f2 100755 --- a/config/database.php +++ b/config/database.php @@ -54,11 +54,8 @@ 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), 'unix_socket' => env('DB_SOCKET', ''), - // TODO - // 'charset' => 'utf8mb4', - // 'collation' => 'utf8mb4_general_ci', - 'charset' => 'latin1', - 'collation' => 'latin1_general_ci', + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 'prefix' => '', 'prefix_indexes' => false, 'strict' => true, diff --git a/docker-compose.yml b/docker-compose.yml index 606b6d8385..ab1fe5c21c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: networks: - raweb depends_on: - - mysql + - mariadb - redis - minio nginx: @@ -38,31 +38,36 @@ services: - raweb depends_on: - laravel.test - mysql: + mariadb: build: context: ./docker/mysql dockerfile: Dockerfile - image: 'mysql-pv:8' + image: 'mariadb-pv:10' environment: + MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}' + MYSQL_ROOT_HOST: "%" MYSQL_DATABASE: '${DB_DATABASE}' MYSQL_USER: '${DB_USERNAME}' - MYSQL_PASSWORD: '${DB_PASSWORD:-secret}' - MYSQL_ROOT_PASSWORD: '${DB_PASSWORD:-secret}' + MYSQL_PASSWORD: '${DB_PASSWORD}' + MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' ports: - '${FORWARD_DB_PORT:-3306}:3306' volumes: - - 'mysql-data:/var/lib/mysql' - - './database:/docker-entrypoint-initdb.d/' + - 'mariadb-data:/var/lib/mysql' + - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh' + - './database:/docker-entrypoint-initdb.d/database' - './docker/mysql/mysql.cnf:/etc/mysql/conf.d/mysql.cnf:ro' networks: - raweb - command: - - '--default-authentication-plugin=mysql_native_password' + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-p${DB_PASSWORD}"] + retries: 3 + timeout: 5s phpmyadmin: image: phpmyadmin/phpmyadmin environment: PMA_ARBITRARY: 1 - PMA_HOST: mysql + PMA_HOST: mariadb PMA_USER: '${DB_USERNAME}' PMA_PASSWORD: '${DB_PASSWORD}' PMA_PORT: 3306 @@ -117,7 +122,7 @@ networks: raweb: driver: bridge volumes: - mysql-data: + mariadb-data: driver: local minio-data: driver: local diff --git a/docker/mysql/Dockerfile b/docker/mysql/Dockerfile index 9502d74469..27d8036340 100644 --- a/docker/mysql/Dockerfile +++ b/docker/mysql/Dockerfile @@ -1,4 +1,3 @@ -FROM mysql:8 +FROM mariadb:10 -RUN microdnf install -y epel-release -RUN microdnf install -y pv +RUN apt-get update && apt-get install -y pv diff --git a/docker/mysql/create-testing-database.sh b/docker/mysql/create-testing-database.sh deleted file mode 100644 index aeb1826f1e..0000000000 --- a/docker/mysql/create-testing-database.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -mysql --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL - CREATE DATABASE IF NOT EXISTS testing; - GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%'; -EOSQL diff --git a/docker/mysql/mysql.cnf b/docker/mysql/mysql.cnf index 4f1829d66a..d126fd7790 100644 --- a/docker/mysql/mysql.cnf +++ b/docker/mysql/mysql.cnf @@ -1,3 +1,12 @@ +[client] +default-character-set = utf8mb4 + +[mysql] +default-character-set = utf8mb4 + [mysqld] skip-host-cache skip-name-resolve + +character-set-server = utf8mb4 +collation-server = utf8mb4_unicode_ci From f271b80e6190183bece63b50e299cff944c6e01e Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Fri, 27 Oct 2023 08:52:27 -0600 Subject: [PATCH 80/92] Cache ticket count (#1928) --- app/Helpers/database/set-claim.php | 30 +++++++++---- app/Helpers/database/ticket.php | 67 ++++++++++++++++++++---------- app/Support/Cache/CacheKey.php | 15 +++++++ 3 files changed, 83 insertions(+), 29 deletions(-) diff --git a/app/Helpers/database/set-claim.php b/app/Helpers/database/set-claim.php index 648116a576..f02e56321b 100644 --- a/app/Helpers/database/set-claim.php +++ b/app/Helpers/database/set-claim.php @@ -8,6 +8,7 @@ use App\Community\Enums\ClaimType; use App\Community\Models\AchievementSetClaim; use App\Site\Enums\Permissions; +use App\Support\Cache\CacheKey; use Carbon\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; @@ -468,23 +469,38 @@ function getExpiringClaim(string $username): array return []; } + $cacheKey = CacheKey::buildUserExpiringClaimsCacheKey($username); + + $value = Cache::get($cacheKey); + if ($value !== null) { + return $value; + } + $claims = AchievementSetClaim::select( DB::raw('COALESCE(SUM(CASE WHEN TIMESTAMPDIFF(MINUTE, NOW(), Finished) <= 0 THEN 1 ELSE 0 END), 0) AS Expired'), - DB::raw('COALESCE(SUM(CASE WHEN TIMESTAMPDIFF(MINUTE, NOW(), Finished) BETWEEN 0 AND 10080 THEN 1 ELSE 0 END), 0) AS Expiring') + DB::raw('COALESCE(SUM(CASE WHEN TIMESTAMPDIFF(MINUTE, NOW(), Finished) BETWEEN 0 AND 10080 THEN 1 ELSE 0 END), 0) AS Expiring'), + DB::raw('COUNT(*) AS Count') ) ->where('User', $username) ->whereIn('Status', [ClaimStatus::Active, ClaimStatus::InReview]) ->where('Special', '!=', ClaimSpecial::ScheduledRelease) ->first(); - if (!$claims) { - return []; + if (!$claims || $claims['Count'] == 0) { + $value = []; + // new claim expiration is 30 days and expiration warning is 7 days, so this guarantees a refresh before expiration + Cache::put($cacheKey, $value, Carbon::now()->addDays(20)); + } else { + $value = [ + 'Expired' => $claims->Expired, + 'Expiring' => $claims->Expiring, + ]; + // refresh once an hour. this query only takes about 2ms, so it's not super expensive, but + // we want to avoid doing it on every page load. + Cache::put($cacheKey, $value, Carbon::now()->addHours(1)); } - return [ - 'Expired' => $claims->Expired, - 'Expiring' => $claims->Expiring, - ]; + return $value; } /** diff --git a/app/Helpers/database/ticket.php b/app/Helpers/database/ticket.php index 9a3424f34c..6bf4d621dc 100644 --- a/app/Helpers/database/ticket.php +++ b/app/Helpers/database/ticket.php @@ -152,6 +152,8 @@ function _createTicket(User $user, int $achID, int $reportType, ?int $hardcore, $gameID = $achData['GameID']; $gameTitle = $achData['GameTitle']; + expireUserTicketCounts($achAuthor); + $problemTypeStr = ($reportType === 1) ? "Triggers at wrong time" : "Doesn't trigger"; $bugReportDetails = "Achievement: [ach=$achID] @@ -166,7 +168,7 @@ function _createTicket(User $user, int $achID, int $reportType, ?int $hardcore, $bugReportMessage = "Hi, $achAuthor!\r\n [user=$username] would like to report a bug with an achievement you've created: $bugReportDetails"; - CreateNewMessage($username, $achData['Author'], "Bug Report ($gameTitle)", $bugReportMessage); + CreateNewMessage($username, $achAuthor, "Bug Report ($gameTitle)", $bugReportMessage); postActivity($username, ActivityType::OpenedTicket, $achID); // notify subscribers other than the achievement's author @@ -384,11 +386,15 @@ function updateTicket(string $user, int $ticketID, int $ticketVal, ?string $reas addArticleComment("Server", ArticleType::AchievementTicket, $ticketID, $comment, $user); + expireUserTicketCounts($ticketData['AchievementAuthor']); + $reporterData = []; if (!getAccountDetails($userReporter, $reporterData)) { return true; } + expireUserTicketCounts($userReporter); + $email = $reporterData['EmailAddress']; $emailTitle = "Ticket status changed"; @@ -415,9 +421,13 @@ function countRequestTicketsByUser(?User $user = null): int return 0; } - return Ticket::where('ReportState', TicketState::Request) - ->where('ReportedByUserID', $user->ID) - ->count(); + $cacheKey = CacheKey::buildUserRequestTicketsCacheKey($user->User); + + return Cache::remember($cacheKey, Carbon::now()->addHours(20), function () use ($user) { + return Ticket::where('ReportState', TicketState::Request) + ->where('ReportedByUserID', $user->ID) + ->count(); + }); } function countOpenTicketsByDev(string $dev): ?array @@ -426,27 +436,40 @@ function countOpenTicketsByDev(string $dev): ?array return null; } - $retVal = [ - TicketState::Open => 0, - TicketState::Request => 0, - ]; + $cacheKey = CacheKey::buildUserOpenTicketsCacheKey($dev); - $tickets = Ticket::with('achievement') - ->whereHas('achievement', function ($query) use ($dev) { - $query - ->where('Author', $dev) - ->whereIn('Flags', [AchievementFlag::OfficialCore, AchievementFlag::Unofficial]); - }) - ->whereIn('ReportState', [TicketState::Open, TicketState::Request]) - ->select('AchievementID', 'ReportState', DB::raw('count(*) as Count')) - ->groupBy('ReportState') - ->get(); + return Cache::remember($cacheKey, Carbon::now()->addHours(20), function () use ($dev) { + $retVal = [ + TicketState::Open => 0, + TicketState::Request => 0, + ]; - foreach ($tickets as $ticket) { - $retVal[$ticket->ReportState] = $ticket->Count; - } + $tickets = Ticket::with('achievement') + ->whereHas('achievement', function ($query) use ($dev) { + $query + ->where('Author', $dev) + ->whereIn('Flags', [AchievementFlag::OfficialCore, AchievementFlag::Unofficial]); + }) + ->whereIn('ReportState', [TicketState::Open, TicketState::Request]) + ->select('AchievementID', 'ReportState', DB::raw('count(*) as Count')) + ->groupBy('ReportState') + ->get(); - return $retVal; + foreach ($tickets as $ticket) { + $retVal[$ticket->ReportState] = (int) $ticket->Count; + } + + return $retVal; + }); +} + +function expireUserTicketCounts(string $username): void +{ + $cacheKey = CacheKey::buildUserRequestTicketsCacheKey($username); + Cache::forget($cacheKey); + + $cacheKey = CacheKey::buildUserOpenTicketsCacheKey($username); + Cache::forget($cacheKey); } function countOpenTicketsByAchievement(int $achievementID): int diff --git a/app/Support/Cache/CacheKey.php b/app/Support/Cache/CacheKey.php index 758744073c..407257907f 100644 --- a/app/Support/Cache/CacheKey.php +++ b/app/Support/Cache/CacheKey.php @@ -62,6 +62,21 @@ public static function buildUserRecentGamesCacheKey(string $username): string return self::buildNormalizedUserCacheKey($username, "recent-games"); } + public static function buildUserOpenTicketsCacheKey(string $username): string + { + return self::buildNormalizedUserCacheKey($username, "open-tickets"); + } + + public static function buildUserRequestTicketsCacheKey(string $username): string + { + return self::buildNormalizedUserCacheKey($username, "request-tickets"); + } + + public static function buildUserExpiringClaimsCacheKey(string $username): string + { + return self::buildNormalizedUserCacheKey($username, "expiring-claims"); + } + /** * Constructs a normalized cache key. * From 65c4afca510f727355d1706208702475a332fe23 Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Fri, 27 Oct 2023 08:55:58 -0600 Subject: [PATCH 81/92] use aggregated data for usergameactivity (#1925) --- app/Helpers/database/user-activity.php | 279 ++++++++++++------------- public/usergameactivity.php | 18 +- 2 files changed, 150 insertions(+), 147 deletions(-) diff --git a/app/Helpers/database/user-activity.php b/app/Helpers/database/user-activity.php index cc1aa941b8..7d6ed5a479 100644 --- a/app/Helpers/database/user-activity.php +++ b/app/Helpers/database/user-activity.php @@ -5,6 +5,9 @@ use App\Community\Models\Comment; use App\Community\Models\UserActivityLegacy; use App\Platform\Enums\AchievementFlag; +use App\Platform\Models\Game; +use App\Platform\Models\PlayerAchievement; +use App\Platform\Models\PlayerSession; use App\Site\Enums\Permissions; use App\Site\Models\User; use App\Support\Cache\CacheKey; @@ -440,100 +443,165 @@ function GetMostPopularTitles(int $daysRange = 7, int $offset = 0, int $count = return $data; } -function getUserGameActivity(string $user, int $gameID): array +function getUserGameActivity(string $username, int $gameID): array { - sanitize_sql_inputs($user); - - $query = "SELECT a.timestamp, a.lastupdate, a.data - FROM Activity a - WHERE a.User='$user' AND a.data=$gameID - AND a.activitytype=" . ActivityType::StartedPlaying; - $dbResult = s_mysql_query($query); - if ($dbResult === false) { - log_sql_fail(); + $user = User::firstWhere('User', $username); + if (!$user) { + return []; + } + $game = Game::firstWhere('ID', $gameID); + if (!$game) { return []; } + $achievements = []; + $unofficialAchievements = []; $sessions = []; - while ($row = mysqli_fetch_assoc($dbResult)) { - $sessions[] = [ - 'StartTime' => strtotime($row['timestamp']), - ]; - if ($row['lastupdate'] !== $row['timestamp']) { - $sessions[] = [ - 'StartTime' => strtotime($row['lastupdate']), - ]; + $playerSessions = PlayerSession::where('user_id', '=', $user->ID) + ->where('game_id', '=', $gameID) + ->get(); + foreach ($playerSessions as $playerSession) { + $session = [ + 'StartTime' => $playerSession->created_at->unix(), + 'EndTime' => $playerSession->updated_at->unix(), + 'IsGenerated' => $playerSession->created_at < Carbon::create(2023, 10, 14, 13, 16, 42), + 'Achievements' => [], + ]; + if (!empty($playerSession->rich_presence)) { + $session['RichPresence'] = $playerSession->rich_presence; + $session['RichPresenceTime'] = $playerSession->rich_presence_updated_at->unix(); } + $sessions[] = $session; } - // create a dummy placeholder session for any achievements unlocked before the first session - $sessions[] = [ - 'StartTime' => 0, - 'IsGenerated' => true, - ]; - // reverse sort by date so we can update the appropriate session when we find it usort($sessions, fn ($a, $b) => $b['StartTime'] - $a['StartTime']); - $query = "SELECT a.timestamp, a.data, a.data2, ach.Title, ach.Description, ach.Points, ach.BadgeName, ach.Flags - FROM Activity a - LEFT JOIN Achievements ach ON ach.ID = a.data - WHERE ach.GameID=$gameID AND a.User='$user' - AND a.activitytype=" . ActivityType::UnlockedAchievement; - $dbResult = s_mysql_query($query); - if ($dbResult === false) { - log_sql_fail(); + $addAchievementToSession = function (&$sessions, $playerAchievement, $when, $hardcore): void { + $createSessionAchievement = function ($playerAchievement, $when, $hardcore): array { + return [ + 'When' => $when, + 'AchievementID' => $playerAchievement->achievement_id, + 'HardcoreMode' => $hardcore, + 'Flags' => $playerAchievement->Flags, + // used by avatar function to avoid additional query + 'Title' => $playerAchievement->Title, + 'Description' => $playerAchievement->Description, + 'Points' => $playerAchievement->Points, + 'BadgeName' => $playerAchievement->BadgeName, + ]; + }; - return []; - } + $maxSessionGap = 4 * 60 * 60; // 4 hours - $achievements = []; - $unofficialAchievements = []; - while ($row = mysqli_fetch_assoc($dbResult)) { - $when = strtotime($row['timestamp']); - $achievements[$row['data']] = $when; + $possibleSession = null; + foreach ($sessions as &$session) { + if ($session['StartTime'] <= $when) { + if ($session['EndTime'] + $maxSessionGap > $when) { + $session['Achievements'][] = $createSessionAchievement($playerAchievement, $when, $hardcore); + $session['EndTime'] = $when; - if ($row['Flags'] != AchievementFlag::OfficialCore) { - $unofficialAchievements[$row['data']] = 1; + return; + } + $possibleSession = $session; + } } - foreach ($sessions as &$session) { - if ($session['StartTime'] < $when) { - $session['Achievements'][] = [ - 'When' => $when, - 'AchievementID' => $row['data'], - 'Title' => $row['Title'], - 'Description' => $row['Description'], - 'Points' => $row['Points'], - 'BadgeName' => $row['BadgeName'], - 'Flags' => $row['Flags'], - 'HardcoreMode' => $row['data2'], - ]; - break; + if ($possibleSession) { + if ($when - $possibleSession['EndTime'] < $maxSessionGap) { + $possibleSession['Achievements'][] = $createSessionAchievement($playerAchievement, $when, $hardcore); + $possibleSession['EndTime'] = $when; + + return; + } + + $index = array_search($sessions, $possibleSession); + if ($index < count($sessions)) { + $possibleSession = $sessions[$index + 1]; + if ($possibleSession['StartTime'] - $when < $maxSessionGap) { + $possibleSession['Achievements'][] = $createSessionAchievement($playerAchievement, $when, $hardcore); + $possibleSession['StartTime'] = $when; + + return; + } } } - } - // calculate the duration of each session - $totalTime = _updateUserGameSessionDurations($sessions, $achievements); + $sessions[] = [ + 'StartTime' => $when, + 'EndTime' => $when, + 'IsGenerated' => true, + 'Achievements' => [$createSessionAchievement($playerAchievement, $when, $hardcore)], + ]; + usort($sessions, fn ($a, $b) => $b['StartTime'] - $a['StartTime']); + }; + + $playerAchievements = PlayerAchievement::where('player_achievements.user_id', '=', $user->ID) + ->join('Achievements', 'player_achievements.achievement_id', '=', 'Achievements.ID') + ->where('Achievements.GameID', '=', $gameID) + ->orderBy('player_achievements.unlocked_at') + ->select(['player_achievements.*', 'Achievements.Flags', 'Achievements.Title', + 'Achievements.Description', 'Achievements.Points', 'Achievements.BadgeName']) + ->get(); + foreach ($playerAchievements as $playerAchievement) { + if ($playerAchievement->Flags != AchievementFlag::OfficialCore) { + $unofficialAchievements[$playerAchievement->achievement_id] = 1; + } + + $achievements[$playerAchievement->achievement_id] = $playerAchievement->unlocked_at->unix(); + + if ($playerAchievement->unlocked_hardcore_at) { + $addAchievementToSession($sessions, $playerAchievement, $playerAchievement->unlocked_hardcore_at->unix(), true); + + if ($playerAchievement->unlocked_hardcore_at != $playerAchievement->unlocked_at) { + $addAchievementToSession($sessions, $playerAchievement, $playerAchievement->unlocked_at->unix(), false); + } + } else { + $addAchievementToSession($sessions, $playerAchievement, $playerAchievement->unlocked_at->unix(), false); + } + } // sort everything and find the first and last achievement timestamps usort($sessions, fn ($a, $b) => $a['StartTime'] - $b['StartTime']); + $hasGenerated = false; + $totalTime = 0; + $achievementsTime = 0; + $intermediateTime = 0; $unlockSessionCount = 0; + $intermediateSessionCount = 0; $firstAchievementTime = null; $lastAchievementTime = null; foreach ($sessions as &$session) { + $elapsed = ($session['EndTime'] - $session['StartTime']); + $totalTime += $elapsed; + if (!empty($session['Achievements'])) { + if ($achievementsTime > 0) { + $achievementsTime += $intermediateTime; + $unlockSessionCount += $intermediateSessionCount; + } + $achievementsTime += $elapsed; + $intermediateTime = 0; + $intermediateSessionCount = 0; + $unlockSessionCount++; + usort($session['Achievements'], fn ($a, $b) => $a['When'] - $b['When']); foreach ($session['Achievements'] as &$achievement) { if ($firstAchievementTime === null) { $firstAchievementTime = $achievement['When']; } $lastAchievementTime = $achievement['When']; } + + if ($session['IsGenerated']) { + $hasGenerated = true; + } + } else { + $intermediateTime += $elapsed; + $intermediateSessionCount++; } } @@ -542,9 +610,12 @@ function getUserGameActivity(string $user, int $gameID): array // approximate time per achievement earned. add this value to each session to account // for time played after getting the last achievement of the session. $achievementsUnlocked = count($achievements); - if ($achievementsUnlocked > 0 && $unlockSessionCount > 1) { - $sessionAdjustment = $totalTime / $achievementsUnlocked; - $totalTime += $sessionAdjustment * $unlockSessionCount; + if ($hasGenerated && $achievementsUnlocked > 0) { + $sessionAdjustment = $achievementsTime / $achievementsUnlocked; + $totalTime += $sessionAdjustment * count($sessions); + if ($unlockSessionCount > 1) { + $achievementsTime += $sessionAdjustment * $unlockSessionCount; + } } else { $sessionAdjustment = 0; } @@ -552,99 +623,15 @@ function getUserGameActivity(string $user, int $gameID): array $activity = [ 'Sessions' => $sessions, 'TotalTime' => $totalTime, + 'AchievementsTime' => $achievementsTime, 'PerSessionAdjustment' => $sessionAdjustment, 'AchievementsUnlocked' => count($achievements) - count($unofficialAchievements), 'UnlockSessionCount' => $unlockSessionCount, 'FirstUnlockTime' => $firstAchievementTime, 'LastUnlockTime' => $lastAchievementTime, 'TotalUnlockTime' => ($lastAchievementTime != null) ? $lastAchievementTime - $firstAchievementTime : 0, + 'CoreAchievementCount' => $game->achievements_published, ]; - // Count num possible achievements - $query = "SELECT COUNT(*) as Count FROM Achievements ach - WHERE ach.Flags=" . AchievementFlag::OfficialCore . " AND ach.GameID=$gameID"; - $dbResult = s_mysql_query($query); - if ($dbResult) { - $activity['CoreAchievementCount'] = mysqli_fetch_assoc($dbResult)['Count']; - } - return $activity; } - -function _updateUserGameSessionDurations(array &$sessions, array $achievements): int -{ - $totalTime = 0; - $newSessions = []; - foreach ($sessions as &$session) { - if (!array_key_exists('Achievements', $session)) { - if ($session['StartTime'] > 0) { - $session['Achievements'] = []; - $session['EndTime'] = $session['StartTime']; - $newSessions[] = $session; - } - } else { - usort($session['Achievements'], fn ($a, $b) => $a['When'] - $b['When']); - - if ($session['StartTime'] === 0) { - $session['StartTime'] = $session['Achievements'][0]['When']; - } - - foreach ($session['Achievements'] as &$achievement) { - if ($achievement['When'] != $achievements[$achievement['AchievementID']]) { - $achievement['UnlockedLater'] = true; - } - } - - // if there are any gaps in the achievements earned within a session that - // are more than four hours apart, split into separate sessions - $split = []; - $prevTime = $session['StartTime']; - $itemsCount = count($session['Achievements']); - for ($i = 0; $i < $itemsCount; $i++) { - $distance = $session['Achievements'][$i]['When'] - $prevTime; - if ($distance > 4 * 60 * 60) { - $split[] = $i; - } - $prevTime = $session['Achievements'][$i]['When']; - } - - if (empty($split)) { - $session['EndTime'] = end($session['Achievements'])['When']; - $totalTime += ($session['EndTime'] - $session['StartTime']); - $newSessions[] = $session; - } else { - $split[] = count($session['Achievements']); - $firstIndex = 0; - $isGenerated = false; - foreach ($split as $i) { - if ($i === 0) { - $newSession = [ - 'StartTime' => $session['StartTime'], - 'EndTime' => $session['StartTime'], - 'Achievements' => [], - ]; - } else { - $newSession = [ - 'StartTime' => $isGenerated ? $session['Achievements'][$firstIndex]['When'] : - $session['StartTime'], - 'EndTime' => $session['Achievements'][$i - 1]['When'], - 'Achievements' => array_slice($session['Achievements'], $firstIndex, $i - $firstIndex), - ]; - } - - $newSession['IsGenerated'] = $isGenerated; - $isGenerated = true; - - $totalTime += ($newSession['EndTime'] - $newSession['StartTime']); - $newSessions[] = $newSession; - - $firstIndex = $i; - } - } - } - } - - $sessions = $newSessions; - - return $totalTime; -} diff --git a/public/usergameactivity.php b/public/usergameactivity.php index a0d8c27471..0ff776bed3 100644 --- a/public/usergameactivity.php +++ b/public/usergameactivity.php @@ -59,7 +59,10 @@ echo "$pageTitleAttr"; echo ""; echo ""; - echo ""; + if ($activity['TotalTime'] != $activity['AchievementsTime']) { + echo ""; + } + echo ""; echo ""; echo ""; echo "
    User:" . userAvatar($user2, icon: false) . "
    Total Playtime:" . formatHMS($activity['TotalTime']) . "$estimated
    Total Playtime:" . formatHMS($activity['TotalTime']) . "$estimated
    Achievement Playtime:" . formatHMS($activity['AchievementsTime']) . "$estimated
    Achievement Sessions:$sessionInfo
    Achievements Unlocked:" . $activity['AchievementsUnlocked'] . "$userProgress
    "; @@ -96,6 +99,19 @@ echo ""; } + + if (array_key_exists('RichPresence', $session) && !empty($session['RichPresence'])) { + $when = getNiceDate($session['RichPresenceTime']); + $formatted = formatHMS($session['RichPresenceTime'] - $prevWhen); + echo " $when (+$formatted)Rich Presence: {$session['RichPresence']}"; + $prevWhen = $session['RichPresenceTime']; + } + + if ($session['EndTime'] != $prevWhen) { + $when = getNiceDate($session['EndTime']); + $formatted = formatHMS($session['EndTime'] - $prevWhen); + echo " $when (+$formatted)End of session"; + } } echo ""; From f9c1eec28212f2155cbd89a9313a796915d6c3e4 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Fri, 27 Oct 2023 11:02:42 -0400 Subject: [PATCH 82/92] refactor(renderAchievementTitle): migrate to blade (#1904) --- app/Helpers/render/achievement.php | 33 +++---------------- public/achievementInfo.php | 15 +++++++-- public/reportissue.php | 13 ++++++-- public/ticketmanager.php | 7 +++- .../community/components/event/aotw.blade.php | 5 +-- .../components/achievement/title.blade.php | 25 ++++++++++++++ .../achievements-list-item.blade.php | 4 +-- 7 files changed, 64 insertions(+), 38 deletions(-) create mode 100644 resources/views/platform/components/achievement/title.blade.php diff --git a/app/Helpers/render/achievement.php b/app/Helpers/render/achievement.php index 6f371c2257..59fba56206 100644 --- a/app/Helpers/render/achievement.php +++ b/app/Helpers/render/achievement.php @@ -1,8 +1,8 @@ ', ['rawTitle' => $label]); } if ($icon !== false) { @@ -61,31 +61,6 @@ function achievementAvatar( ); } -/** - * Render achievement title, parsing `[m]` (missable) as a tag - */ -function renderAchievementTitle(?string $title, bool $tags = true): string -{ - if (!$title) { - return ''; - } - if (!Str::contains($title, '[m]')) { - return $title; - } - - $missableTag = ''; - if ($tags) { - $missableTag = " [m]"; - } - $title = str_replace('[m]', '', $title); - - // If we don't strip consecutive spaces, the - // browser doesn't collapse them in forum
     tags.
    -    $title = preg_replace('/\s+/', ' ', $title);
    -
    -    return trim("$title$missableTag");
    -}
    -
     function renderAchievementCard(int|string|array $achievement, ?string $context = null, ?string $iconUrl = null): string
     {
         $id = is_int($achievement) || is_string($achievement) ? (int) $achievement : ($achievement['AchievementID'] ?? $achievement['ID'] ?? null);
    @@ -103,7 +78,9 @@ function renderAchievementCard(int|string|array $achievement, ?string $context =
             $data = Cache::store('array')->rememberForever('achievement:' . $id . ':card-data', fn () => GetAchievementData($id));
         }
     
    -    $title = renderAchievementTitle($data['AchievementTitle'] ?? $data['Title'] ?? null);
    +    $title = Blade::render('', [
    +        'rawTitle' => $data['AchievementTitle'] ?? $data['Title'] ?? '',
    +    ]);
         $description = $data['AchievementDesc'] ?? $data['Description'] ?? null;
         $achPoints = $data['Points'] ?? null;
         $badgeName = $data['BadgeName'] ?? null;
    diff --git a/public/achievementInfo.php b/public/achievementInfo.php
    index 4e760a8ccc..11cbd84ddf 100644
    --- a/public/achievementInfo.php
    +++ b/public/achievementInfo.php
    @@ -195,9 +195,18 @@ function ResetProgress() {
         ";
     
    +    $breadcrumbAchievementTitle = Blade::render('
    +        ', [
    +        'rawTitle' => $achievementTitle,
    +        'isDisplayingTags' => false,
    +    ]);
    +
         echo "";
     
         echo Blade::render('
    @@ -228,7 +237,9 @@ function ResetProgress() {
         echo "";
         echo "
    "; - $renderedTitle = renderAchievementTitle($achievementTitle); + $renderedTitle = Blade::render('', [ + 'rawTitle' => $achievementTitle, + ]); echo "
    "; echo "
    "; diff --git a/public/reportissue.php b/public/reportissue.php index 7b7bab9017..665efb470a 100644 --- a/public/reportissue.php +++ b/public/reportissue.php @@ -56,8 +56,17 @@ function displayCore() {
    diff --git a/public/ticketmanager.php b/public/ticketmanager.php index c60571b928..fab95f6217 100644 --- a/public/ticketmanager.php +++ b/public/ticketmanager.php @@ -137,7 +137,12 @@ if (!empty($gameIDGiven)) { echo " » $gameTitle ($consoleName)"; if (!empty($achievementIDGiven)) { - echo " » " . renderAchievementTitle($achievementTitle, tags: false); + echo " » " . Blade::render(' + + ', [ + 'rawTitle' => $achievementTitle, + 'isDisplayingTags' => false, + ]); } } } else { diff --git a/resources/views/community/components/event/aotw.blade.php b/resources/views/community/components/event/aotw.blade.php index 64554474f9..8a946403d0 100644 --- a/resources/views/community/components/event/aotw.blade.php +++ b/resources/views/community/components/event/aotw.blade.php @@ -13,7 +13,6 @@ $achievementRetroPoints = $achievement->TrueRatio; $achievementBadgeName = $achievement->BadgeName; -$renderedAchievementTitle = renderAchievementTitle($achievementName); $renderedGameTitle = renderGameTitle($game->Title); $achievementIconSrc = media_asset("/Badge/$achievementBadgeName.png"); $gameSystemIconUrl = getSystemIconUrl($game->ConsoleID); @@ -33,7 +32,9 @@
    - {!! $renderedAchievementTitle !!} + + +

    {{ $achievementPoints }} ({{ $achievementRetroPoints }}) Points

    {{ $achievement->Description }}

    diff --git a/resources/views/platform/components/achievement/title.blade.php b/resources/views/platform/components/achievement/title.blade.php new file mode 100644 index 0000000000..5ca5193b2a --- /dev/null +++ b/resources/views/platform/components/achievement/title.blade.php @@ -0,0 +1,25 @@ +@props([ + 'isDisplayingTags' => true, + 'rawTitle' => '', +]) + + tags. +$processedTitle = preg_replace('/\s+/', ' ', $processedTitle); +?> + +{{ $processedTitle }} +@if ($isDisplayingTags && $containsMissableTag) + + [m] + +@endif diff --git a/resources/views/platform/components/game/achievements-list/achievements-list-item.blade.php b/resources/views/platform/components/game/achievements-list/achievements-list-item.blade.php index 1af624e6da..43000962e1 100644 --- a/resources/views/platform/components/game/achievements-list/achievements-list-item.blade.php +++ b/resources/views/platform/components/game/achievements-list/achievements-list-item.blade.php @@ -35,8 +35,6 @@ tooltip: false ); -$renderedAchievementTitle = renderAchievementTitle($achievement['Title']); - $unlockDate = ''; if (isset($achievement['DateEarned'])) { $unlockDate = Carbon::parse($achievement['DateEarned'])->format('F j Y, g:ia'); @@ -56,7 +54,7 @@
    - {!! $renderedAchievementTitle !!} + @if ($achievement['Points'] > 0 || $achievement['TrueRatio'] > 0) From bc6d93d91ee440e68273276654fa20f636c6a039 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Fri, 27 Oct 2023 11:07:15 -0400 Subject: [PATCH 83/92] fix(home): remediate aotw game title rendering issue (#1902) --- resources/views/community/components/event/aotw.blade.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/views/community/components/event/aotw.blade.php b/resources/views/community/components/event/aotw.blade.php index 8a946403d0..4ab5ebb610 100644 --- a/resources/views/community/components/event/aotw.blade.php +++ b/resources/views/community/components/event/aotw.blade.php @@ -43,7 +43,7 @@
    - + {{-- Keep the image and game title in a single tooltipped container. Do not tooltip the console name. --}}
    - - + {{-- Provide invisible space to slide the console underneath --}} + Console icon From c2c2f9b073a25e036cb5b71316b701295d891cfb Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Fri, 27 Oct 2023 09:21:56 -0600 Subject: [PATCH 84/92] add tests for dorequest?r=uploadachievement (#1906) --- app/Helpers/database/achievement.php | 177 ++-- app/Helpers/database/static.php | 25 +- app/Helpers/database/user-activity.php | 22 +- app/Helpers/database/user-permission.php | 15 +- app/Helpers/database/user.php | 15 +- app/Platform/EventServiceProvider.php | 2 + ...tchUpdateDeveloperContributionYieldJob.php | 20 +- .../DispatchUpdateGameMetricsJob.php | 5 + .../Feature/Connect/UploadAchievementTest.php | 904 ++++++++++++++++++ 9 files changed, 1014 insertions(+), 171 deletions(-) create mode 100644 tests/Feature/Connect/UploadAchievementTest.php diff --git a/app/Helpers/database/achievement.php b/app/Helpers/database/achievement.php index efb5d8f900..b5e7f1d0ca 100644 --- a/app/Helpers/database/achievement.php +++ b/app/Helpers/database/achievement.php @@ -205,11 +205,6 @@ function UploadNewAchievement( return false; } - $dbAuthor = $author; - $rawDesc = $desc; - $rawTitle = $title; - sanitize_sql_inputs($title, $desc, $mem, $progress, $progressMax, $progressFmt, $dbAuthor, $type); - $typeValue = ""; if ($type === null || trim($type) === '' || $type === 'not-given') { $typeValue = "NULL"; @@ -226,67 +221,84 @@ function UploadNewAchievement( return false; } - $query = " - INSERT INTO Achievements ( - ID, GameID, Title, Description, - MemAddr, Progress, ProgressMax, - ProgressFormat, Points, Flags, type, - Author, DateCreated, DateModified, - Updated, VotesPos, VotesNeg, - BadgeName, DisplayOrder, AssocVideo, - TrueRatio - ) - VALUES ( - NULL, '$gameID', '$title', '$desc', - '$mem', '$progress', '$progressMax', - '$progressFmt', $points, $flag, $typeValue, - '$dbAuthor', NOW(), NOW(), - NOW(), 0, 0, - '$badge', 0, NULL, - 0 - )"; - $db = getMysqliConnection(); - if (mysqli_query($db, $query) !== false) { - $idInOut = mysqli_insert_id($db); - postActivity($author, ActivityType::UploadAchievement, $idInOut); - - static_addnewachievement($idInOut); - addArticleComment( - "Server", - ArticleType::Achievement, - $idInOut, - "$author uploaded this achievement.", - $author - ); - - // uploaded new achievement - AchievementCreated::dispatch(Achievement::find($idInOut)); - - return true; - } + $achievement = new Achievement(); + $achievement->GameID = $gameID; + $achievement->Title = $title; + $achievement->Description = $desc; + $achievement->MemAddr = $mem; + $achievement->Points = $points; + $achievement->Flags = $flag; + $achievement->type = ($typeValue == 'NULL') ? null : $type; + $achievement->Author = $author; + $achievement->BadgeName = $badge; + + $achievement->save(); + $idInOut = $achievement->ID; + postActivity($author, ActivityType::UploadAchievement, $idInOut); + + static_addnewachievement($idInOut); + addArticleComment( + "Server", + ArticleType::Achievement, + $idInOut, + "$author uploaded this achievement.", + $author + ); + + // uploaded new achievement + AchievementCreated::dispatch($achievement); - // failed - return false; + return true; } + // Achievement being updated - $query = "SELECT Flags, type, MemAddr, Points, Title, Description, BadgeName, Author FROM Achievements WHERE ID='$idInOut'"; - $dbResult = s_mysql_query($query); - if ($dbResult !== false && mysqli_num_rows($dbResult) == 1) { - $data = mysqli_fetch_assoc($dbResult); + $achievement = Achievement::find($idInOut); + if ($achievement) { + $fields = []; + + $changingPoints = ($achievement->Points != $points); + if ($changingPoints) { + $achievement->Points = $points; + $fields[] = "points"; + } + + if ($achievement->BadgeName !== $badge) { + $achievement->BadgeName = $badge; + $fields[] = "badge"; + } + + if ($achievement->Title !== $title) { + $achievement->Title = $title; + $fields[] = "title"; + } + + if ($achievement->Description !== $desc) { + $achievement->Description = $desc; + $fields[] = "description"; + } + + $changingType = ($achievement->type != $type && $type !== 'not-given'); + if ($changingType) { + $achievement->type = $type; + $fields[] = "type"; + } - $changingAchSet = ($data['Flags'] != $flag); - $changingType = ($data['type'] != $type && $type !== 'not-given'); - $changingPoints = ($data['Points'] != $points); - $changingTitle = ($data['Title'] !== $rawTitle); - $changingDescription = ($data['Description'] !== $rawDesc); - $changingBadge = ($data['BadgeName'] !== $badge); - $changingLogic = ($data['MemAddr'] != $mem); + $changingLogic = ($achievement->MemAddr != $mem); + if ($changingLogic) { + $achievement->MemAddr = $mem; + $fields[] = "logic"; + } + + $changingAchSet = ($achievement->Flags != $flag); + if ($changingAchSet) { + $achievement->Flags = $flag; + } if ($flag === AchievementFlag::OfficialCore || $changingAchSet) { // If modifying core or changing achievement state // changing ach set detected; user is $author, permissions is $userPermissions, target set is $flag // Only allow jr. devs to modify core achievements if they are the author and not updating logic or state - if ($userPermissions < Permissions::Developer && ($changingLogic || $changingAchSet || $data['Author'] !== $author)) { + if ($userPermissions < Permissions::Developer && ($changingLogic || $changingAchSet || $achievement->Author !== $author)) { // Must be developer to modify core logic! $errorOut = "You must be a developer to perform this action! Please drop a message in the forums to apply."; @@ -296,42 +308,21 @@ function UploadNewAchievement( if ($flag === AchievementFlag::Unofficial) { // If modifying unofficial // Only allow jr. devs to modify unofficial if they are the author - if ($userPermissions == Permissions::JuniorDeveloper && $data['Author'] !== $author) { + if ($userPermissions == Permissions::JuniorDeveloper && $achievement->Author !== $author) { $errorOut = "You must be a developer to perform this action! Please drop a message in the forums to apply."; return false; } } - // `null` is a valid type value, so we use a different fallback value. - if ($type === 'not-given' && $data['type'] !== null) { - $typeValue = "'" . $data['type'] . "'"; - } - - $query = "UPDATE Achievements SET Title='$title', Description='$desc', Progress='$progress', ProgressMax='$progressMax', ProgressFormat='$progressFmt', MemAddr='$mem', Points=$points, Flags=$flag, type=$typeValue, DateModified=NOW(), Updated=NOW(), BadgeName='$badge' WHERE ID=$idInOut"; - - $db = getMysqliConnection(); - if (mysqli_query($db, $query) !== false) { - // if ($changingAchSet || $changingPoints) { - // // When changing achievement set, all existing achievements that rely on this should be purged. - // // $query = "DELETE FROM Awarded WHERE ID='$idInOut'"; - // // nah, that's a bit harsh... esp if you're changing something tiny like the badge!! - // - // // if (s_mysql_query($query) !== false) { - // // $rowsAffected = mysqli_affected_rows($db); - // // // great - // // } else { - // // //meh - // // } - // } + if ($achievement->isDirty()) { + $achievement->save(); static_setlastupdatedgame($gameID); static_setlastupdatedachievement($idInOut); postActivity($author, ActivityType::EditAchievement, $idInOut); - $achievement = Achievement::find($idInOut); - if ($changingAchSet) { if ($flag === AchievementFlag::OfficialCore) { addArticleComment( @@ -354,25 +345,6 @@ function UploadNewAchievement( } expireGameTopAchievers($gameID); } else { - $fields = []; - if ($changingPoints) { - $fields[] = "points"; - } - if ($changingBadge) { - $fields[] = "badge"; - } - if ($changingLogic) { - $fields[] = "logic"; - } - if ($changingTitle) { - $fields[] = "title"; - } - if ($changingDescription) { - $fields[] = "description"; - } - if ($changingType) { - $fields[] = "type"; - } $editString = implode(', ', $fields); if (!empty($editString)) { @@ -392,12 +364,9 @@ function UploadNewAchievement( if ($changingType) { AchievementTypeChanged::dispatch($achievement); } - - return true; } - log_sql_fail(); - return false; + return true; } return false; diff --git a/app/Helpers/database/static.php b/app/Helpers/database/static.php index 53e10dd1ce..c510592436 100644 --- a/app/Helpers/database/static.php +++ b/app/Helpers/database/static.php @@ -8,12 +8,9 @@ */ function static_addnewachievement(int $id): void { - $query = "UPDATE StaticData AS sd "; - $query .= "SET sd.NumAchievements=sd.NumAchievements+1, sd.LastCreatedAchievementID='$id'"; - $dbResult = s_mysql_query($query); - if (!$dbResult) { - log_sql_fail(); - } + $query = "UPDATE StaticData "; + $query .= "SET NumAchievements=NumAchievements+1, LastCreatedAchievementID=$id"; + legacyDbStatement($query); } /** @@ -113,12 +110,8 @@ function static_setlastearnedachievement(int $id, string $user, int $points): vo */ function static_setlastupdatedgame(int $id): void { - $query = "UPDATE StaticData AS sd "; - $query .= "SET sd.LastUpdatedGameID = '$id'"; - $dbResult = s_mysql_query($query); - if (!$dbResult) { - log_sql_fail(); - } + $query = "UPDATE StaticData SET LastUpdatedGameID = $id"; + legacyDbStatement($query); } /** @@ -126,10 +119,6 @@ function static_setlastupdatedgame(int $id): void */ function static_setlastupdatedachievement(int $id): void { - $query = "UPDATE StaticData AS sd "; - $query .= "SET sd.LastUpdatedAchievementID = '$id'"; - $dbResult = s_mysql_query($query); - if (!$dbResult) { - log_sql_fail(); - } + $query = "UPDATE StaticData SET LastUpdatedAchievementID = $id"; + legacyDbStatement($query); } diff --git a/app/Helpers/database/user-activity.php b/app/Helpers/database/user-activity.php index 7d6ed5a479..7119e61cc5 100644 --- a/app/Helpers/database/user-activity.php +++ b/app/Helpers/database/user-activity.php @@ -169,8 +169,6 @@ function addArticleComment( return false; } - sanitize_sql_inputs($commentPayload); - // Note: $user is the person who just made a comment. $userID = getUserIDFromUser($user); @@ -183,33 +181,27 @@ function addArticleComment( return true; } - // Replace all single quotes with double quotes (to work with MYSQL DB) - // $commentPayload = str_replace( "'", "''", $commentPayload ); - if (is_array($articleID)) { + $bindings = []; + $articleIDs = $articleID; $arrayCount = count($articleID); $count = 0; $query = "INSERT INTO Comment (ArticleType, ArticleID, UserID, Payload) VALUES"; foreach ($articleID as $id) { - $query .= "( $articleType, $id, $userID, '$commentPayload' )"; + $bindings['commentPayload' . $count] = $commentPayload; + $query .= "( $articleType, $id, $userID, :commentPayload$count )"; if (++$count !== $arrayCount) { $query .= ","; } } } else { - $query = "INSERT INTO Comment (ArticleType, ArticleID, UserID, Payload) VALUES( $articleType, $articleID, $userID, '$commentPayload' )"; + $query = "INSERT INTO Comment (ArticleType, ArticleID, UserID, Payload) VALUES( $articleType, $articleID, $userID, :commentPayload)"; + $bindings = ['commentPayload' => $commentPayload]; $articleIDs = [$articleID]; } - $db = getMysqliConnection(); - $dbResult = mysqli_query($db, $query); - - if (!$dbResult) { - log_sql_fail(); - - return false; - } + legacyDbStatement($query, $bindings); // Inform Subscribers of this comment: foreach ($articleIDs as $id) { diff --git a/app/Helpers/database/user-permission.php b/app/Helpers/database/user-permission.php index 70506b41a5..60d165068e 100644 --- a/app/Helpers/database/user-permission.php +++ b/app/Helpers/database/user-permission.php @@ -9,19 +9,10 @@ function getUserPermissions(?string $user): int return 0; } - sanitize_sql_inputs($user); - - $query = "SELECT Permissions FROM UserAccounts WHERE User='$user'"; - $dbResult = s_mysql_query($query); - if (!$dbResult) { - log_sql_fail(); - - return 0; - } - - $data = mysqli_fetch_assoc($dbResult); + $query = "SELECT Permissions FROM UserAccounts WHERE User=:user"; + $row = legacyDbFetch($query, ['user' => $user]); - return (int) $data['Permissions']; + return $row ? (int) $row['Permissions'] : Permissions::Unregistered; } function SetAccountPermissionsJSON( diff --git a/app/Helpers/database/user.php b/app/Helpers/database/user.php index 814cea4b62..59f2b59cb2 100644 --- a/app/Helpers/database/user.php +++ b/app/Helpers/database/user.php @@ -45,19 +45,10 @@ function getUserIDFromUser(?string $user): int return 0; } - sanitize_sql_inputs($user); - - $query = "SELECT ID FROM UserAccounts WHERE User LIKE '$user'"; - $dbResult = s_mysql_query($query); - - if ($dbResult !== false) { - $data = mysqli_fetch_assoc($dbResult); - - return (int) ($data['ID'] ?? 0); - } + $query = "SELECT ID FROM UserAccounts WHERE User = :user"; + $row = legacyDbFetch($query, ['user' => $user]); - // cannot find user $user - return 0; + return $row ? (int) $row['ID'] : 0; } function getUserMetadataFromID(int $userID): ?array diff --git a/app/Platform/EventServiceProvider.php b/app/Platform/EventServiceProvider.php index 0b8b02b5d1..6efc0ef20f 100755 --- a/app/Platform/EventServiceProvider.php +++ b/app/Platform/EventServiceProvider.php @@ -22,6 +22,7 @@ use App\Platform\Events\PlayerMetricsUpdated; use App\Platform\Events\PlayerRankedStatusChanged; use App\Platform\Events\PlayerSessionHeartbeat; +// use App\Platform\Listeners\DispatchUpdateDeveloperContributionYieldJob; use App\Platform\Listeners\DispatchUpdateGameMetricsJob; use App\Platform\Listeners\DispatchUpdatePlayerGameMetricsJob; use App\Platform\Listeners\DispatchUpdatePlayerMetricsJob; @@ -34,6 +35,7 @@ class EventServiceProvider extends ServiceProvider { protected $listen = [ AchievementCreated::class => [ + DispatchUpdateGameMetricsJob::class, // dispatches GameMetricsUpdated ], AchievementPublished::class => [ DispatchUpdateGameMetricsJob::class, // dispatches GameMetricsUpdated diff --git a/app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php b/app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php index e8433151ee..ab5b1a7241 100644 --- a/app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php +++ b/app/Platform/Listeners/DispatchUpdateDeveloperContributionYieldJob.php @@ -17,16 +17,16 @@ public function handle(object $event): void $user = null; switch ($event::class) { - // TODO case AchievementPublished::class: - // $achievement = $event->achievement; - // $achievement->loadMissing('developer'); - // $user = $achievement->developer; - // break; - // TODO case AchievementUnpublished::class: - // $achievement = $event->achievement; - // $achievement->loadMissing('developer'); - // $user = $achievement->developer; - // break; + case AchievementPublished::class: + $achievement = $event->achievement; + $achievement->loadMissing('developer'); + $user = $achievement->developer; + break; + case AchievementUnpublished::class: + $achievement = $event->achievement; + $achievement->loadMissing('developer'); + $user = $achievement->developer; + break; case AchievementPointsChanged::class: $achievement = $event->achievement; $achievement->loadMissing('developer'); diff --git a/app/Platform/Listeners/DispatchUpdateGameMetricsJob.php b/app/Platform/Listeners/DispatchUpdateGameMetricsJob.php index af9edcb98f..8de9eeabf9 100644 --- a/app/Platform/Listeners/DispatchUpdateGameMetricsJob.php +++ b/app/Platform/Listeners/DispatchUpdateGameMetricsJob.php @@ -2,6 +2,7 @@ namespace App\Platform\Listeners; +use App\Platform\Events\AchievementCreated; use App\Platform\Events\AchievementPointsChanged; use App\Platform\Events\AchievementPublished; use App\Platform\Events\AchievementTypeChanged; @@ -35,6 +36,10 @@ public function handle(object $event): void $achievement = $event->achievement; $game = $achievement->game; break; + case AchievementCreated::class: + $achievement = $event->achievement; + $game = $achievement->game; + break; case PlayerGameMetricsUpdated::class: $game = $event->game; break; diff --git a/tests/Feature/Connect/UploadAchievementTest.php b/tests/Feature/Connect/UploadAchievementTest.php new file mode 100644 index 0000000000..4c52d3b48a --- /dev/null +++ b/tests/Feature/Connect/UploadAchievementTest.php @@ -0,0 +1,904 @@ +create([ + 'Permissions' => Permissions::Developer, + 'appToken' => Str::random(16), + 'ContribCount' => 0, + 'ContribYield' => 0, + ]); + $game = $this->seedGame(withHash: false); + + /** @var Achievement $achievement1 */ + $achievement1 = Achievement::factory()->create(['GameID' => $game->ID + 1, 'Author' => $author->User]); + + AchievementSetClaim::factory()->create(['User' => $author->User, 'GameID' => $game->ID]); + + $params = [ + 'u' => $author->User, + 't' => $author->appToken, + 'g' => $game->ID, + 'n' => 'Title1', + 'd' => 'Description1', + 'z' => 5, + 'm' => '0xH0000=1', + 'f' => 5, // Unofficial - hardcode for test to prevent false success if enum changes + 'b' => '001234', + ]; + + // ==================================================== + // create an achievement + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID + 1, + 'Error' => '', + ]); + + /** @var Achievement $achievement2 */ + $achievement2 = Achievement::findOrFail($achievement1->ID + 1); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title1'); + $this->assertEquals($achievement2->MemAddr, '0xH0000=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertNull($achievement2->user_id); + $this->assertEquals($achievement2->BadgeName, '001234'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 0); + $this->assertEquals($game->achievements_unpublished, 1); + $this->assertEquals($game->points_total, 0); + + // ==================================================== + // publish achievement + $params['a'] = $achievement2->ID; + $params['f'] = 3; // Official - hardcode for test to prevent false success if enum changes + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title1'); + $this->assertEquals($achievement2->MemAddr, '0xH0000=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '001234'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 1); + $this->assertEquals($game->achievements_unpublished, 0); + $this->assertEquals($game->points_total, 5); + + // ==================================================== + // modify achievement + $params['n'] = 'Title2'; + $params['d'] = 'Description2'; + $params['z'] = 10; + $params['m'] = '0xH0001=1'; + $params['b'] = '002345'; + $params['x'] = 'progression'; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 1); + $this->assertEquals($game->achievements_unpublished, 0); + $this->assertEquals($game->points_total, 10); + + // ==================================================== + // unlock achievement; contrib yield changes + $this->addHardcoreUnlock($author, $achievement2); + $this->addHardcoreUnlock($this->user, $achievement2); + + $author->refresh(); + // When DispatchUpdateDeveloperContributionYieldJob is enabled, update all + // of the TODO blocks in this file to expect the contribution changes. + $this->assertEquals($author->ContribCount, 0); + /* TODO + $this->assertEquals($author->ContribCount, 1); + $this->assertEquals($author->ContribYield, 10); + */ + + $game->refresh(); + $this->assertEquals($game->players_total, 2); + $this->assertEquals($game->players_hardcore, 2); + + // ==================================================== + // rescore achievement; contrib yield changes + $params['z'] = 5; + unset($params['x']); // ommitting optional 'x' parameter should not change type + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 1); + $this->assertEquals($game->achievements_unpublished, 0); + $this->assertEquals($game->points_total, 5); + $this->assertEquals($game->players_total, 2); + $this->assertEquals($game->players_hardcore, 2); + + $author->refresh(); + /* TODO + $this->assertEquals($author->ContribCount, 1); + $this->assertEquals($author->ContribYield, 5); + */ + + // ==================================================== + // demote achievement; contrib yield changes + $params['f'] = 5; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 0); + $this->assertEquals($game->achievements_unpublished, 1); + $this->assertEquals($game->points_total, 0); + $this->assertEquals($game->players_total, 0); + $this->assertEquals($game->players_hardcore, 0); + + $author->refresh(); + $this->assertEquals($author->ContribCount, 0); + $this->assertEquals($author->ContribYield, 0); + + // ==================================================== + // change points while demoted + $params['z'] = 10; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 0); + $this->assertEquals($game->achievements_unpublished, 1); + $this->assertEquals($game->points_total, 0); + $this->assertEquals($game->players_total, 0); + $this->assertEquals($game->players_hardcore, 0); + + $author->refresh(); + $this->assertEquals($author->ContribCount, 0); + $this->assertEquals($author->ContribYield, 0); + + // ==================================================== + // repromote achievement; contrib yield changes + $params['f'] = 3; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 1); + $this->assertEquals($game->achievements_unpublished, 0); + $this->assertEquals($game->points_total, 10); + $this->assertEquals($game->players_total, 2); + $this->assertEquals($game->players_hardcore, 2); + + $author->refresh(); + /* TODO + $this->assertEquals($author->ContribCount, 1); + $this->assertEquals($author->ContribYield, 10); + */ + } + + public function testNonDevPermissions(): void + { + /** @var User $author */ + $author = User::factory()->create([ + 'Permissions' => Permissions::Registered, + 'appToken' => Str::random(16), + ]); + $game = $this->seedGame(withHash: false); + + /** @var Achievement $achievement1 */ + $achievement1 = Achievement::factory()->create(['GameID' => $game->ID, 'Author' => $author->User]); + + $params = [ + 'u' => $author->User, + 't' => $author->appToken, + 'g' => $game->ID, + 'n' => 'Title1', + 'd' => 'Description1', + 'z' => 5, + 'm' => '0xH0000=1', + 'f' => 5, // Unofficial - hardcode for test to prevent false success if enum changes + 'b' => '001234', + ]; + + // ==================================================== + // non-developer cannot create achievements + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => 0, + 'Error' => "You must be a developer to perform this action! Please drop a message in the forums to apply.", + ]); + } + + public function testJrDevPermissions(): void + { + /** @var User $author */ + $author = User::factory()->create([ + 'Permissions' => Permissions::JuniorDeveloper, + 'appToken' => Str::random(16), + ]); + $game = $this->seedGame(withHash: false); + + /** @var Achievement $achievement1 */ + $achievement1 = Achievement::factory()->create(['GameID' => $game->ID, 'Author' => $this->user->User]); + + $params = [ + 'u' => $author->User, + 't' => $author->appToken, + 'g' => $game->ID, + 'n' => 'Title1', + 'd' => 'Description1', + 'z' => 5, + 'm' => '0xH0000=1', + 'f' => 5, // Unofficial - hardcode for test to prevent false success if enum changes + 'b' => '001234', + ]; + + // ==================================================== + // junior developer cannot create achievement without claim + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => 0, + 'Error' => "You must have an active claim on this game to perform this action.", + ]); + + // ==================================================== + // junior developer can create achievement with claim + AchievementSetClaim::factory()->create(['User' => $author->User, 'GameID' => $game->ID]); + + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID + 1, + 'Error' => '', + ]); + + /** @var Achievement $achievement2 */ + $achievement2 = Achievement::findOrFail($achievement1->ID + 1); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title1'); + $this->assertEquals($achievement2->MemAddr, '0xH0000=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '001234'); + + // ==================================================== + // junior developer can modify their own achievement + $params['a'] = $achievement2->ID; + $params['n'] = 'Title2'; + $params['d'] = 'Description2'; + $params['z'] = 10; + $params['m'] = '0xH0001=1'; + $params['b'] = '002345'; + $params['x'] = 'progression'; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID + 1, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + // ==================================================== + // junior developer cannot modify an achievement owned by someone else + $params['a'] = $achievement1->ID; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => $achievement1->ID, + 'Error' => 'You must be a developer to perform this action! Please drop a message in the forums to apply.', + ]); + + $achievement1->refresh(); + $this->assertNotEquals($achievement1->Title, 'Title2'); + $this->assertNotEquals($achievement1->MemAddr, '0xH0001=1'); + $this->assertNotEquals($achievement1->Points, 10); + $this->assertEquals($achievement1->Flags, AchievementFlag::Unofficial); + $this->assertNotEquals($achievement1->type, 'progression'); + $this->assertNotEquals($achievement1->Author, $author->User); + $this->assertNotEquals($achievement1->BadgeName, '002345'); + + // ==================================================== + // junior developer cannot promote their own achievement + $params['a'] = $achievement2->ID; + $params['f'] = 3; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => $achievement2->ID, + 'Error' => 'You must be a developer to perform this action! Please drop a message in the forums to apply.', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + // ==================================================== + // junior developer cannot demote their own achievement + $achievement2->Flags = AchievementFlag::OfficialCore; + $achievement2->save(); + $params['f'] = 5; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => $achievement2->ID, + 'Error' => 'You must be a developer to perform this action! Please drop a message in the forums to apply.', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + // ==================================================== + // junior developer cannot change logic of their own achievement in core + $params['f'] = 3; + $params['m'] = '0xH0002=1'; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => $achievement2->ID, + 'Error' => 'You must be a developer to perform this action! Please drop a message in the forums to apply.', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + // ==================================================== + // junior developer can change all non-logic of their own achievement in core + $params['n'] = 'Title3'; + $params['d'] = 'Description3'; + $params['z'] = 5; + $params['m'] = '0xH0001=1'; + $params['b'] = '003456'; + $params['x'] = ''; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title3'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '003456'); + } + + public function testDevPermissions(): void + { + /** @var User $author */ + $author = User::factory()->create([ + 'Permissions' => Permissions::Developer, + 'appToken' => Str::random(16), + ]); + $game = $this->seedGame(withHash: false); + + /** @var Achievement $achievement1 */ + $achievement1 = Achievement::factory()->create(['GameID' => $game->ID, 'Author' => $this->user->User]); + + $params = [ + 'u' => $author->User, + 't' => $author->appToken, + 'g' => $game->ID, + 'n' => 'Title1', + 'd' => 'Description1', + 'z' => 5, + 'm' => '0xH0000=1', + 'f' => 5, // Unofficial - hardcode for test to prevent false success if enum changes + 'b' => '001234', + ]; + + // ==================================================== + // developer cannot create achievement without claim + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => 0, + 'Error' => "You must have an active claim on this game to perform this action.", + ]); + + // ==================================================== + // developer can create achievement with claim + AchievementSetClaim::factory()->create(['User' => $author->User, 'GameID' => $game->ID]); + + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID + 1, + 'Error' => '', + ]); + + /** @var Achievement $achievement2 */ + $achievement2 = Achievement::findOrFail($achievement1->ID + 1); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title1'); + $this->assertEquals($achievement2->MemAddr, '0xH0000=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '001234'); + + // ==================================================== + // developer can modify their own achievement + $params['a'] = $achievement2->ID; + $params['n'] = 'Title2'; + $params['d'] = 'Description2'; + $params['z'] = 10; + $params['m'] = '0xH0001=1'; + $params['b'] = '002345'; + $params['x'] = 'progression'; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID + 1, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + // ==================================================== + // developer can promote their own achievement + $params['f'] = 3; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + // ==================================================== + // developer can change all properties of their own achievement in core + $params['n'] = 'Title3'; + $params['d'] = 'Description3'; + $params['z'] = 5; + $params['m'] = '0xH0002=1'; + $params['b'] = '003456'; + $params['x'] = ''; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title3'); + $this->assertEquals($achievement2->MemAddr, '0xH0002=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::OfficialCore); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '003456'); + + // ==================================================== + // developer can demote their own achievement + $params['f'] = 5; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title3'); + $this->assertEquals($achievement2->MemAddr, '0xH0002=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '003456'); + + // ==================================================== + // developer can modify an achievement owned by someone else + $params['a'] = $achievement1->ID; + $params['n'] = 'Title2'; + $params['d'] = 'Description2'; + $params['z'] = 10; + $params['m'] = '0xH0001=1'; + $params['b'] = '002345'; + $params['x'] = 'progression'; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID, + 'Error' => '', + ]); + + $achievement1->refresh(); + $this->assertEquals($achievement1->Title, 'Title2'); + $this->assertEquals($achievement1->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement1->Points, 10); + $this->assertEquals($achievement1->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement1->type, 'progression'); + $this->assertEquals($achievement1->Author, $this->user->User); + $this->assertEquals($achievement1->BadgeName, '002345'); + + // ==================================================== + // developer can promote someone else's achievement + $params['f'] = 3; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID, + 'Error' => '', + ]); + + $achievement1->refresh(); + $this->assertEquals($achievement1->GameID, $game->ID); + $this->assertEquals($achievement1->Title, 'Title2'); + $this->assertEquals($achievement1->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement1->Points, 10); + $this->assertEquals($achievement1->Flags, AchievementFlag::OfficialCore); + $this->assertEquals($achievement1->type, 'progression'); + $this->assertEquals($achievement1->Author, $this->user->User); + $this->assertEquals($achievement1->BadgeName, '002345'); + + // ==================================================== + // developer can change all properties of someone else's achievement in core + $params['n'] = 'Title3'; + $params['d'] = 'Description3'; + $params['z'] = 5; + $params['m'] = '0xH0002=1'; + $params['b'] = '003456'; + $params['x'] = ''; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID, + 'Error' => '', + ]); + + $achievement1->refresh(); + $this->assertEquals($achievement1->GameID, $game->ID); + $this->assertEquals($achievement1->Title, 'Title3'); + $this->assertEquals($achievement1->MemAddr, '0xH0002=1'); + $this->assertEquals($achievement1->Points, 5); + $this->assertEquals($achievement1->Flags, AchievementFlag::OfficialCore); + $this->assertNull($achievement1->type); + $this->assertEquals($achievement1->Author, $this->user->User); + $this->assertEquals($achievement1->BadgeName, '003456'); + + // ==================================================== + // developer can demote someone else's achievement + $params['f'] = 5; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID, + 'Error' => '', + ]); + + $achievement1->refresh(); + $this->assertEquals($achievement1->GameID, $game->ID); + $this->assertEquals($achievement1->Title, 'Title3'); + $this->assertEquals($achievement1->MemAddr, '0xH0002=1'); + $this->assertEquals($achievement1->Points, 5); + $this->assertEquals($achievement1->Flags, AchievementFlag::Unofficial); + $this->assertNull($achievement1->type); + $this->assertEquals($achievement1->Author, $this->user->User); + $this->assertEquals($achievement1->BadgeName, '003456'); + } + + public function testRolloutConsole(): void + { + /** @var User $author */ + $author = User::factory()->create([ + 'Permissions' => Permissions::Developer, + 'appToken' => Str::random(16), + ]); + /** @var System $system */ + $system = System::factory()->create(['ID' => 500]); + $game = $this->seedGame(system: $system, withHash: false); + + AchievementSetClaim::factory()->create(['User' => $author->User, 'GameID' => $game->ID]); + + /** @var Achievement $achievement1 */ + $achievement1 = Achievement::factory()->create(['GameID' => $game->ID + 1, 'Author' => $author->User]); + + $params = [ + 'u' => $author->User, + 't' => $author->appToken, + 'g' => $game->ID, + 'n' => 'Title1', + 'd' => 'Description1', + 'z' => 5, + 'm' => '0xH0000=1', + 'f' => 5, + 'b' => '001234', + ]; + + // ==================================================== + // can upload to unofficial + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement1->ID + 1, + 'Error' => '', + ]); + + /** @var Achievement $achievement2 */ + $achievement2 = Achievement::findOrFail($achievement1->ID + 1); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title1'); + $this->assertEquals($achievement2->MemAddr, '0xH0000=1'); + $this->assertEquals($achievement2->Points, 5); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertNull($achievement2->type); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertNull($achievement2->user_id); + $this->assertEquals($achievement2->BadgeName, '001234'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 0); + $this->assertEquals($game->achievements_unpublished, 1); + $this->assertEquals($game->points_total, 0); + + // ==================================================== + // can modify in unofficial + $params['a'] = $achievement2->ID; + $params['n'] = 'Title2'; + $params['d'] = 'Description2'; + $params['z'] = 10; + $params['m'] = '0xH0001=1'; + $params['b'] = '002345'; + $params['x'] = 'progression'; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => true, + 'AchievementID' => $achievement2->ID, + 'Error' => '', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + + $game->refresh(); + $this->assertEquals($game->achievements_published, 0); + $this->assertEquals($game->achievements_unpublished, 1); + $this->assertEquals($game->points_total, 0); + + // ==================================================== + // cannot promote for rollout console + $params['f'] = 3; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => $achievement2->ID, + 'Error' => 'You cannot promote achievements for a game from an unsupported console (console ID: 500).', + ]); + + $achievement2->refresh(); + $this->assertEquals($achievement2->GameID, $game->ID); + $this->assertEquals($achievement2->Title, 'Title2'); + $this->assertEquals($achievement2->MemAddr, '0xH0001=1'); + $this->assertEquals($achievement2->Points, 10); + $this->assertEquals($achievement2->Flags, AchievementFlag::Unofficial); + $this->assertEquals($achievement2->type, 'progression'); + $this->assertEquals($achievement2->Author, $author->User); + $this->assertEquals($achievement2->BadgeName, '002345'); + } + + public function testOtherErrors(): void + { + /** @var User $author */ + $author = User::factory()->create([ + 'Permissions' => Permissions::Developer, + 'appToken' => Str::random(16), + ]); + $game = $this->seedGame(withHash: false); + + AchievementSetClaim::factory()->create(['User' => $author->User, 'GameID' => $game->ID]); + + $params = [ + 'u' => $author->User, + 't' => $author->appToken, + 'g' => $game->ID, + 'n' => 'Title1', + 'd' => 'Description1', + 'z' => 5, + 'm' => '0xH0000=1', + 'f' => 5, + 'b' => '001234', + ]; + + // ==================================================== + // invalid flag + $params['f'] = 4; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => 0, + 'Error' => 'Invalid achievement flag', + ]); + + // ==================================================== + // invalid points + $params['f'] = 5; + $params['z'] = 15; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => 0, + 'Error' => 'Invalid points value (15).', + ]); + + // ==================================================== + // invalid type + $params['z'] = 10; + $params['x'] = 'unknown'; + $this->get($this->apiUrl('uploadachievement', $params)) + ->assertExactJson([ + 'Success' => false, + 'AchievementID' => 0, + 'Error' => 'Invalid achievement type', + ]); + } +} From 1e762a2ebb1615f4c97c1c00e74aeef51de84418 Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Fri, 27 Oct 2023 10:17:32 -0600 Subject: [PATCH 85/92] populate as much of Recent Players list as possible from aggregate data (#1932) --- app/Helpers/database/player-game.php | 40 +++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/app/Helpers/database/player-game.php b/app/Helpers/database/player-game.php index adfbb62e93..9080eef86a 100644 --- a/app/Helpers/database/player-game.php +++ b/app/Helpers/database/player-game.php @@ -5,6 +5,7 @@ use App\Platform\Enums\UnlockMode; use App\Platform\Models\Achievement; use App\Platform\Models\PlayerAchievement; +use App\Platform\Models\PlayerSession; use App\Site\Enums\Permissions; use App\Site\Models\User; use App\Support\Cache\CacheKey; @@ -698,26 +699,51 @@ function getTotalUniquePlayers( return (int) (legacyDbFetch($query, $bindings)['players_count'] ?? 0); } -function getGameRecentPlayers(int $gameID, int $maximum_results = 0): array +function getGameRecentPlayers(int $gameID, int $maximum_results = 10): array { $retval = []; + $sessions = PlayerSession::where('game_id', $gameID) + ->join('UserAccounts', 'UserAccounts.ID', '=', 'user_id') + ->where('UserAccounts.Permissions', '>=', Permissions::Unregistered) + ->whereNotNull('rich_presence') + ->orderBy('rich_presence_updated_at', 'DESC') + ->groupBy('user_id') + ->select(['user_id', 'User', 'rich_presence', DB::raw('MAX(rich_presence_updated_at) AS rich_presence_updated_at')]); + + if ($maximum_results) { + $sessions = $sessions->limit($maximum_results); + } + + foreach ($sessions->get() as $session) { + $retval[] = [ + 'UserID' => $session->user_id, + 'User' => $session->User, + 'Date' => $session->rich_presence_updated_at->__toString(), + 'Activity' => $session->rich_presence, + ]; + } + + if ($maximum_results) { + $maximum_results -= count($retval); + if ($maximum_results == 0) { + return $retval; + } + } + $query = "SELECT ua.ID as UserID, ua.User, ua.RichPresenceMsgDate AS Date, ua.RichPresenceMsg AS Activity FROM UserAccounts AS ua WHERE ua.LastGameID = $gameID AND ua.Permissions >= " . Permissions::Unregistered . " AND ua.RichPresenceMsgDate > TIMESTAMPADD(MONTH, -6, NOW()) + AND ua.ID NOT IN (" . implode(',', array_column($retval, 'UserID')) . ") ORDER BY ua.RichPresenceMsgDate DESC"; if ($maximum_results > 0) { $query .= " LIMIT $maximum_results"; } - $dbResult = s_mysql_query($query); - - if ($dbResult !== false) { - while ($data = mysqli_fetch_assoc($dbResult)) { - $retval[] = $data; - } + foreach (legacyDbFetchAll($query) as $data) { + $retval[] = $data; } return $retval; From 50fc146b133eee4e9387778332cf61f0f90394f5 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Fri, 27 Oct 2023 10:23:03 -0600 Subject: [PATCH 86/92] fix error on page with no player_sessions --- app/Helpers/database/player-game.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/Helpers/database/player-game.php b/app/Helpers/database/player-game.php index 9080eef86a..f6b2538216 100644 --- a/app/Helpers/database/player-game.php +++ b/app/Helpers/database/player-game.php @@ -731,11 +731,15 @@ function getGameRecentPlayers(int $gameID, int $maximum_results = 10): array } } + $userFilter = ''; + if (count($retval)) { + $userFilter = 'AND ua.ID NOT IN (' . implode(',', array_column($retval, 'UserID')) . ')'; + } + $query = "SELECT ua.ID as UserID, ua.User, ua.RichPresenceMsgDate AS Date, ua.RichPresenceMsg AS Activity FROM UserAccounts AS ua WHERE ua.LastGameID = $gameID AND ua.Permissions >= " . Permissions::Unregistered . " - AND ua.RichPresenceMsgDate > TIMESTAMPADD(MONTH, -6, NOW()) - AND ua.ID NOT IN (" . implode(',', array_column($retval, 'UserID')) . ") + AND ua.RichPresenceMsgDate > TIMESTAMPADD(MONTH, -6, NOW()) $userFilter ORDER BY ua.RichPresenceMsgDate DESC"; if ($maximum_results > 0) { From daadc026f1a04ccb50266de2c0791a0449535b5e Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Fri, 27 Oct 2023 11:10:34 -0600 Subject: [PATCH 87/92] Add manual unlock buttons to ticket page (#1929) --- app/Helpers/database/player-achievement.php | 35 +---- app/Helpers/database/user.php | 32 ---- .../Actions/UpdatePlayerGameMetrics.php | 5 +- lang/en_US/legacy.php | 1 + public/admin.php | 7 +- public/dorequest.php | 2 +- public/request/user/award-achievement.php | 44 ++++++ public/ticketmanager.php | 138 ++++++++++++++---- 8 files changed, 165 insertions(+), 99 deletions(-) create mode 100644 public/request/user/award-achievement.php diff --git a/app/Helpers/database/player-achievement.php b/app/Helpers/database/player-achievement.php index 3d5e886066..7a5a4a436e 100644 --- a/app/Helpers/database/player-achievement.php +++ b/app/Helpers/database/player-achievement.php @@ -47,19 +47,12 @@ function playerHasUnlock(?string $user, int $achievementId): array /** * @deprecated see UnlockPlayerAchievementAction */ -function unlockAchievement(string $username, int $achievementId, bool $isHardcore): array +function unlockAchievement(User $user, int $achievementId, bool $isHardcore): array { $retVal = [ 'Success' => false, ]; - $user = User::firstWhere('User', $username); - if (!$user) { - $retVal['Error'] = "Data not found for user $username"; - - return $retVal; - } - $achievement = Achievement::find($achievementId); if (!$achievement) { $retVal['Error'] = "Data not found for achievement $achievementId"; @@ -285,28 +278,14 @@ function getRecentUnlocksPlayersData( */ function getUnlocksSince(int $id, string $date): array { - sanitize_sql_inputs($date); - - $query = " - SELECT - COALESCE(SUM(CASE WHEN HardcoreMode = " . UnlockMode::Softcore . " THEN 1 ELSE 0 END), 0) AS softcoreCount, - COALESCE(SUM(CASE WHEN HardcoreMode = " . UnlockMode::Hardcore . " THEN 1 ELSE 0 END), 0) AS hardcoreCount - FROM - Awarded - WHERE - AchievementID = $id - AND - Date > '$date'"; - - $dbResult = s_mysql_query($query); - - if ($dbResult !== false) { - return mysqli_fetch_assoc($dbResult); - } + $softcoreCount = PlayerAchievement::where('achievement_id', $id) + ->where('unlocked_at', '>', $date)->count(); + $hardcoreCount = PlayerAchievement::where('achievement_id', $id) + ->where('unlocked_hardcore_at', '>', $date)->count(); return [ - 'softcoreCount' => 0, - 'hardcoreCount' => 0, + 'softcoreCount' => $softcoreCount, + 'hardcoreCount' => $hardcoreCount, ]; } diff --git a/app/Helpers/database/user.php b/app/Helpers/database/user.php index 59f2b59cb2..c8609a2327 100644 --- a/app/Helpers/database/user.php +++ b/app/Helpers/database/user.php @@ -63,38 +63,6 @@ function getUserMetadataFromID(int $userID): ?array return null; } -function getUserUnlockDates(string $user, int $gameID, ?array &$dataOut): int -{ - sanitize_sql_inputs($user); - - $query = "SELECT ach.ID, ach.Title, ach.Description, ach.Points, ach.BadgeName, aw.HardcoreMode, aw.Date - FROM Achievements ach - INNER JOIN Awarded AS aw ON ach.ID = aw.AchievementID - WHERE ach.GameID = $gameID AND aw.User = '$user' - ORDER BY ach.ID, aw.HardcoreMode DESC"; - - $dbResult = s_mysql_query($query); - - $dataOut = []; - - if (!$dbResult) { - return 0; - } - - $lastID = 0; - while ($data = mysqli_fetch_assoc($dbResult)) { - $achID = $data['ID']; - if ($lastID == $achID) { - continue; - } - - $dataOut[] = $data; - $lastID = $achID; - } - - return count($dataOut); -} - /** * @param array|null $dataOut */ diff --git a/app/Platform/Actions/UpdatePlayerGameMetrics.php b/app/Platform/Actions/UpdatePlayerGameMetrics.php index dba2770014..7693a54619 100644 --- a/app/Platform/Actions/UpdatePlayerGameMetrics.php +++ b/app/Platform/Actions/UpdatePlayerGameMetrics.php @@ -29,6 +29,7 @@ public function execute(PlayerGame $playerGame, bool $silent = false): void $achievementsUnlocked = $user->achievements()->where('GameID', $game->id) ->published() ->withPivot([ + 'unlocker_id', 'unlocked_at', 'unlocked_hardcore_at', ]) @@ -61,8 +62,8 @@ public function execute(PlayerGame $playerGame, bool $silent = false): void ? $playerGame->created_at : $startedAt; - $lastPlayedAt = $playerAchievements->pluck('unlocked_at') - ->merge($playerAchievementsHardcore->pluck('unlocked_hardcore_at')) + $lastPlayedAt = $playerAchievements->whereNull('unlocker_id')->pluck('unlocked_at') + ->merge($playerAchievementsHardcore->whereNull('unlocker_id')->pluck('unlocked_hardcore_at')) ->add($playerGame->last_played_at) ->filter() ->max(); diff --git a/lang/en_US/legacy.php b/lang/en_US/legacy.php index 51b060b12e..c0a80f97e5 100755 --- a/lang/en_US/legacy.php +++ b/lang/en_US/legacy.php @@ -31,6 +31,7 @@ 'submit' => __("Submitted."), 'update' => __("Updated."), + 'achievement_unlocked' => __("Achievement unlocked."), 'achievement_update' => __("Achievement updated."), 'email_change' => __('Email address changed.'), 'email_verify' => __("Email verified."), diff --git a/public/admin.php b/public/admin.php index f7f94d067b..4a15bd3ccc 100644 --- a/public/admin.php +++ b/public/admin.php @@ -61,17 +61,16 @@ $usersToAward = preg_split('/\W+/', $awardAchievementUser); $errors = []; foreach ($usersToAward as $nextUser) { - $validUser = validateUsername($nextUser); - if (!$validUser) { + $player = User::firstWhere('User', $nextUser); + if (!$player) { continue; } $ids = separateList($awardAchievementID); foreach ($ids as $nextID) { - $awardResponse = unlockAchievement($validUser, $nextID, $awardAchHardcore); + $awardResponse = unlockAchievement($player, $nextID, $awardAchHardcore); if (array_key_exists('Error', $awardResponse)) { $errors[] = $awardResponse['Error']; } - $player = User::firstWhere('User', $validUser); dispatch( new UnlockPlayerAchievementJob( $player->id, diff --git a/public/dorequest.php b/public/dorequest.php index e01e82acef..4d3f54d47a 100644 --- a/public/dorequest.php +++ b/public/dorequest.php @@ -267,7 +267,7 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null) * Prefer later values, i.e. allow AddEarnedAchievementJSON to overwrite the 'success' key * TODO refactor to optimistic update without unlock in place. what are the returned values used for? */ - $response = array_merge($response, unlockAchievement($username, $achIDToAward, $hardcore)); + $response = array_merge($response, unlockAchievement($user, $achIDToAward, $hardcore)); if (Achievement::where('ID', $achIDToAward)->exists()) { dispatch(new UnlockPlayerAchievementJob($user->id, $achIDToAward, $hardcore)) diff --git a/public/request/user/award-achievement.php b/public/request/user/award-achievement.php new file mode 100644 index 0000000000..a258b4e3fe --- /dev/null +++ b/public/request/user/award-achievement.php @@ -0,0 +1,44 @@ +user(); +if ($user === null) { + abort(401); +} + +if ($user->getAttribute('Permissions') < Permissions::Moderator) { + abort(403); +} + +$input = Validator::validate(Arr::wrap(request()->post()), [ + 'user' => 'required|string|exists:UserAccounts,User', + 'achievement' => 'required|integer|exists:Achievements,ID', + 'hardcore' => 'required|integer|min:0|max:1', +]); + +$player = User::firstWhere('User', $input['user']); + +$achievementId = $input['achievement']; +$awardHardcore = (bool) $input['hardcore']; + +$awardResponse = unlockAchievement($player, $achievementId, $awardHardcore); +if (array_key_exists('Error', $awardResponse)) { + return response()->json(['error' => $awardResponse['Error']]); +} + +$action = app()->make(UnlockPlayerAchievement::class); + +$action->execute( + $player, + Achievement::findOrFail($achievementId), + $awardHardcore, + unlockedBy: $user, +); + +return response()->json(['message' => __('legacy.success.achievement_unlocked')]); diff --git a/public/ticketmanager.php b/public/ticketmanager.php index fab95f6217..ebf055c3c8 100644 --- a/public/ticketmanager.php +++ b/public/ticketmanager.php @@ -7,7 +7,9 @@ use App\Community\Enums\TicketType; use App\Platform\Enums\AchievementFlag; use App\Platform\Models\Achievement; +use App\Platform\Models\PlayerAchievement; use App\Site\Enums\Permissions; +use App\Site\Models\User; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Str; @@ -533,6 +535,7 @@ $reportNotes = str_replace('
    ', "\n", $nextTicket['ReportNotes']); $reportedAt = $nextTicket['ReportedAt']; + $reportedAtTime = strtotime($reportedAt); $niceReportedAt = getNiceDate(strtotime($reportedAt)); $reportedBy = $nextTicket['ReportedBy']; $resolvedAt = $nextTicket['ResolvedAt']; @@ -773,29 +776,78 @@ } echo ""; - $numAchievements = getUserUnlockDates($reportedBy, $gameID, $unlockData); - $unlockData[] = ['ID' => 0, 'Title' => 'Ticket Created', 'Date' => $reportedAt, 'HardcoreMode' => 0]; - usort($unlockData, fn ($a, $b) => strtotime($b["Date"]) - strtotime($a["Date"])); - - $unlockDate = null; - foreach ($unlockData as $unlockEntry) { - if ($unlockEntry['ID'] == $achID) { - $unlockDate = $unlockEntry['Date']; + $unlocks = PlayerAchievement::join('UserAccounts', 'UserAccounts.ID', '=', 'player_achievements.user_id') + ->join('Achievements', 'Achievements.ID', '=', 'player_achievements.achievement_id') + ->where('GameID', '=', $gameID) + ->where('UserAccounts.User', '=', $reportedBy) + ->orderByRaw('IFNULL(unlocked_hardcore_at, unlocked_at) DESC') + ->select(['player_achievements.*', 'Achievements.Title', + 'Achievements.Description', 'Achievements.Points', 'Achievements.BadgeName']); + $numAchievements = $unlocks->count(); + + $unlocks = $unlocks->get(); + $existingUnlock = null; + foreach ($unlocks as $unlock) { + if ($unlock->achievement_id == $achID) { + $existingUnlock = $unlock; break; } } - if ($unlockDate != null) { - echo "$reportedBy earned this achievement at " . getNiceDate(strtotime($unlockDate)); - if ($unlockDate >= $reportedAt) { - echo " (after the report)."; + if ($existingUnlock != null) { + if ($existingUnlock->unlocked_hardcore_at) { + $unlockDate = $existingUnlock->unlocked_hardcore_at->unix(); + } else { + $unlockDate = $existingUnlock->unlocked_at->unix(); + } + + if ($existingUnlock->unlocker_id) { + $unlocker = User::firstWhere('ID', $existingUnlock->unlocker_id); + echo "$reportedBy was manually awarded this achievement at " . getNiceDate($unlockDate) . " by " . $unlocker->User . "."; } else { - echo " (before the report)."; + echo "$reportedBy earned this achievement at " . getNiceDate($unlockDate); + + if ($unlockDate >= $reportedAtTime) { + echo " (after the report)."; + } else { + echo " (before the report)."; + } } - } elseif ($numAchievements == 0) { - echo "$reportedBy has not earned any achievements for this game."; } else { - echo "$reportedBy did not earn this achievement."; + if ($numAchievements == 0) { + echo "$reportedBy has not earned any achievements for this game."; + } else { + echo "$reportedBy did not earn this achievement."; + } + + if ($permissions >= Permissions::Moderator) { + $hasHardcoreUnlock = false; + foreach ($unlocks as $unlock) { + if ($unlock->unlocked_hardcore_at) { + $hasHardcoreUnlock = true; + break; + } + } + + echo ""; + echo ""; + if ($hasHardcoreUnlock || $numAchievements == 0) { + echo ""; + } + } } echo ""; @@ -807,29 +859,51 @@ echo "