Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions app/Connect/Actions/SubmitGameTitleAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
use App\Models\GameRelease;
use App\Models\Role;
use App\Models\System;
use App\Platform\Actions\AssociateAchievementSetToGameAction;
use App\Platform\Actions\UpsertGameCoreAchievementSetFromLegacyFlagsAction;
use App\Platform\Enums\AchievementSetType;
use Illuminate\Http\Request;

class SubmitGameTitleAction extends BaseAuthenticatedApiAction
Expand Down Expand Up @@ -88,8 +90,8 @@ protected function process(): array
return $this->accessDenied('You do not have permission to add games to an inactive system.');
}

// title must be unique.
// If the title already exists, just try to add the hash to the existing game
// `title` must be unique.
// If the title already exists, just try to add the hash to the existing game.
$release = GameRelease::query()
->where('title', $this->gameTitle)
->with('game')
Expand All @@ -110,14 +112,19 @@ protected function process(): array
$game->achievements_unpublished = 0;
$game->save();

// Create the initial canonical title in game_releases.
// create the initial canonical title in game_releases
$game->releases()->create([
'title' => $this->gameTitle,
'is_canonical_game_title' => true,
]);

// create an empty GameAchievementSet and AchievementSet
(new UpsertGameCoreAchievementSetFromLegacyFlagsAction())->execute($game);

// attempt to auto-attach subset games to their parent game
if (str_contains($this->gameTitle, '[Subset -')) {
$this->tryAutoAttachSubsetToParent($game);
}
}
}

Expand Down Expand Up @@ -147,4 +154,49 @@ protected function process(): array
'Response' => ['GameID' => $game->id], // clients expect this value to be nested
];
}

/**
* When developers create subset games (eg: "Mega Man 2 [Subset - Bonus]"), the subset's
* achievement set would normally be orphaned and only accessible via /game/{subsetGameId}.
*
* This method automatically attaches the subset to its parent game so users can access
* it via /game/{parentGameId}?set={achievementSetId} instead. We use WillBeExclusive as
* the default type since subset requirements are unknown at creation time, and Exclusive
* is the safest assumption (requires unique hash, doesn't load with core achievements).
*
* If no parent game is found, the subset remains orphaned and can be manually attached
* later via the Filament admin panel.
*/
private function tryAutoAttachSubsetToParent(Game $subsetGame): void
{
/**
* Try to extract the parent game title and subset title from `$this->gameTitle`.
*
* "Mega Man 2 [Subset - Bonus]"
* - parent title: "Mega Man 2"
* - subset title: "Bonus"
*/
if (!preg_match('/^(.+?)\s*\[Subset\s*-\s*(.+?)\]/', $this->gameTitle, $matches)) {
return;
}

$parentTitle = trim($matches[1]);
$subsetName = trim($matches[2]);

// Now, try to find the parent game by an exact title match on the same system.
$parentGame = Game::where('Title', $parentTitle)
->where('ConsoleID', $this->systemId)
->first();

if (!$parentGame) {
return;
}

(new AssociateAchievementSetToGameAction())->execute(
targetGame: $parentGame,
sourceGame: $subsetGame,
type: AchievementSetType::WillBeExclusive,
title: $subsetName
);
}
}
102 changes: 102 additions & 0 deletions tests/Feature/Connect/SubmitGameTitleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use App\Models\Game;
use App\Models\Role;
use App\Models\System;
use App\Platform\Actions\UpsertGameCoreAchievementSetFromLegacyFlagsAction;
use App\Platform\Enums\AchievementSetType;
use Database\Seeders\RolesTableSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
Expand Down Expand Up @@ -351,4 +352,105 @@ public function testSubmitInactiveConsole(): void

$this->assertAuditComment(ArticleType::GameHash, $newGame->id, "$md5 linked by {$this->user->display_name}.");
}

public function testSubmitNewSubsetGameWithExistingParent(): void
{
/** @var System $system */
$system = System::factory()->create();

// create the parent game first with its core achievement set
/** @var Game $parentGame */
$parentGame = Game::factory()->create([
'Title' => 'Mega Man 2',
'ConsoleID' => $system->id,
]);
(new UpsertGameCoreAchievementSetFromLegacyFlagsAction())->execute($parentGame);

$this->seed(RolesTableSeeder::class);
$this->addServerUser();

$this->user->assignRole(Role::DEVELOPER);
$this->user->save();

$md5 = fake()->md5;
$subsetTitle = 'Mega Man 2 [Subset - Bonus]';

$this->get($this->apiUrl('submitgametitle', [
'm' => $md5,
'i' => $subsetTitle,
'c' => $system->id,
]))
->assertStatus(200)
->assertJsonStructure([
'Success',
'GameID',
'Response' => ['GameID'],
]);

// verify the subset game was created
$subsetGame = Game::where('Title', $subsetTitle)->first();
$this->assertNotNull($subsetGame);
$this->assertEquals($subsetTitle, $subsetGame->title);
$this->assertEquals($system->id, $subsetGame->ConsoleID);

// verify the subset game has its own core achievement set
$this->assertEquals(1, $subsetGame->gameAchievementSets()->count());
$subsetCoreSet = $subsetGame->gameAchievementSets()->first();
$this->assertEquals(AchievementSetType::Core, $subsetCoreSet->type);

// verify the parent game now has the subset's achievement set attached as WillBeExclusive
$parentGame->refresh();
$parentGameSets = $parentGame->gameAchievementSets()->get();
$this->assertEquals(2, $parentGameSets->count());

$attachedSet = $parentGameSets->firstWhere('type', AchievementSetType::WillBeExclusive);
$this->assertNotNull($attachedSet);
$this->assertEquals('Bonus', $attachedSet->title);
$this->assertEquals($subsetCoreSet->achievement_set_id, $attachedSet->achievement_set_id);
}

public function testSubmitNewSubsetGameWithNoParent(): void
{
/** @var System $system */
$system = System::factory()->create();

// ... we intentionally don't create a parent game right here ...

$this->seed(RolesTableSeeder::class);
$this->addServerUser();

$this->user->assignRole(Role::DEVELOPER);
$this->user->save();

$md5 = fake()->md5;
$subsetTitle = 'Nonexistent Game [Subset - Bonus]';

$this->get($this->apiUrl('submitgametitle', [
'm' => $md5,
'i' => $subsetTitle,
'c' => $system->id,
]))
->assertStatus(200)
->assertJsonStructure([
'Success',
'GameID',
'Response' => ['GameID'],
]);

// verify the subset game was created
$subsetGame = Game::where('Title', $subsetTitle)->first();
$this->assertNotNull($subsetGame);
$this->assertEquals($subsetTitle, $subsetGame->title);

// verify the subset game has its own core achievement set
$this->assertEquals(1, $subsetGame->gameAchievementSets()->count());
$subsetCoreSet = $subsetGame->gameAchievementSets()->first();
$this->assertEquals(AchievementSetType::Core, $subsetCoreSet->type);

// verify no parent attachment was made (since no parent exists)
// the subset's achievement set should only be attached to itself
$attachedGames = $subsetCoreSet->achievementSet->games()->get();
$this->assertEquals(1, $attachedGames->count());
$this->assertEquals($subsetGame->id, $attachedGames->first()->id);
}
}