();
const { t } = useTranslation();
@@ -79,13 +81,18 @@ export function useColumnDefinitions(options: {
);
}
- columns.push(
- ...([
+ if (auth?.user) {
+ columns.push(
buildPlayerGameProgressColumnDef({
tableApiRouteName,
tableApiRouteParams,
t_label: t('Progress'),
}),
+ );
+ }
+
+ columns.push(
+ ...([
buildHasActiveOrInReviewClaimsColumnDef({
tableApiRouteName,
tableApiRouteParams,
@@ -100,7 +107,7 @@ export function useColumnDefinitions(options: {
);
return columns;
- }, [options.canSeeOpenTicketsColumn, options.forUsername, system.id, t]);
+ }, [auth?.user, options.canSeeOpenTicketsColumn, options.forUsername, system.id, t]);
return columnDefinitions;
}
diff --git a/resources/views/pages-legacy/achievementInfo.blade.php b/resources/views/pages-legacy/achievementInfo.blade.php
index 1c949ad855..233410b86e 100644
--- a/resources/views/pages-legacy/achievementInfo.blade.php
+++ b/resources/views/pages-legacy/achievementInfo.blade.php
@@ -47,7 +47,7 @@
$dateCreated = $dataOut['DateCreated'];
$dateModified = $dataOut['DateModified'];
$achMem = $dataOut['MemAddr'];
-$isAuthor = $user == $author;
+$isAuthor = $userModel?->display_name === $author;
$canEmbedVideo = (
$permissions >= Permissions::Developer
diff --git a/resources/views/pages-legacy/gameInfo.blade.php b/resources/views/pages-legacy/gameInfo.blade.php
index b4e37db8ec..cbcf9a3a98 100644
--- a/resources/views/pages-legacy/gameInfo.blade.php
+++ b/resources/views/pages-legacy/gameInfo.blade.php
@@ -125,7 +125,7 @@
$allSimilarGames = $gameModel->similarGamesList;
$allGameHubSets = $gameModel->hubs;
-$gameHubs = $allGameHubSets->map($mapGameHubToAlt)->values()->all();
+$gameHubs = $allGameHubSets->map($mapGameHubToAlt)->values()->sortBy('Title')->all();
$v = requestInputSanitized('v', 0, 'integer');
$gate = false;
@@ -1040,7 +1040,7 @@ function resize() {
echo "";
}
- $mappedSimilarGames = $allSimilarGames->map($mapGameToAlt);
+ $mappedSimilarGames = $allSimilarGames->sortBy('Title')->map($mapGameToAlt);
$onlySimilarGameSubsets = $mappedSimilarGames
->filter(fn (array $game) => str_contains($game['Title'], '[Subset -') && $game['ConsoleName'] !== 'Events')
diff --git a/resources/views/pages-legacy/globalRanking.blade.php b/resources/views/pages-legacy/globalRanking.blade.php
index 64843ececa..0c42576f73 100644
--- a/resources/views/pages-legacy/globalRanking.blade.php
+++ b/resources/views/pages-legacy/globalRanking.blade.php
@@ -232,10 +232,15 @@
if ($dataPoint['Points'] != $rankPoints) {
if ($rankPoints === null && $friends === 0 && $type === 2 && $offset > 0) {
- // first rank of subsequent pages for all users / all time should be calculated
- // as it might be shared with users on the previous page
- $rankType = ($unlockMode == UnlockMode::Hardcore) ? RankType::Hardcore : RankType::Softcore;
- $rank = getUserRank($dataPoint['User'], $rankType);
+ // The first rank of subsequent pages for all users / all time should be calculated,
+ // as it might be tied with users on the previous page.
+ // Values >10 indicate descending order, so we'll use modulo to get the base sort type.
+ $rank = match ($sort % 10) {
+ 5 => getUserRank($dataPoint['User'], RankType::Hardcore),
+ 2 => getUserRank($dataPoint['User'], RankType::Softcore),
+ 6 => getUserRank($dataPoint['User'], RankType::TruePoints),
+ default => $rowRank
+ };
} else {
$rank = $rowRank;
}
diff --git a/resources/views/pages-legacy/searchresults.blade.php b/resources/views/pages-legacy/searchresults.blade.php
index eedf2929c5..702fd4fe5e 100644
--- a/resources/views/pages-legacy/searchresults.blade.php
+++ b/resources/views/pages-legacy/searchresults.blade.php
@@ -4,6 +4,7 @@
use App\Enums\SearchType;
use App\Models\Achievement;
+use App\Models\GameSet;
authenticateFromCookie($user, $permissions, $userDetails);
@@ -112,6 +113,21 @@
echo "";
break;
+ case SearchType::Hub:
+ echo "Hub | ";
+ $hub = GameSet::find($nextID);
+ echo "";
+ echo gameAvatar(
+ [
+ 'GameID' => $hub->game_id,
+ 'ImageIcon' => $hub->image_asset_path
+ ],
+ title: $hub->title,
+ href: route('hub.show', $hub)
+ );
+ echo " | ";
+ break;
+
case SearchType::Forum:
echo "Forum Comment | ";
echo "";
diff --git a/tests/Feature/Connect/AwardAchievementTest.php b/tests/Feature/Connect/AwardAchievementTest.php
index a881766a4b..6fdf5e17ef 100644
--- a/tests/Feature/Connect/AwardAchievementTest.php
+++ b/tests/Feature/Connect/AwardAchievementTest.php
@@ -355,7 +355,7 @@ public function testSoftcoreUnlockPromotedToHardcore(): void
);
}
- public function testDelegatedUnlock(): void
+ public function testDelegatedUnlockByName(): void
{
$now = Carbon::now()->clone()->subMinutes(5)->startOfSecond();
Carbon::setTestNow($now);
@@ -536,6 +536,114 @@ public function testDelegatedUnlock(): void
]);
}
+ public function testDelegatedUnlockByUlid(): void
+ {
+ $now = Carbon::now()->clone()->subMinutes(5)->startOfSecond();
+ Carbon::setTestNow($now);
+
+ /** @var System $standalonesSystem */
+ $standalonesSystem = System::factory()->create(['ID' => 102]);
+ /** @var Game $gameOne */
+ $gameOne = $this->seedGame(system: $standalonesSystem, withHash: false);
+
+ /** @var User $integrationUser */
+ $integrationUser = User::factory()->create(['Permissions' => Permissions::Registered, 'appToken' => Str::random(16)]);
+ /** @var User $delegatedUser */
+ $delegatedUser = User::factory()->create(['User' => 'Username', 'Permissions' => Permissions::Registered, 'appToken' => Str::random(16)]);
+
+ $delegatedUser->LastGameID = $gameOne->id;
+ $delegatedUser->save();
+
+ /** @var Achievement $achievement1 */
+ $achievement1 = Achievement::factory()->published()->create(['ID' => 1, 'GameID' => $gameOne->ID, 'user_id' => $integrationUser->id]);
+ /** @var Achievement $achievement2 */
+ $achievement2 = Achievement::factory()->published()->create(['ID' => 2, 'GameID' => $gameOne->ID, 'user_id' => $integrationUser->id]);
+ /** @var Achievement $achievement3 */
+ $achievement3 = Achievement::factory()->published()->create(['ID' => 3, 'GameID' => $gameOne->ID, 'user_id' => $integrationUser->id]);
+ /** @var Achievement $achievement4 */
+ $achievement4 = Achievement::factory()->published()->create(['ID' => 4, 'GameID' => $gameOne->ID, 'user_id' => 9999999]);
+ /** @var Achievement $achievement5 */
+ $achievement5 = Achievement::factory()->published()->create(['ID' => 5, 'GameID' => $gameOne->ID, 'user_id' => $integrationUser->id]);
+ /** @var Achievement $achievement6 */
+ $achievement6 = Achievement::factory()->published()->create(['ID' => 6, 'GameID' => $gameOne->ID, 'user_id' => $integrationUser->id]);
+
+ $unlock1Date = $now->clone()->subMinutes(65);
+ $this->addHardcoreUnlock($delegatedUser, $achievement1, $unlock1Date);
+ $this->addHardcoreUnlock($delegatedUser, $achievement5, $unlock1Date);
+ $this->addHardcoreUnlock($delegatedUser, $achievement6, $unlock1Date);
+
+ $playerSession1 = PlayerSession::where([
+ 'user_id' => $delegatedUser->id,
+ 'game_id' => $achievement3->game_id,
+ ])->orderByDesc('id')->first();
+ $this->assertModelExists($playerSession1);
+
+ // cache the unlocks for the game - verify singular unlock captured
+ $unlocks = getUserAchievementUnlocksForGame($delegatedUser->User, $gameOne->ID);
+ $this->assertEquals([$achievement1->ID, $achievement5->ID, $achievement6->ID], array_keys($unlocks));
+
+ // do the delegated hardcore unlock
+ $scoreBefore = $delegatedUser->RAPoints;
+ $softcoreScoreBefore = $delegatedUser->RASoftcorePoints;
+
+ $params = [
+ 'u' => $integrationUser->User,
+ 't' => $integrationUser->appToken,
+ 'r' => 'awardachievement',
+ 'k' => $delegatedUser->ulid, // !!
+ 'h' => 1,
+ 'a' => $achievement3->ID,
+ 'v' => '62c47b9fba313855ff8a09673780bb35',
+ ];
+
+ $requestUrl = sprintf('dorequest.php?%s', http_build_query($params));
+ $this->post($requestUrl)
+ ->assertExactJson([
+ 'Success' => true,
+ 'AchievementID' => $achievement3->ID,
+ 'AchievementsRemaining' => 2,
+ 'Score' => $scoreBefore + $achievement3->Points,
+ 'SoftcoreScore' => $softcoreScoreBefore,
+ ]);
+ $delegatedUser->refresh();
+
+ // player session resumed
+ $playerSession2 = PlayerSession::where([
+ 'user_id' => $delegatedUser->id,
+ 'game_id' => $achievement3->game_id,
+ ])->orderByDesc('id')->first();
+ $this->assertModelExists($playerSession2);
+
+ // game attached
+ $playerGame = PlayerGame::where([
+ 'user_id' => $delegatedUser->id,
+ 'game_id' => $achievement3->game_id,
+ ])->first();
+ $this->assertModelExists($playerGame);
+ $this->assertNotNull($playerGame->last_played_at);
+
+ // achievement unlocked
+ $playerAchievement = PlayerAchievement::where([
+ 'user_id' => $delegatedUser->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::whereName($delegatedUser->User)->first();
+ $this->assertEquals($scoreBefore + $achievement3->Points, $user1->RAPoints);
+ $this->assertEquals($softcoreScoreBefore, $user1->RASoftcorePoints);
+
+ // make sure the unlock cache was updated
+ $unlocks = getUserAchievementUnlocksForGame($delegatedUser->User, $gameOne->ID);
+ $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']);
+ }
+
public function testBackdatedUnlock(): void
{
$now = Carbon::now()->clone()->subMinutes(5)->startOfSecond();
diff --git a/tests/Feature/Connect/PingTest.php b/tests/Feature/Connect/PingTest.php
index fae65b7d60..f17f8b41e3 100644
--- a/tests/Feature/Connect/PingTest.php
+++ b/tests/Feature/Connect/PingTest.php
@@ -245,7 +245,7 @@ public function testPingUserTokenMismatch(): void
]);
}
- public function testPingDelegated(): void
+ public function testPingDelegatedByName(): void
{
/** @var System $standalonesSystem */
$standalonesSystem = System::factory()->create(['ID' => 102]);
@@ -272,7 +272,94 @@ public function testPingDelegated(): void
'r' => 'ping',
'g' => $gameOne->id,
'm' => 'Doing good',
- 'k' => $delegatedUser->User,
+ 'k' => $delegatedUser->User, // !!
+ ];
+
+ $this->post('dorequest.php', $params)
+ ->assertStatus(200)
+ ->assertExactJson([
+ 'Success' => true,
+ ]);
+
+ $delegatedPlayerSession = PlayerSession::where([
+ 'user_id' => $delegatedUser->id,
+ 'game_id' => $gameOne->id,
+ ])->first();
+ $this->assertModelExists($delegatedPlayerSession);
+ $this->assertEquals(1, $delegatedPlayerSession->duration);
+ $this->assertEquals('Doing good', $delegatedPlayerSession->rich_presence);
+
+ // While delegating, updates are made on behalf of username `k`.
+ $this->assertDatabaseMissing((new PlayerSession())->getTable(), [
+ 'user_id' => $integrationUser->id,
+ 'game_id' => $gameOne->id,
+ ]);
+
+ // Next, try to delegate on a non-standalone game.
+ // This is not allowed and should fail.
+ /** @var System $normalSystem */
+ $normalSystem = System::factory()->create(['ID' => 1]);
+ /** @var Game $gameTwo */
+ $gameTwo = Game::factory()->create(['ConsoleID' => $normalSystem->ID]);
+
+ $params['g'] = $gameTwo->id;
+
+ $this->post('dorequest.php', $params)
+ ->assertStatus(403)
+ ->assertExactJson([
+ "Success" => false,
+ "Error" => "You do not have permission to do that.",
+ "Code" => "access_denied",
+ "Status" => 403,
+ ]);
+
+ // Next, try to delegate on a game with no achievements authored by the integration user.
+ // This is not allowed and should fail.
+ /** @var Game $gameThree */
+ $gameThree = Game::factory()->create(['ConsoleID' => $standalonesSystem->ID]);
+ /** @var User $randomUser */
+ $randomUser = User::factory()->create(['Permissions' => Permissions::Registered, 'appToken' => Str::random(16)]);
+ Achievement::factory()->published()->count(6)->create(['GameID' => $gameThree->id, 'user_id' => $randomUser->id]);
+ $params['g'] = $gameThree->id;
+
+ $this->post('dorequest.php', $params)
+ ->assertStatus(403)
+ ->assertExactJson([
+ "Success" => false,
+ "Error" => "You do not have permission to do that.",
+ "Code" => "access_denied",
+ "Status" => 403,
+ ]);
+ }
+
+ public function testPingDelegatedByUlid(): void
+ {
+ /** @var System $standalonesSystem */
+ $standalonesSystem = System::factory()->create(['ID' => 102]);
+ /** @var Game $gameOne */
+ $gameOne = Game::factory()->create(['ConsoleID' => $standalonesSystem->ID]);
+
+ /** @var User $integrationUser */
+ $integrationUser = User::factory()->create(['Permissions' => Permissions::Registered, 'appToken' => Str::random(16)]);
+ /** @var User $delegatedUser */
+ $delegatedUser = User::factory()->create(['Permissions' => Permissions::Registered, 'appToken' => Str::random(16)]);
+
+ $delegatedUser->LastGameID = $gameOne->id;
+ $delegatedUser->save();
+
+ // The integration user is the sole author of all the set's achievements.
+ Achievement::factory()->published()->count(6)->create([
+ 'GameID' => $gameOne->id,
+ 'user_id' => $integrationUser->id,
+ ]);
+
+ $params = [
+ 'u' => $integrationUser->User,
+ 't' => $integrationUser->appToken,
+ 'r' => 'ping',
+ 'g' => $gameOne->id,
+ 'm' => 'Doing good',
+ 'k' => $delegatedUser->ulid, // !!
];
$this->post('dorequest.php', $params)
diff --git a/tests/Feature/Connect/StartSessionTest.php b/tests/Feature/Connect/StartSessionTest.php
index 90dd577777..1e7bc6e703 100644
--- a/tests/Feature/Connect/StartSessionTest.php
+++ b/tests/Feature/Connect/StartSessionTest.php
@@ -631,7 +631,7 @@ public function testStartSession(): void
$this->assertEquals($this->userAgentUnknown, $playerSession5->user_agent);
}
- public function testStartSessionDelegated(): void
+ public function testStartSessionDelegatedByName(): void
{
$now = Carbon::create(2020, 3, 4, 16, 40, 13); // 4:40:13pm 4 Mar 2020
Carbon::setTestNow($now);
@@ -797,4 +797,115 @@ public function testStartSessionDelegated(): void
"Status" => 405,
]);
}
+
+ public function testStartSessionDelegatedByUlid(): void
+ {
+ $now = Carbon::create(2020, 3, 4, 16, 40, 13); // 4:40:13pm 4 Mar 2020
+ Carbon::setTestNow($now);
+
+ /** @var System $standalonesSystem */
+ $standalonesSystem = System::factory()->create(['ID' => 102]);
+ /** @var Game $gameOne */
+ $gameOne = Game::factory()->create(['ConsoleID' => $standalonesSystem->ID]);
+
+ /** @var User $integrationUser */
+ $integrationUser = User::factory()->create(['Permissions' => Permissions::Registered, 'appToken' => Str::random(16)]);
+ /** @var User $delegatedUser */
+ $delegatedUser = User::factory()->create(['Permissions' => Permissions::Registered, 'appToken' => Str::random(16)]);
+
+ // The integration user is the sole author of all the set's achievements.
+ $coreAchievements = Achievement::factory()->published()->count(3)->create([
+ 'GameID' => $gameOne->id,
+ 'user_id' => $integrationUser->id,
+ ]);
+ $this->upsertGameCoreSetAction->execute($gameOne);
+
+ /** @var Game $bonusGameOne */
+ $bonusGameOne = Game::factory()->create([
+ 'ConsoleID' => $standalonesSystem->ID,
+ 'Title' => $gameOne->Title . ' [Subset - Bonus]',
+ ]);
+ $bonusAchievements = Achievement::factory()->published()->count(3)->create([
+ 'GameID' => $bonusGameOne->id,
+ 'user_id' => $integrationUser->id,
+ ]);
+ $this->upsertGameCoreSetAction->execute($bonusGameOne);
+ $this->associateAchievementSetToGameAction->execute($gameOne, $bonusGameOne, AchievementSetType::Bonus, 'Bonus');
+
+ // ... core unlocks ...
+ $unlock1Date = $now->clone()->subMinutes(65);
+ $this->addHardcoreUnlock($delegatedUser, $coreAchievements->get(0), $unlock1Date);
+ $unlock2Date = $now->clone()->subMinutes(22);
+ $this->addHardcoreUnlock($delegatedUser, $coreAchievements->get(1), $unlock2Date);
+ $unlock3Date = $now->clone()->subMinutes(1);
+ $this->addSoftcoreUnlock($delegatedUser, $coreAchievements->get(2), $unlock3Date);
+
+ // ... bonus unlocks ...
+ $bonusUnlock1Date = $now->clone()->subMinutes(45);
+ $this->addHardcoreUnlock($delegatedUser, $bonusAchievements->get(0), $bonusUnlock1Date);
+ $bonusUnlock2Date = $now->clone()->subMinutes(15);
+ $this->addSoftcoreUnlock($delegatedUser, $bonusAchievements->get(1), $bonusUnlock2Date);
+
+ $this->seedEmulatorUserAgents();
+
+ $params = [
+ 'u' => $integrationUser->User,
+ 't' => $integrationUser->appToken,
+ 'r' => 'startsession',
+ 'g' => $gameOne->id,
+ 'k' => $delegatedUser->ulid, // !!
+ ];
+
+ // ----------------------------
+ // game with unlocks
+ $requestUrl = sprintf('dorequest.php?%s', http_build_query($params));
+ $this->withHeaders(['User-Agent' => $this->userAgentValid])
+ ->post($requestUrl)
+ ->assertExactJson([
+ 'Success' => true,
+ 'HardcoreUnlocks' => [
+ [
+ 'ID' => $coreAchievements->get(0)->ID,
+ 'When' => $unlock1Date->timestamp,
+ ],
+ [
+ 'ID' => $coreAchievements->get(1)->ID,
+ 'When' => $unlock2Date->timestamp,
+ ],
+ [
+ 'ID' => $bonusAchievements->get(0)->ID,
+ 'When' => $bonusUnlock1Date->timestamp,
+ ],
+ ],
+ 'Unlocks' => [
+ [
+ 'ID' => $coreAchievements->get(2)->ID,
+ 'When' => $unlock3Date->timestamp,
+ ],
+ [
+ 'ID' => $bonusAchievements->get(1)->ID,
+ 'When' => $bonusUnlock2Date->timestamp,
+ ],
+ ],
+ 'ServerNow' => Carbon::now()->timestamp,
+ ]);
+
+ // player session created
+ $playerSession = PlayerSession::where([
+ 'user_id' => $delegatedUser->id,
+ 'game_id' => $bonusGameOne->id,
+ ])->first();
+ $this->assertModelExists($playerSession);
+ $this->assertEquals(1, $playerSession->duration);
+ $this->assertEquals('Playing ' . $bonusGameOne->title, $playerSession->rich_presence);
+
+ $this->assertEquals($bonusGameOne->id, $delegatedUser->LastGameID);
+ $this->assertEquals("Playing " . $bonusGameOne->Title, $delegatedUser->RichPresenceMsg);
+
+ // While delegating, updates are made on behalf of username `k`.
+ $this->assertDatabaseMissing((new PlayerSession())->getTable(), [
+ 'user_id' => $integrationUser->id,
+ 'game_id' => $bonusGameOne->id,
+ ]);
+ }
}
|