From 0f96851d5d8e5bc84ac6757e3c355142d8718c5e Mon Sep 17 00:00:00 2001 From: bakaneko Date: Tue, 24 Oct 2023 19:58:47 +0900 Subject: [PATCH 01/34] prevent ranking if any open issues --- app/Models/Beatmapset.php | 3 +- .../factories/BeatmapDiscussionFactory.php | 5 ++ database/factories/BeatmapsetFactory.php | 4 +- tests/Models/BeatmapsetTest.php | 58 ++++++++++++++----- 4 files changed, 52 insertions(+), 18 deletions(-) diff --git a/app/Models/Beatmapset.php b/app/Models/Beatmapset.php index 79512b546ce..e4e9e47b3ea 100644 --- a/app/Models/Beatmapset.php +++ b/app/Models/Beatmapset.php @@ -862,7 +862,8 @@ public function removeFromLoved(User $user, string $reason) public function rank() { - if (!$this->isQualified()) { + if (!$this->isQualified() + || $this->beatmapDiscussions()->openIssues()->exists()) { return false; } diff --git a/database/factories/BeatmapDiscussionFactory.php b/database/factories/BeatmapDiscussionFactory.php index 6e8be0f31bd..9db3d8372e8 100644 --- a/database/factories/BeatmapDiscussionFactory.php +++ b/database/factories/BeatmapDiscussionFactory.php @@ -44,6 +44,11 @@ public function mapperNote() return $this->state(['message_type' => 'mapper_note']); } + public function messageType(string $type) + { + return $this->state(['message_type' => $type]); + } + public function problem() { return $this->state(['message_type' => 'problem']); diff --git a/database/factories/BeatmapsetFactory.php b/database/factories/BeatmapsetFactory.php index 8c15681d933..63148e0d414 100644 --- a/database/factories/BeatmapsetFactory.php +++ b/database/factories/BeatmapsetFactory.php @@ -94,11 +94,11 @@ public function withDescription(): static }); } - public function withDiscussion() + public function withDiscussion(string $type = 'problem') { return $this ->has(Beatmap::factory()->state(fn (array $attr, Beatmapset $set) => ['user_id' => $set->user_id])) - ->has(BeatmapDiscussion::factory()->general()->state(fn (array $attr, Beatmapset $set) => [ + ->has(BeatmapDiscussion::factory()->general()->messageType($type)->state(fn (array $attr, Beatmapset $set) => [ 'user_id' => $set->user_id, ])); } diff --git a/tests/Models/BeatmapsetTest.php b/tests/Models/BeatmapsetTest.php index fc8e12ee806..cff44a6ac8f 100644 --- a/tests/Models/BeatmapsetTest.php +++ b/tests/Models/BeatmapsetTest.php @@ -17,15 +17,19 @@ use App\Models\Notification; use App\Models\User; use App\Models\UserNotification; +use Database\Factories\Factory; use Queue; use Tests\TestCase; class BeatmapsetTest extends TestCase { + private $fakeGenre; + private $fakeLanguage; + public function testLove() { $user = User::factory()->create(); - $beatmapset = $this->createBeatmapset(); + $beatmapset = $this->beatmapsetFactory()->create(); $notifications = Notification::count(); $userNotifications = UserNotification::count(); @@ -44,7 +48,7 @@ public function testLove() public function testLoveBeatmapApprovedStates(): void { $user = User::factory()->create(); - $beatmapset = $this->createBeatmapset(); + $beatmapset = $this->beatmapsetFactory()->create(); $specifiedBeatmap = $beatmapset->beatmaps()->first(); $beatmapset->beatmaps()->saveMany([ @@ -67,7 +71,7 @@ public function testLoveBeatmapApprovedStates(): void // region single-playmode beatmap sets public function testNominate() { - $beatmapset = $this->createBeatmapset(); + $beatmapset = $this->beatmapsetFactory()->create(); $user = User::factory()->withGroup('bng', $beatmapset->playmodesStr())->create(); $notifications = Notification::count(); @@ -86,7 +90,7 @@ public function testNominate() public function testNominateNATAnyRuleset(): void { - $beatmapset = $this->createBeatmapset(); + $beatmapset = $this->beatmapsetFactory()->create(); $user = User::factory()->withGroup('nat', [])->create(); $this->expectCountChange(fn () => $beatmapset->nominations, 1); @@ -98,7 +102,7 @@ public function testNominateNATAnyRuleset(): void public function testQualify() { - $beatmapset = $this->createBeatmapset(); + $beatmapset = $this->beatmapsetFactory()->create(); $user = User::factory()->withGroup('bng', $beatmapset->playmodesStr())->create(); $notifications = Notification::count(); @@ -116,7 +120,7 @@ public function testQualify() public function testLimitedBNGQualifyingNominationBNGNominated() { - $beatmapset = $this->createBeatmapset(); + $beatmapset = $this->beatmapsetFactory()->create(); $this->fillNominationsExceptLastForMode($beatmapset, 'bng', $beatmapset->playmodesStr()[0]); $nominator = User::factory()->withGroup('bng_limited', $beatmapset->playmodesStr())->create(); @@ -131,7 +135,7 @@ public function testLimitedBNGQualifyingNominationBNGNominated() public function testLimitedBNGQualifyingNominationNATNominated() { - $beatmapset = $this->createBeatmapset(); + $beatmapset = $this->beatmapsetFactory()->create(); $this->fillNominationsExceptLastForMode($beatmapset, 'nat', $beatmapset->playmodesStr()[0]); $nominator = User::factory()->withGroup('bng_limited', $beatmapset->playmodesStr())->create(); @@ -146,7 +150,7 @@ public function testLimitedBNGQualifyingNominationNATNominated() public function testLimitedBNGQualifyingNominationLimitedBNGNominated() { - $beatmapset = $this->createBeatmapset(); + $beatmapset = $this->beatmapsetFactory()->create(); $this->fillNominationsExceptLastForMode($beatmapset, 'bng_limited', $beatmapset->playmodesStr()[0]); $nominator = User::factory()->withGroup('bng_limited', $beatmapset->playmodesStr())->create(); @@ -157,10 +161,10 @@ public function testLimitedBNGQualifyingNominationLimitedBNGNominated() } public function testNominateWithDefaultMetadata() { - $beatmapset = $this->createBeatmapset([ + $beatmapset = $this->beatmapsetFactory([ 'genre_id' => Genre::UNSPECIFIED, 'language_id' => Language::UNSPECIFIED, - ]); + ])->create(); $nominator = User::factory()->withGroup('bng', $beatmapset->playmodesStr())->create(); $this->expectException(AuthorizationException::class); @@ -173,9 +177,9 @@ public function testNominateWithDefaultMetadata() */ public function testRank(string $state, bool $success): void { - $beatmapset = $this->createBeatmapset([ + $beatmapset = $this->beatmapsetFactory([ 'approved' => Beatmapset::STATES[$state], - ]); + ])->create(); $otherUser = User::factory()->create(); @@ -197,6 +201,17 @@ public function testRank(string $state, bool $success): void $this->assertSame($success, $beatmapset->fresh()->isRanked()); } + /** + * @dataProvider rankWithOpenIssueDataProvider + */ + public function testRankWithOpenIssue(string $type): void + { + $beatmapset = $this->beatmapsetFactory()->qualified()->withDiscussion($type)->create(); + + $this->assertTrue($beatmapset->isQualified()); + $this->assertFalse($beatmapset->rank()); + } + public function testGlobalScopeActive() { $beatmapset = Beatmapset::factory()->inactive()->create(); @@ -466,7 +481,18 @@ public function dataProviderForTestRank(): array ]; } - private function createBeatmapset($params = []): Beatmapset + public function rankWithOpenIssueDataProvider() + { + return [ + ['problem'], + ['suggestion'], + ]; + } + + /** + * @return Factory + */ + private function beatmapsetFactory($params = []): Factory { $defaultParams = [ 'approved' => Beatmapset::STATES['pending'], @@ -477,8 +503,10 @@ private function createBeatmapset($params = []): Beatmapset $params['user_id'] ??= User::factory(); - $beatmapset = Beatmapset::factory()->create(array_merge($defaultParams, $params)); - $beatmapset->beatmaps()->save(Beatmap::factory()->make()); + $beatmapset = Beatmapset::factory() + ->state([...$defaultParams, ...$params]) + ->has(Beatmap::factory()->state(fn (array $attr, Beatmapset $set) => ['user_id' => $set->user_id])); + BeatmapMirror::factory()->default()->create(); return $beatmapset; From f0848b6286fee88b53089a1de1357270a968732b Mon Sep 17 00:00:00 2001 From: bakaneko Date: Tue, 24 Oct 2023 20:41:24 +0900 Subject: [PATCH 02/34] some factory cleanup --- tests/Models/BeatmapsetTest.php | 41 +++++++++++---------------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/tests/Models/BeatmapsetTest.php b/tests/Models/BeatmapsetTest.php index cff44a6ac8f..909b69ff203 100644 --- a/tests/Models/BeatmapsetTest.php +++ b/tests/Models/BeatmapsetTest.php @@ -9,6 +9,7 @@ use App\Jobs\Notifications\BeatmapsetDisqualify; use App\Jobs\Notifications\BeatmapsetResetNominations; use App\Models\Beatmap; +use App\Models\BeatmapDiscussion; use App\Models\BeatmapMirror; use App\Models\Beatmapset; use App\Models\BeatmapsetNomination; @@ -23,9 +24,6 @@ class BeatmapsetTest extends TestCase { - private $fakeGenre; - private $fakeLanguage; - public function testLove() { $user = User::factory()->create(); @@ -161,7 +159,7 @@ public function testLimitedBNGQualifyingNominationLimitedBNGNominated() } public function testNominateWithDefaultMetadata() { - $beatmapset = $this->beatmapsetFactory([ + $beatmapset = $this->beatmapsetFactory()->state([ 'genre_id' => Genre::UNSPECIFIED, 'language_id' => Language::UNSPECIFIED, ])->create(); @@ -177,9 +175,7 @@ public function testNominateWithDefaultMetadata() */ public function testRank(string $state, bool $success): void { - $beatmapset = $this->beatmapsetFactory([ - 'approved' => Beatmapset::STATES[$state], - ])->create(); + $beatmapset = $this->beatmapsetFactory()->$state()->create(); $otherUser = User::factory()->create(); @@ -206,7 +202,9 @@ public function testRank(string $state, bool $success): void */ public function testRankWithOpenIssue(string $type): void { - $beatmapset = $this->beatmapsetFactory()->qualified()->withDiscussion($type)->create(); + $beatmapset = $this->beatmapsetFactory() + ->qualified() + ->has(BeatmapDiscussion::factory()->general()->messageType($type))->create(); $this->assertTrue($beatmapset->isQualified()); $this->assertFalse($beatmapset->rank()); @@ -492,24 +490,15 @@ public function rankWithOpenIssueDataProvider() /** * @return Factory */ - private function beatmapsetFactory($params = []): Factory + private function beatmapsetFactory(): Factory { - $defaultParams = [ - 'approved' => Beatmapset::STATES['pending'], - 'download_disabled' => true, - 'genre_id' => $this->fakeGenre->genre_id, - 'language_id' => $this->fakeLanguage->language_id, - ]; - - $params['user_id'] ??= User::factory(); - - $beatmapset = Beatmapset::factory() - ->state([...$defaultParams, ...$params]) - ->has(Beatmap::factory()->state(fn (array $attr, Beatmapset $set) => ['user_id' => $set->user_id])); - BeatmapMirror::factory()->default()->create(); - return $beatmapset; + return Beatmapset::factory() + ->owner() + ->pending() + ->state(['download_disabled' => true]) + ->has(Beatmap::factory()->state(fn (array $attr, Beatmapset $set) => ['user_id' => $set->user_id])); } private function createHybridBeatmapset($params = [], $playmodes = ['osu', 'taiko']): Beatmapset @@ -517,8 +506,8 @@ private function createHybridBeatmapset($params = [], $playmodes = ['osu', 'taik $defaultParams = [ 'approved' => Beatmapset::STATES['pending'], 'download_disabled' => true, - 'genre_id' => $this->fakeGenre->genre_id, - 'language_id' => $this->fakeLanguage->language_id, + // 'genre_id' => $this->fakeGenre->genre_id, + // 'language_id' => $this->fakeLanguage->language_id, ]; $params['user_id'] ??= User::factory(); @@ -547,7 +536,5 @@ protected function setUp(): void Genre::factory()->create(['genre_id' => Genre::UNSPECIFIED]); Language::factory()->create(['language_id' => Language::UNSPECIFIED]); - $this->fakeGenre = Genre::factory()->create(); - $this->fakeLanguage = Language::factory()->create(); } } From a5bc0e9b9a42ce1d359b1d20c2a0cf8f7d1a0aba Mon Sep 17 00:00:00 2001 From: bakaneko Date: Tue, 24 Oct 2023 21:24:11 +0900 Subject: [PATCH 03/34] ...why does this even have options? --- tests/Models/BeatmapsetTest.php | 38 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/Models/BeatmapsetTest.php b/tests/Models/BeatmapsetTest.php index 909b69ff203..bf9f97c666b 100644 --- a/tests/Models/BeatmapsetTest.php +++ b/tests/Models/BeatmapsetTest.php @@ -234,7 +234,7 @@ public function testGlobalScopeSoftDelete() public function testHybridLegacyNominate(): void { $user = User::factory()->withGroup('bng', ['osu'])->create(); - $beatmapset = $this->createHybridBeatmapset(null, ['osu', 'taiko']); + $beatmapset = $this->createHybridBeatmapset(); // create legacy nomination event to enable legacy nomination mode BeatmapsetNomination::factory()->create([ @@ -259,7 +259,7 @@ public function testHybridLegacyNominate(): void public function testHybridLegacyQualify(): void { $user = User::factory()->withGroup('bng', ['osu'])->create(); - $beatmapset = $this->createHybridBeatmapset(null, ['osu', 'taiko']); + $beatmapset = $this->createHybridBeatmapset(); // create legacy nomination event to enable legacy nomination mode BeatmapsetNomination::factory()->create([ @@ -311,7 +311,7 @@ public function testHybridNominateWithNullPlaymode(): void public function testHybridNominateWithNoPlaymodePermission(): void { $user = User::factory()->withGroup('bng', ['osu'])->create(); - $beatmapset = $this->createHybridBeatmapset(null, ['osu', 'taiko']); + $beatmapset = $this->createHybridBeatmapset(); $notifications = Notification::count(); $userNotifications = UserNotification::count(); @@ -332,7 +332,7 @@ public function testHybridNominateWithNoPlaymodePermission(): void public function testHybridNominateWithPlaymodePermissionSingleMode(): void { $user = User::factory()->withGroup('bng', ['osu'])->create(); - $beatmapset = $this->createHybridBeatmapset(null, ['osu', 'taiko']); + $beatmapset = $this->createHybridBeatmapset(); $notifications = Notification::count(); $userNotifications = UserNotification::count(); @@ -351,7 +351,7 @@ public function testHybridNominateWithPlaymodePermissionSingleMode(): void public function testHybridNominateWithPlaymodePermissionTooMany(): void { $user = User::factory()->withGroup('bng', ['osu'])->create(); - $beatmapset = $this->createHybridBeatmapset(null, ['osu', 'taiko']); + $beatmapset = $this->createHybridBeatmapset(); $this->fillNominationsExceptLastForMode($beatmapset, 'bng', 'osu'); @@ -368,7 +368,7 @@ public function testHybridNominateWithPlaymodePermissionTooMany(): void public function testHybridNominateWithPlaymodePermissionMultipleModes(): void { $user = User::factory()->withGroup('bng', ['osu', 'taiko'])->create(); - $beatmapset = $this->createHybridBeatmapset(null, ['osu', 'taiko']); + $beatmapset = $this->createHybridBeatmapset(); $notifications = Notification::count(); $userNotifications = UserNotification::count(); @@ -501,25 +501,25 @@ private function beatmapsetFactory(): Factory ->has(Beatmap::factory()->state(fn (array $attr, Beatmapset $set) => ['user_id' => $set->user_id])); } - private function createHybridBeatmapset($params = [], $playmodes = ['osu', 'taiko']): Beatmapset + private function createHybridBeatmapset($playmodes = ['osu', 'taiko']): Beatmapset { - $defaultParams = [ - 'approved' => Beatmapset::STATES['pending'], - 'download_disabled' => true, - // 'genre_id' => $this->fakeGenre->genre_id, - // 'language_id' => $this->fakeLanguage->language_id, - ]; - - $params['user_id'] ??= User::factory(); + BeatmapMirror::factory()->default()->create(); - $beatmapset = Beatmapset::factory()->create(array_merge($defaultParams, $params)); + $beatmapset = Beatmapset::factory() + ->owner() + ->pending() + ->state(['download_disabled' => true]); foreach ($playmodes as $playmode) { - $beatmapset->beatmaps()->save(Beatmap::factory()->make(['playmode' => Beatmap::modeInt($playmode)])); + $beatmapset = $beatmapset->has( + Beatmap::factory()->state(fn (array $attr, Beatmapset $set) => [ + 'playmode' => Beatmap::modeInt($playmode), + 'user_id' => $set->user_id, + ]) + ); } - BeatmapMirror::factory()->default()->create(); - return $beatmapset; + return $beatmapset->create(); } private function fillNominationsExceptLastForMode(Beatmapset $beatmapset, string $group, string $playmode): void From a0e86a6ca546a64bf547614a21f74e64441dbf51 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Tue, 24 Oct 2023 21:25:14 +0900 Subject: [PATCH 04/34] remove for now --- database/factories/BeatmapDiscussionFactory.php | 4 ++-- database/factories/BeatmapsetFactory.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/database/factories/BeatmapDiscussionFactory.php b/database/factories/BeatmapDiscussionFactory.php index 9db3d8372e8..4d4abf7c491 100644 --- a/database/factories/BeatmapDiscussionFactory.php +++ b/database/factories/BeatmapDiscussionFactory.php @@ -41,7 +41,7 @@ public function general() public function mapperNote() { - return $this->state(['message_type' => 'mapper_note']); + return $this->messageType('mapper_note'); } public function messageType(string $type) @@ -51,7 +51,7 @@ public function messageType(string $type) public function problem() { - return $this->state(['message_type' => 'problem']); + return $this->messageType('problem'); } public function review() diff --git a/database/factories/BeatmapsetFactory.php b/database/factories/BeatmapsetFactory.php index 63148e0d414..8c15681d933 100644 --- a/database/factories/BeatmapsetFactory.php +++ b/database/factories/BeatmapsetFactory.php @@ -94,11 +94,11 @@ public function withDescription(): static }); } - public function withDiscussion(string $type = 'problem') + public function withDiscussion() { return $this ->has(Beatmap::factory()->state(fn (array $attr, Beatmapset $set) => ['user_id' => $set->user_id])) - ->has(BeatmapDiscussion::factory()->general()->messageType($type)->state(fn (array $attr, Beatmapset $set) => [ + ->has(BeatmapDiscussion::factory()->general()->state(fn (array $attr, Beatmapset $set) => [ 'user_id' => $set->user_id, ])); } From cd2306fb96af2e364e4f028569b9bafdde461084 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Wed, 25 Oct 2023 02:57:45 +0900 Subject: [PATCH 05/34] lint --- app/Models/Beatmapset.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/Models/Beatmapset.php b/app/Models/Beatmapset.php index e4e9e47b3ea..18c64a32832 100644 --- a/app/Models/Beatmapset.php +++ b/app/Models/Beatmapset.php @@ -862,8 +862,10 @@ public function removeFromLoved(User $user, string $reason) public function rank() { - if (!$this->isQualified() - || $this->beatmapDiscussions()->openIssues()->exists()) { + if ( + !$this->isQualified() + || $this->beatmapDiscussions()->openIssues()->exists() + ) { return false; } From 945be406df3dda1cf7ca7156a8f2119c98281cfd Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 26 Oct 2023 18:17:40 +0900 Subject: [PATCH 06/34] add tests for modding:rank --- app/Console/Commands/ModdingRankCommand.php | 11 ++- database/factories/BeatmapFactory.php | 6 ++ database/factories/BeatmapsetFactory.php | 5 +- tests/Commands/ModdingRankCommandTest.php | 98 +++++++++++++++++++++ 4 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 tests/Commands/ModdingRankCommandTest.php diff --git a/app/Console/Commands/ModdingRankCommand.php b/app/Console/Commands/ModdingRankCommand.php index 1cfcb3cea0f..8c938a7c5a7 100644 --- a/app/Console/Commands/ModdingRankCommand.php +++ b/app/Console/Commands/ModdingRankCommand.php @@ -16,7 +16,7 @@ class ModdingRankCommand extends Command * * @var string */ - protected $signature = 'modding:rank'; + protected $signature = 'modding:rank {--no-wait}'; /** * The console command description. @@ -25,6 +25,8 @@ class ModdingRankCommand extends Command */ protected $description = 'Rank maps in queue.'; + private bool $noWait = false; + /** * Execute the console command. * @@ -32,6 +34,8 @@ class ModdingRankCommand extends Command */ public function handle() { + $this->noWait = get_bool($this->option('no-wait')); + $this->info('Ranking beatmapsets...'); $modeInts = array_values(Beatmap::MODES); @@ -72,7 +76,6 @@ private function rankAll($modeInt) ->where('queued_at', '<', now()->subDays(config('osu.beatmapset.minimum_days_for_rank'))); $rankingQueue = $toBeRankedQuery->count(); - $toBeRanked = $toBeRankedQuery ->orderBy('queued_at', 'ASC') ->limit($toRankLimit) @@ -90,6 +93,10 @@ private function rankAll($modeInt) private function waitRandom() { + if ($this->noWait) { + return; + } + $delay = rand(5, 120); $this->info("Pausing for {$delay} seconds..."); sleep($delay); diff --git a/database/factories/BeatmapFactory.php b/database/factories/BeatmapFactory.php index ea300a47f1d..991ec7ecce9 100644 --- a/database/factories/BeatmapFactory.php +++ b/database/factories/BeatmapFactory.php @@ -7,6 +7,7 @@ namespace Database\Factories; +use App\Libraries\Ruleset; use App\Models\Beatmap; use App\Models\Beatmapset; @@ -72,6 +73,11 @@ public function ranked(): static return $this->state(['approved' => Beatmapset::STATES['ranked']]); } + public function ruleset(Ruleset $ruleset): static + { + return $this->state(['playmode' => $ruleset->value]); + } + public function wip(): static { return $this->state(['approved' => Beatmapset::STATES['wip']]); diff --git a/database/factories/BeatmapsetFactory.php b/database/factories/BeatmapsetFactory.php index 8c15681d933..21e699807e5 100644 --- a/database/factories/BeatmapsetFactory.php +++ b/database/factories/BeatmapsetFactory.php @@ -13,6 +13,7 @@ use App\Models\Genre; use App\Models\Language; use App\Models\User; +use Carbon\Carbon; class BeatmapsetFactory extends Factory { @@ -70,9 +71,9 @@ public function pending() ]); } - public function qualified() + public function qualified(?Carbon $approvedAt = null) { - $approvedAt = now(); + $approvedAt ??= now(); return $this->state([ 'approved' => Beatmapset::STATES['qualified'], diff --git a/tests/Commands/ModdingRankCommandTest.php b/tests/Commands/ModdingRankCommandTest.php new file mode 100644 index 00000000000..05752163661 --- /dev/null +++ b/tests/Commands/ModdingRankCommandTest.php @@ -0,0 +1,98 @@ +. Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +namespace Tests\Commands; + +use App\Exceptions\InvariantException; +use App\Libraries\Ruleset; +use App\Models\Beatmap; +use App\Models\BeatmapDiscussion; +use App\Models\BeatmapMirror; +use App\Models\Beatmapset; +use App\Models\Solo\Score; +use Artisan; +use Carbon\Carbon; +use Database\Factories\Factory; +use LaravelRedis; +use Tests\TestCase; + +class ModdingRankCommandTest extends TestCase +{ + /** + * @dataProvider rankDataProvider + */ + public function testRank(int $qualifiedDaysAgo, int $expected): void + { + $this->beatmapset(Ruleset::osu, $qualifiedDaysAgo)->create(); + + $this->expectCountChange(fn () => Beatmapset::ranked()->count(), $expected); + + $this->artisan('modding:rank', ['--no-wait' => true]); + } + + public function testRankOpenIssue(): void + { + $beatmapset = $this->beatmapset(Ruleset::osu) + ->has(BeatmapDiscussion::factory()->general()->problem()) + ->create(); + + $this->expectCountChange(fn () => Beatmapset::ranked()->count(), 0); + + $this->artisan('modding:rank', ['--no-wait' => true]); + } + + public function testRankQuota(): void + { + $this->beatmapset(Ruleset::osu)->count(2)->create(); + + $this->expectCountChange(fn () => Beatmapset::qualified()->count(), -2); + $this->expectCountChange(fn () => Beatmapset::ranked()->count(), 2); + + $this->artisan('modding:rank', ['--no-wait' => true]); + } + + public function testRankQuotaSeparateRuleset(): void + { + foreach (Ruleset::cases() as $ruleset) { + $this->beatmapset($ruleset)->create(); + } + + $this->expectCountChange(fn () => Beatmapset::ranked()->count(), count(Ruleset::cases())); + + $this->artisan('modding:rank', ['--no-wait' => true]); + } + + + public function rankDataProvider() + { + // 1 day ago isn't used because it might or might not be equal to the cutoff depending on how fast it runs. + return [ + [0, 0], + [2, 1], + ]; + } + + protected function setUp(): void + { + parent::setUp(); + + config()->set('osu.beatmapset.minimum_days_for_rank', 1); + config()->set('osu.beatmapset.rank_per_day', 2); + + BeatmapMirror::factory()->default()->create(); + } + + /** + * @return Factory + */ + protected function beatmapset(Ruleset $ruleset, int $qualifiedDaysAgo = 2): Factory + { + return Beatmapset::factory() + ->owner() + ->qualified(now()->subDays($qualifiedDaysAgo)) + ->state(['download_disabled' => true]) + ->has(Beatmap::factory()->ruleset($ruleset)); + } +} From 60a13f1ded489485c04696e880e7206b2795c4fb Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 26 Oct 2023 20:06:22 +0900 Subject: [PATCH 07/34] exclude beatmapsets with open issues from queue --- app/Console/Commands/ModdingRankCommand.php | 34 ++++++++++++++++----- tests/Commands/ModdingRankCommandTest.php | 17 +++++++---- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/app/Console/Commands/ModdingRankCommand.php b/app/Console/Commands/ModdingRankCommand.php index 8c938a7c5a7..5ea8563ff08 100644 --- a/app/Console/Commands/ModdingRankCommand.php +++ b/app/Console/Commands/ModdingRankCommand.php @@ -16,7 +16,7 @@ class ModdingRankCommand extends Command * * @var string */ - protected $signature = 'modding:rank {--no-wait}'; + protected $signature = 'modding:rank {--no-wait} {--count-only}'; /** * The console command description. @@ -25,6 +25,7 @@ class ModdingRankCommand extends Command */ protected $description = 'Rank maps in queue.'; + private bool $countOnly = false; private bool $noWait = false; /** @@ -34,9 +35,14 @@ class ModdingRankCommand extends Command */ public function handle() { + $this->countOnly = get_bool($this->option('count-only')); $this->noWait = get_bool($this->option('no-wait')); - $this->info('Ranking beatmapsets...'); + if ($this->countOnly) { + $this->info('Number of beatmapsets in queue:'); + } else { + $this->info('Ranking beatmapsets...'); + } $modeInts = array_values(Beatmap::MODES); @@ -44,7 +50,13 @@ public function handle() foreach ($modeInts as $modeInt) { $this->waitRandom(); - $this->rankAll($modeInt); + + if ($this->countOnly) { + $count = $this->toBeRankedQuery($modeInt)->count(); + $this->info(Beatmap::modeStr($modeInt).": {$count}"); + } else { + $this->rankAll($modeInt); + } } $this->info('Done'); @@ -70,10 +82,7 @@ private function rankAll($modeInt) $toRankLimit = min(config('osu.beatmapset.rank_per_run'), $rankableQuota); - $toBeRankedQuery = Beatmapset::qualified() - ->withoutTrashed() - ->withModesForRanking($modeInt) - ->where('queued_at', '<', now()->subDays(config('osu.beatmapset.minimum_days_for_rank'))); + $toBeRankedQuery = $this->toBeRankedQuery($modeInt); $rankingQueue = $toBeRankedQuery->count(); $toBeRanked = $toBeRankedQuery @@ -91,9 +100,18 @@ private function rankAll($modeInt) } } + private function toBeRankedQuery(int $modeInt) + { + return Beatmapset::qualified() + ->withoutTrashed() + ->withModesForRanking($modeInt) + ->where('queued_at', '<', now()->subDays(config('osu.beatmapset.minimum_days_for_rank'))) + ->whereDoesntHave('beatmapDiscussions', fn ($query) => $query->openIssues()); + } + private function waitRandom() { - if ($this->noWait) { + if ($this->noWait || $this->countOnly) { return; } diff --git a/tests/Commands/ModdingRankCommandTest.php b/tests/Commands/ModdingRankCommandTest.php index 05752163661..542c24cdbef 100644 --- a/tests/Commands/ModdingRankCommandTest.php +++ b/tests/Commands/ModdingRankCommandTest.php @@ -5,17 +5,12 @@ namespace Tests\Commands; -use App\Exceptions\InvariantException; use App\Libraries\Ruleset; use App\Models\Beatmap; use App\Models\BeatmapDiscussion; use App\Models\BeatmapMirror; use App\Models\Beatmapset; -use App\Models\Solo\Score; -use Artisan; -use Carbon\Carbon; use Database\Factories\Factory; -use LaravelRedis; use Tests\TestCase; class ModdingRankCommandTest extends TestCase @@ -34,7 +29,7 @@ public function testRank(int $qualifiedDaysAgo, int $expected): void public function testRankOpenIssue(): void { - $beatmapset = $this->beatmapset(Ruleset::osu) + $this->beatmapset(Ruleset::osu) ->has(BeatmapDiscussion::factory()->general()->problem()) ->create(); @@ -43,6 +38,16 @@ public function testRankOpenIssue(): void $this->artisan('modding:rank', ['--no-wait' => true]); } + public function testRankOpenIssueCounts(): void + { + $this->beatmapset(Ruleset::osu) + ->has(BeatmapDiscussion::factory()->general()->problem()) + ->create(); + + $command = $this->artisan('modding:rank', ['--count-only' => true]); + $command->expectsOutputToContain('osu: 0'); + } + public function testRankQuota(): void { $this->beatmapset(Ruleset::osu)->count(2)->create(); From 625011887ca46846ae2c8150f72013239e2790ac Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 26 Oct 2023 20:14:12 +0900 Subject: [PATCH 08/34] use Ruleset instead --- app/Console/Commands/ModdingRankCommand.php | 26 ++++++++++----------- tests/Commands/ModdingRankCommandTest.php | 2 +- tests/Models/BeatmapsetTest.php | 7 +++--- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/app/Console/Commands/ModdingRankCommand.php b/app/Console/Commands/ModdingRankCommand.php index 5ea8563ff08..e34aebbcf38 100644 --- a/app/Console/Commands/ModdingRankCommand.php +++ b/app/Console/Commands/ModdingRankCommand.php @@ -5,7 +5,7 @@ namespace App\Console\Commands; -use App\Models\Beatmap; +use App\Libraries\Ruleset; use App\Models\Beatmapset; use Illuminate\Console\Command; @@ -44,31 +44,31 @@ public function handle() $this->info('Ranking beatmapsets...'); } - $modeInts = array_values(Beatmap::MODES); + $rulesets = Ruleset::cases(); - shuffle($modeInts); + shuffle($rulesets); - foreach ($modeInts as $modeInt) { + foreach ($rulesets as $ruleset) { $this->waitRandom(); if ($this->countOnly) { - $count = $this->toBeRankedQuery($modeInt)->count(); - $this->info(Beatmap::modeStr($modeInt).": {$count}"); + $count = $this->toBeRankedQuery($ruleset)->count(); + $this->info("{$ruleset->name}: {$count}"); } else { - $this->rankAll($modeInt); + $this->rankAll($ruleset); } } $this->info('Done'); } - private function rankAll($modeInt) + private function rankAll(Ruleset $ruleset) { - $this->info('Ranking beatmapsets with at least mode: '.Beatmap::modeStr($modeInt)); + $this->info("Ranking beatmapsets with at least mode: {$ruleset->name}"); $rankedTodayCount = Beatmapset::ranked() ->withoutTrashed() - ->withModesForRanking($modeInt) + ->withModesForRanking($ruleset->value) ->where('approved_date', '>=', now()->subDays()) ->count(); @@ -82,7 +82,7 @@ private function rankAll($modeInt) $toRankLimit = min(config('osu.beatmapset.rank_per_run'), $rankableQuota); - $toBeRankedQuery = $this->toBeRankedQuery($modeInt); + $toBeRankedQuery = $this->toBeRankedQuery($ruleset); $rankingQueue = $toBeRankedQuery->count(); $toBeRanked = $toBeRankedQuery @@ -100,11 +100,11 @@ private function rankAll($modeInt) } } - private function toBeRankedQuery(int $modeInt) + private function toBeRankedQuery(Ruleset $ruleset) { return Beatmapset::qualified() ->withoutTrashed() - ->withModesForRanking($modeInt) + ->withModesForRanking($ruleset->value) ->where('queued_at', '<', now()->subDays(config('osu.beatmapset.minimum_days_for_rank'))) ->whereDoesntHave('beatmapDiscussions', fn ($query) => $query->openIssues()); } diff --git a/tests/Commands/ModdingRankCommandTest.php b/tests/Commands/ModdingRankCommandTest.php index 542c24cdbef..aafd2202c12 100644 --- a/tests/Commands/ModdingRankCommandTest.php +++ b/tests/Commands/ModdingRankCommandTest.php @@ -45,7 +45,7 @@ public function testRankOpenIssueCounts(): void ->create(); $command = $this->artisan('modding:rank', ['--count-only' => true]); - $command->expectsOutputToContain('osu: 0'); + $command->expectsOutputToContain(Ruleset::osu->name.': 0'); } public function testRankQuota(): void diff --git a/tests/Models/BeatmapsetTest.php b/tests/Models/BeatmapsetTest.php index bf9f97c666b..5c955b5e461 100644 --- a/tests/Models/BeatmapsetTest.php +++ b/tests/Models/BeatmapsetTest.php @@ -8,6 +8,7 @@ use App\Exceptions\AuthorizationException; use App\Jobs\Notifications\BeatmapsetDisqualify; use App\Jobs\Notifications\BeatmapsetResetNominations; +use App\Libraries\Ruleset; use App\Models\Beatmap; use App\Models\BeatmapDiscussion; use App\Models\BeatmapMirror; @@ -501,7 +502,7 @@ private function beatmapsetFactory(): Factory ->has(Beatmap::factory()->state(fn (array $attr, Beatmapset $set) => ['user_id' => $set->user_id])); } - private function createHybridBeatmapset($playmodes = ['osu', 'taiko']): Beatmapset + private function createHybridBeatmapset($rulesets = [Ruleset::osu, Ruleset::taiko]): Beatmapset { BeatmapMirror::factory()->default()->create(); @@ -510,10 +511,10 @@ private function createHybridBeatmapset($playmodes = ['osu', 'taiko']): Beatmaps ->pending() ->state(['download_disabled' => true]); - foreach ($playmodes as $playmode) { + foreach ($rulesets as $ruleset) { $beatmapset = $beatmapset->has( Beatmap::factory()->state(fn (array $attr, Beatmapset $set) => [ - 'playmode' => Beatmap::modeInt($playmode), + 'playmode' => $ruleset->value, 'user_id' => $set->user_id, ]) ); From bc79f4b9e0161f27b4e3928faafd6a5fa9107479 Mon Sep 17 00:00:00 2001 From: nanaya Date: Fri, 27 Oct 2023 19:33:55 +0900 Subject: [PATCH 09/34] Limit automatic group creation --- .../Controllers/GroupHistoryController.php | 3 +-- app/Libraries/Groups.php | 26 +++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/GroupHistoryController.php b/app/Http/Controllers/GroupHistoryController.php index 5d7361fcd9c..f63fe3d4a61 100644 --- a/app/Http/Controllers/GroupHistoryController.php +++ b/app/Http/Controllers/GroupHistoryController.php @@ -25,8 +25,7 @@ public function index() $query = UserGroupEvent::visibleForUser(auth()->user()); if ($params['group'] !== null) { - // Not `app('groups')->byIdentifier(...)` because that would create the group if not found - $groupId = app('groups')->allByIdentifier()->get($params['group'])?->getKey(); + $groupId = app('groups')->byIdentifier($params['group'])?->getKey(); if ($groupId !== null) { $query->where('group_id', $groupId); diff --git a/app/Libraries/Groups.php b/app/Libraries/Groups.php index bac65a03743..6aef1ffe697 100644 --- a/app/Libraries/Groups.php +++ b/app/Libraries/Groups.php @@ -7,6 +7,7 @@ use App\Models\Group; use App\Traits\Memoizes; +use Ds\Set; use Exception; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\ModelNotFoundException; @@ -61,13 +62,34 @@ public function byIdOrFail(int|string|null $id): Group /** * Get a group by its identifier (e.g. "admin"). * - * If the requested group doesn't exist, a new one is created. + * If the requested group doesn't exist and it's one of the privilege + * related groups, a new is created. */ - public function byIdentifier(string $id): Group + public function byIdentifier(string $id): ?Group { $group = $this->allByIdentifier()->get($id); if ($group === null) { + static $privGroups; + $privGroups ??= new Set([ + 'admin', + 'alumni', + 'announce', + 'bng', + 'bng_limited', + 'bot', + 'default', + 'dev', + 'gmt', + 'loved', + 'nat', + 'no_profile', + ]); + + if (!$privGroups->contains($id)) { + return null; + } + try { $group = Group::create([ 'group_desc' => '', From 2e1f254dac33f56e8364cc26d8e363f4d67a87c1 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Mon, 30 Oct 2023 20:51:51 +0900 Subject: [PATCH 10/34] extra test for hybrid sets --- tests/Commands/ModdingRankCommandTest.php | 57 +++++++++++++++++++---- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/tests/Commands/ModdingRankCommandTest.php b/tests/Commands/ModdingRankCommandTest.php index aafd2202c12..58484ab78b2 100644 --- a/tests/Commands/ModdingRankCommandTest.php +++ b/tests/Commands/ModdingRankCommandTest.php @@ -20,16 +20,31 @@ class ModdingRankCommandTest extends TestCase */ public function testRank(int $qualifiedDaysAgo, int $expected): void { - $this->beatmapset(Ruleset::osu, $qualifiedDaysAgo)->create(); + $this->beatmapset([Ruleset::osu], $qualifiedDaysAgo)->create(); $this->expectCountChange(fn () => Beatmapset::ranked()->count(), $expected); $this->artisan('modding:rank', ['--no-wait' => true]); } + /** + * @dataProvider rankHybridDataProvider + */ + public function testRankHybrid(array $beatmapsetRulesets, array $expectedCounts): void + { + foreach ($beatmapsetRulesets as $rulesets) { + $this->beatmapset($rulesets)->create(); + } + + $command = $this->artisan('modding:rank', ['--count-only' => true]); + foreach (Ruleset::cases() as $ruleset) { + $command->expectsOutputToContain("{$ruleset->name}: {$expectedCounts[$ruleset->value]}"); + } + } + public function testRankOpenIssue(): void { - $this->beatmapset(Ruleset::osu) + $this->beatmapset([Ruleset::osu]) ->has(BeatmapDiscussion::factory()->general()->problem()) ->create(); @@ -40,7 +55,7 @@ public function testRankOpenIssue(): void public function testRankOpenIssueCounts(): void { - $this->beatmapset(Ruleset::osu) + $this->beatmapset([Ruleset::osu]) ->has(BeatmapDiscussion::factory()->general()->problem()) ->create(); @@ -50,7 +65,7 @@ public function testRankOpenIssueCounts(): void public function testRankQuota(): void { - $this->beatmapset(Ruleset::osu)->count(2)->create(); + $this->beatmapset([Ruleset::osu])->count(2)->create(); $this->expectCountChange(fn () => Beatmapset::qualified()->count(), -2); $this->expectCountChange(fn () => Beatmapset::ranked()->count(), 2); @@ -61,7 +76,7 @@ public function testRankQuota(): void public function testRankQuotaSeparateRuleset(): void { foreach (Ruleset::cases() as $ruleset) { - $this->beatmapset($ruleset)->create(); + $this->beatmapset([$ruleset])->create(); } $this->expectCountChange(fn () => Beatmapset::ranked()->count(), count(Ruleset::cases())); @@ -79,6 +94,24 @@ public function rankDataProvider() ]; } + public function rankHybridDataProvider() + { + return [ + // hybrid counts as ruleset with lowest enum value + [[[Ruleset::osu, Ruleset::taiko, Ruleset::catch, Ruleset::mania]], [1, 0, 0, 0]], + [[[Ruleset::taiko, Ruleset::catch, Ruleset::mania]], [0, 1, 0, 0]], + [[[Ruleset::catch, Ruleset::mania]], [0, 0, 1, 0]], + [[[Ruleset::mania]], [0, 0, 0, 1]], + + // not comprehensive + [[[Ruleset::osu, Ruleset::taiko], [Ruleset::osu]], [2, 0, 0, 0]], + [[[Ruleset::osu, Ruleset::taiko], [Ruleset::taiko]], [1, 1, 0, 0]], + [[[Ruleset::mania, Ruleset::taiko], [Ruleset::taiko]], [0, 2, 0, 0]], + [[[Ruleset::mania, Ruleset::taiko], [Ruleset::mania]], [0, 1, 0, 1]], + [[[Ruleset::catch, Ruleset::taiko], [Ruleset::mania]], [0, 1, 0, 1]], + ]; + } + protected function setUp(): void { parent::setUp(); @@ -90,14 +123,20 @@ protected function setUp(): void } /** + * @param Ruleset[] $rulesets * @return Factory */ - protected function beatmapset(Ruleset $ruleset, int $qualifiedDaysAgo = 2): Factory + protected function beatmapset(array $rulesets, int $qualifiedDaysAgo = 2): Factory { - return Beatmapset::factory() + $factory = Beatmapset::factory() ->owner() ->qualified(now()->subDays($qualifiedDaysAgo)) - ->state(['download_disabled' => true]) - ->has(Beatmap::factory()->ruleset($ruleset)); + ->state(['download_disabled' => true]); + + foreach ($rulesets as $ruleset) { + $factory = $factory->has(Beatmap::factory()->ruleset($ruleset)); + } + + return $factory; } } From 1a70ec0698fe944a70e8881105e8c6dbd3137366 Mon Sep 17 00:00:00 2001 From: nanaya Date: Thu, 2 Nov 2023 18:20:18 +0900 Subject: [PATCH 11/34] Apparently this is valid now --- app/Libraries/Groups.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Libraries/Groups.php b/app/Libraries/Groups.php index 6aef1ffe697..653fedffcb6 100644 --- a/app/Libraries/Groups.php +++ b/app/Libraries/Groups.php @@ -70,8 +70,7 @@ public function byIdentifier(string $id): ?Group $group = $this->allByIdentifier()->get($id); if ($group === null) { - static $privGroups; - $privGroups ??= new Set([ + static $privGroups = new Set([ 'admin', 'alumni', 'announce', From 60edb41367c22a0dfdd0df7f942186bd40ce866f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 Nov 2023 20:09:57 +0100 Subject: [PATCH 12/34] Manually update mods database with new `AlwaysValidForSubmission` flag --- database/mods.json | 764 +++++++++++++++++++++++++++------------------ 1 file changed, 453 insertions(+), 311 deletions(-) diff --git a/database/mods.json b/database/mods.json index 45561b70bd7..bcbe5f16460 100644 --- a/database/mods.json +++ b/database/mods.json @@ -24,7 +24,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "NF", @@ -42,7 +43,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "HT", @@ -75,7 +77,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "DC", @@ -102,7 +105,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "HR", @@ -118,7 +122,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "SD", @@ -143,7 +148,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "PF", @@ -168,7 +174,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "DT", @@ -201,7 +208,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "NC", @@ -228,7 +236,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "HD", @@ -251,7 +260,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "FL", @@ -284,7 +294,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "BL", @@ -298,7 +309,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "ST", @@ -313,7 +325,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "AC", @@ -350,7 +363,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "TP", @@ -382,7 +396,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "DA", @@ -428,7 +443,8 @@ "RequiresConfiguration": true, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "CL", @@ -467,7 +483,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "RD", @@ -494,7 +511,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "MR", @@ -515,7 +533,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "AL", @@ -532,7 +551,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "SG", @@ -549,7 +569,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "AT", @@ -566,12 +587,14 @@ "SO", "MG", "RP", - "AS" + "AS", + "TD" ], "RequiresConfiguration": false, "UserPlayable": false, "ValidForMultiplayer": false, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "CN", @@ -589,12 +612,14 @@ "SO", "MG", "RP", - "AS" + "AS", + "TD" ], "RequiresConfiguration": false, "UserPlayable": false, "ValidForMultiplayer": false, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "RX", @@ -617,7 +642,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "AP", @@ -635,12 +661,14 @@ "RX", "SO", "MG", - "RP" + "RP", + "TD" ], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "SO", @@ -657,7 +685,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "TR", @@ -668,12 +697,14 @@ "IncompatibleMods": [ "WG", "MG", - "RP" + "RP", + "FR" ], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "WG", @@ -696,7 +727,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "SI", @@ -714,7 +746,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "GR", @@ -739,7 +772,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "DF", @@ -764,7 +798,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "WU", @@ -802,7 +837,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "WD", @@ -840,7 +876,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "TC", @@ -858,7 +895,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "BR", @@ -885,7 +923,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "AD", @@ -917,7 +956,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "MU", @@ -954,7 +994,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "NS", @@ -973,7 +1014,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "MG", @@ -1001,7 +1043,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "RP", @@ -1028,7 +1071,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "AS", @@ -1062,7 +1106,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": false, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "FR", @@ -1071,12 +1116,14 @@ "Type": "Fun", "Settings": [], "IncompatibleMods": [ + "TR", "AD" ], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "BU", @@ -1092,7 +1139,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "SY", @@ -1104,7 +1152,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "TD", @@ -1112,11 +1161,16 @@ "Description": "Automatically applied to plays on devices with a touchscreen.", "Type": "System", "Settings": [], - "IncompatibleMods": [], + "IncompatibleMods": [ + "AT", + "CN", + "AP" + ], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": true }, { "Acronym": "SV2", @@ -1127,8 +1181,9 @@ "IncompatibleMods": [], "RequiresConfiguration": false, "UserPlayable": false, - "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayer": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false } ] }, @@ -1149,7 +1204,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "NF", @@ -1166,7 +1222,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "HT", @@ -1199,7 +1256,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "DC", @@ -1226,7 +1284,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "HR", @@ -1241,7 +1300,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "SD", @@ -1264,7 +1324,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "PF", @@ -1288,7 +1349,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "DT", @@ -1321,7 +1383,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "NC", @@ -1348,7 +1411,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "HD", @@ -1360,7 +1424,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "FL", @@ -1385,7 +1450,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "AC", @@ -1420,7 +1486,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "RD", @@ -1441,7 +1508,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "DA", @@ -1481,7 +1549,8 @@ "RequiresConfiguration": true, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "CL", @@ -1493,7 +1562,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "SW", @@ -1507,7 +1577,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "SG", @@ -1523,7 +1594,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "AT", @@ -1540,7 +1612,8 @@ "RequiresConfiguration": false, "UserPlayable": false, "ValidForMultiplayer": false, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "CN", @@ -1558,7 +1631,8 @@ "RequiresConfiguration": false, "UserPlayable": false, "ValidForMultiplayer": false, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "RX", @@ -1578,7 +1652,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "WU", @@ -1616,7 +1691,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "WD", @@ -1654,7 +1730,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "MU", @@ -1691,7 +1768,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "AS", @@ -1725,7 +1803,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": false, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "SV2", @@ -1736,8 +1815,9 @@ "IncompatibleMods": [], "RequiresConfiguration": false, "UserPlayable": false, - "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayer": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false } ] }, @@ -1766,7 +1846,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "NF", @@ -1783,7 +1864,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "HT", @@ -1815,7 +1897,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "DC", @@ -1841,7 +1924,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "HR", @@ -1856,7 +1940,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "SD", @@ -1879,7 +1964,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "PF", @@ -1903,7 +1989,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "DT", @@ -1935,7 +2022,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "NC", @@ -1961,7 +2049,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "HD", @@ -1973,7 +2062,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "FL", @@ -1998,7 +2088,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "AC", @@ -2034,7 +2125,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "DA", @@ -2086,7 +2178,8 @@ "RequiresConfiguration": true, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "CL", @@ -2098,7 +2191,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "MR", @@ -2110,7 +2204,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "AT", @@ -2125,7 +2220,8 @@ "RequiresConfiguration": false, "UserPlayable": false, "ValidForMultiplayer": false, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "CN", @@ -2141,7 +2237,8 @@ "RequiresConfiguration": false, "UserPlayable": false, "ValidForMultiplayer": false, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "RX", @@ -2160,7 +2257,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "WU", @@ -2197,7 +2295,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "WD", @@ -2234,7 +2333,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "FF", @@ -2246,7 +2346,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "MU", @@ -2283,7 +2384,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "NS", @@ -2302,7 +2404,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "SV2", @@ -2313,8 +2416,9 @@ "IncompatibleMods": [], "RequiresConfiguration": false, "UserPlayable": false, - "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayer": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false } ] }, @@ -2343,7 +2447,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "NF", @@ -2359,7 +2464,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "HT", @@ -2392,7 +2498,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "DC", @@ -2419,7 +2526,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "HR", @@ -2434,7 +2542,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "SD", @@ -2456,7 +2565,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "PF", @@ -2479,7 +2589,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "DT", @@ -2512,7 +2623,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "NC", @@ -2539,7 +2651,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "FI", @@ -2561,7 +2674,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "HD", @@ -2583,7 +2697,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "FL", @@ -2611,7 +2726,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "AC", @@ -2646,161 +2762,145 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { - "Acronym": "4K", - "Name": "Four Keys", - "Description": "Play with four keys.", + "Acronym": "RD", + "Name": "Random", + "Description": "Shuffle around the keys!", "Type": "Conversion", - "Settings": [], - "IncompatibleMods": [ - "5K", - "6K", - "7K", - "8K", - "9K", - "10K", - "1K", - "2K", - "3K" + "Settings": [ + { + "Name": "seed", + "Type": "number", + "Label": "Seed", + "Description": "Use a custom seed instead of a random one" + } ], + "IncompatibleMods": [], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { - "Acronym": "5K", - "Name": "Five Keys", - "Description": "Play with five keys.", + "Acronym": "DS", + "Name": "Dual Stages", + "Description": "Double the stages, double the fun!", "Type": "Conversion", "Settings": [], - "IncompatibleMods": [ - "4K", - "6K", - "7K", - "8K", - "9K", - "10K", - "1K", - "2K", - "3K" - ], + "IncompatibleMods": [], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { - "Acronym": "6K", - "Name": "Six Keys", - "Description": "Play with six keys.", + "Acronym": "MR", + "Name": "Mirror", + "Description": "Notes are flipped horizontally.", "Type": "Conversion", "Settings": [], - "IncompatibleMods": [ - "4K", - "5K", - "7K", - "8K", - "9K", - "10K", - "1K", - "2K", - "3K" - ], + "IncompatibleMods": [], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { - "Acronym": "7K", - "Name": "Seven Keys", - "Description": "Play with seven keys.", + "Acronym": "DA", + "Name": "Difficulty Adjust", + "Description": "Override a beatmap's difficulty settings.", "Type": "Conversion", - "Settings": [], + "Settings": [ + { + "Name": "drain_rate", + "Type": "number", + "Label": "HP Drain", + "Description": "Override a beatmap's set HP." + }, + { + "Name": "overall_difficulty", + "Type": "number", + "Label": "Accuracy", + "Description": "Override a beatmap's set OD." + }, + { + "Name": "extended_limits", + "Type": "boolean", + "Label": "Extended Limits", + "Description": "Adjust difficulty beyond sane limits." + } + ], "IncompatibleMods": [ - "4K", - "5K", - "6K", - "8K", - "9K", - "10K", - "1K", - "2K", - "3K" + "EZ", + "HR" ], + "RequiresConfiguration": true, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "CL", + "Name": "Classic", + "Description": "Feeling nostalgic?", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { - "Acronym": "8K", - "Name": "Eight Keys", - "Description": "Play with eight keys.", + "Acronym": "IN", + "Name": "Invert", + "Description": "Hold the keys. To the beat.", "Type": "Conversion", "Settings": [], "IncompatibleMods": [ - "4K", - "5K", - "6K", - "7K", - "9K", - "10K", - "1K", - "2K", - "3K" + "HO" ], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { - "Acronym": "9K", - "Name": "Nine Keys", - "Description": "Play with nine keys.", + "Acronym": "CS", + "Name": "Constant Speed", + "Description": "No more tricky speed changes!", "Type": "Conversion", "Settings": [], - "IncompatibleMods": [ - "4K", - "5K", - "6K", - "7K", - "8K", - "10K", - "1K", - "2K", - "3K" - ], + "IncompatibleMods": [], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { - "Acronym": "10K", - "Name": "Ten Keys", - "Description": "Play with ten keys.", + "Acronym": "HO", + "Name": "Hold Off", + "Description": "Replaces all hold notes with normal notes.", "Type": "Conversion", "Settings": [], "IncompatibleMods": [ - "4K", - "5K", - "6K", - "7K", - "8K", - "9K", - "1K", - "2K", - "3K" + "IN" ], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "1K", @@ -2809,20 +2909,21 @@ "Type": "Conversion", "Settings": [], "IncompatibleMods": [ + "2K", + "3K", "4K", "5K", "6K", "7K", "8K", "9K", - "10K", - "2K", - "3K" + "10K" ], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "2K", @@ -2831,20 +2932,21 @@ "Type": "Conversion", "Settings": [], "IncompatibleMods": [ + "1K", + "3K", "4K", "5K", "6K", "7K", "8K", "9K", - "10K", - "1K", - "3K" + "10K" ], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "3K", @@ -2853,149 +2955,182 @@ "Type": "Conversion", "Settings": [], "IncompatibleMods": [ + "1K", + "2K", "4K", "5K", "6K", "7K", "8K", "9K", - "10K", - "1K", - "2K" + "10K" ], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { - "Acronym": "RD", - "Name": "Random", - "Description": "Shuffle around the keys!", + "Acronym": "4K", + "Name": "Four Keys", + "Description": "Play with four keys.", "Type": "Conversion", - "Settings": [ - { - "Name": "seed", - "Type": "number", - "Label": "Seed", - "Description": "Use a custom seed instead of a random one" - } + "Settings": [], + "IncompatibleMods": [ + "1K", + "2K", + "3K", + "5K", + "6K", + "7K", + "8K", + "9K", + "10K" ], - "IncompatibleMods": [], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { - "Acronym": "DS", - "Name": "Dual Stages", - "Description": "Double the stages, double the fun!", + "Acronym": "5K", + "Name": "Five Keys", + "Description": "Play with five keys.", "Type": "Conversion", "Settings": [], - "IncompatibleMods": [], + "IncompatibleMods": [ + "1K", + "2K", + "3K", + "4K", + "6K", + "7K", + "8K", + "9K", + "10K" + ], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { - "Acronym": "MR", - "Name": "Mirror", - "Description": "Notes are flipped horizontally.", + "Acronym": "6K", + "Name": "Six Keys", + "Description": "Play with six keys.", "Type": "Conversion", "Settings": [], - "IncompatibleMods": [], - "RequiresConfiguration": false, - "UserPlayable": true, - "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true - }, - { - "Acronym": "DA", - "Name": "Difficulty Adjust", - "Description": "Override a beatmap's difficulty settings.", - "Type": "Conversion", - "Settings": [ - { - "Name": "drain_rate", - "Type": "number", - "Label": "HP Drain", - "Description": "Override a beatmap's set HP." - }, - { - "Name": "overall_difficulty", - "Type": "number", - "Label": "Accuracy", - "Description": "Override a beatmap's set OD." - }, - { - "Name": "extended_limits", - "Type": "boolean", - "Label": "Extended Limits", - "Description": "Adjust difficulty beyond sane limits." - } - ], "IncompatibleMods": [ - "EZ", - "HR" + "1K", + "2K", + "3K", + "4K", + "5K", + "7K", + "8K", + "9K", + "10K" ], - "RequiresConfiguration": true, + "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { - "Acronym": "CL", - "Name": "Classic", - "Description": "Feeling nostalgic?", + "Acronym": "7K", + "Name": "Seven Keys", + "Description": "Play with seven keys.", "Type": "Conversion", "Settings": [], - "IncompatibleMods": [], + "IncompatibleMods": [ + "1K", + "2K", + "3K", + "4K", + "5K", + "6K", + "8K", + "9K", + "10K" + ], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { - "Acronym": "IN", - "Name": "Invert", - "Description": "Hold the keys. To the beat.", + "Acronym": "8K", + "Name": "Eight Keys", + "Description": "Play with eight keys.", "Type": "Conversion", "Settings": [], "IncompatibleMods": [ - "HO" + "1K", + "2K", + "3K", + "4K", + "5K", + "6K", + "7K", + "9K", + "10K" ], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { - "Acronym": "CS", - "Name": "Constant Speed", - "Description": "No more tricky speed changes!", + "Acronym": "9K", + "Name": "Nine Keys", + "Description": "Play with nine keys.", "Type": "Conversion", "Settings": [], - "IncompatibleMods": [], + "IncompatibleMods": [ + "1K", + "2K", + "3K", + "4K", + "5K", + "6K", + "7K", + "8K", + "10K" + ], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { - "Acronym": "HO", - "Name": "Hold Off", - "Description": "Replaces all hold notes with normal notes.", + "Acronym": "10K", + "Name": "Ten Keys", + "Description": "Play with ten keys.", "Type": "Conversion", "Settings": [], "IncompatibleMods": [ - "IN" + "1K", + "2K", + "3K", + "4K", + "5K", + "6K", + "7K", + "8K", + "9K" ], "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "AT", @@ -3010,7 +3145,8 @@ "RequiresConfiguration": false, "UserPlayable": false, "ValidForMultiplayer": false, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "CN", @@ -3026,7 +3162,8 @@ "RequiresConfiguration": false, "UserPlayable": false, "ValidForMultiplayer": false, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "WU", @@ -3064,7 +3201,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "WD", @@ -3102,7 +3240,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "MU", @@ -3139,7 +3278,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false }, { "Acronym": "AS", @@ -3173,7 +3313,8 @@ "RequiresConfiguration": false, "UserPlayable": true, "ValidForMultiplayer": false, - "ValidForMultiplayerAsFreeMod": false + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false }, { "Acronym": "SV2", @@ -3184,8 +3325,9 @@ "IncompatibleMods": [], "RequiresConfiguration": false, "UserPlayable": false, - "ValidForMultiplayer": true, - "ValidForMultiplayerAsFreeMod": true + "ValidForMultiplayer": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false } ] } From 25b85774a0ec74fa501f5f622676993f901b46c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 Nov 2023 20:17:07 +0100 Subject: [PATCH 13/34] Add failing test case covering expected behaviour --- tests/Models/Multiplayer/ScoreLinkTest.php | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/Models/Multiplayer/ScoreLinkTest.php b/tests/Models/Multiplayer/ScoreLinkTest.php index 8931a0aad31..cc1e09f7800 100644 --- a/tests/Models/Multiplayer/ScoreLinkTest.php +++ b/tests/Models/Multiplayer/ScoreLinkTest.php @@ -8,6 +8,7 @@ namespace Tests\Models\Multiplayer; use App\Exceptions\InvariantException; +use App\Models\Beatmap; use App\Models\Multiplayer\PlaylistItem; use App\Models\Multiplayer\ScoreLink; use App\Models\ScoreToken; @@ -151,4 +152,32 @@ public function testUnexpectedModWhenNoModsAreAllowed() ], ]); } + + public function testUnexpectedModAcceptedIfAlwaysValidForSubmission() + { + $beatmap = Beatmap::factory()->create([ + 'playmode' => 0, // must be osu! specifically. no other ruleset currently has an appropriate mod. + ]); + $playlistItem = PlaylistItem::factory()->create([ + 'ruleset_id' => 0, + 'beatmap_id' => $beatmap, + // no required or allowed mods. + ]); + $scoreToken = ScoreToken::factory()->create([ + 'beatmap_id' => $playlistItem->beatmap_id, + 'playlist_item_id' => $playlistItem, + ]); + + $this->expectNotToPerformAssertions(); + ScoreLink::complete($scoreToken, [ + 'beatmap_id' => $playlistItem->beatmap_id, + 'ruleset_id' => $playlistItem->ruleset_id, + 'user_id' => $scoreToken->user_id, + 'ended_at' => json_date(Carbon::now()), + 'mods' => [['acronym' => 'TD']], + 'statistics' => [ + 'great' => 1, + ], + ]); + } } From 76d1ef2ad93e351e34d7492709b28e01991ba73e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 Nov 2023 20:40:27 +0100 Subject: [PATCH 14/34] Allow plays with touch device mod to be submitted in multiplayer regardless of required/allowed mods --- app/Libraries/Mods.php | 13 +++++++++++++ app/Models/Multiplayer/ScoreLink.php | 2 ++ 2 files changed, 15 insertions(+) diff --git a/app/Libraries/Mods.php b/app/Libraries/Mods.php index 1b1f4d1c7b1..cc107c032b0 100644 --- a/app/Libraries/Mods.php +++ b/app/Libraries/Mods.php @@ -112,6 +112,19 @@ public function assertValidForMultiplayer(int $rulesetId, array $ids, bool $isRe } } + public function excludeModsAlwaysValidForSubmission(int $rulesetId, array $ids): array + { + $this->validateSelection($rulesetId, $ids); + + $rulesetMods = $this->mods[$rulesetId]; + + return collect($ids) + ->filter(function ($id) use ($rulesetMods) { + return !$rulesetMods[$id]['AlwaysValidForSubmission']; + }) + ->all(); + } + public function idsToBitset($ids): int { if (!is_array($ids)) { diff --git a/app/Models/Multiplayer/ScoreLink.php b/app/Models/Multiplayer/ScoreLink.php index 2dc0be08c9d..f1ec3d108b6 100644 --- a/app/Models/Multiplayer/ScoreLink.php +++ b/app/Models/Multiplayer/ScoreLink.php @@ -33,6 +33,8 @@ public static function complete(ScoreToken $token, array $params): static $playlistItem = $token->playlistItem; $requiredMods = array_column($playlistItem->required_mods, 'acronym'); $mods = array_column($score->data->mods, 'acronym'); + $mods = app('mods')->excludeModsAlwaysValidForSubmission($playlistItem->ruleset_id, $mods); + if (!empty($requiredMods)) { if (!empty(array_diff($requiredMods, $mods))) { throw new InvariantException('This play does not include the mods required.'); From 867d3bd1714a4a9832877026d209dc7afe792c64 Mon Sep 17 00:00:00 2001 From: nanaya Date: Mon, 6 Nov 2023 19:11:30 +0900 Subject: [PATCH 15/34] Always create when running test instead --- app/Libraries/Groups.php | 50 +------------------ app/Models/Group.php | 17 +++++++ phpunit.dusk.xml | 3 ++ phpunit.xml | 3 ++ tests/Browser/SanityTest.php | 5 +- .../BeatmapsControllerSoloScoresTest.php | 1 - tests/Jobs/RemoveBeatmapsetSoloScoresTest.php | 1 - tests/Models/Solo/ScoreEsIndexTest.php | 1 - tests/SeederExtension.php | 38 ++++++++++++++ tests/TestCase.php | 18 +++---- 10 files changed, 72 insertions(+), 65 deletions(-) create mode 100644 tests/SeederExtension.php diff --git a/app/Libraries/Groups.php b/app/Libraries/Groups.php index 653fedffcb6..d3cbb0384f2 100644 --- a/app/Libraries/Groups.php +++ b/app/Libraries/Groups.php @@ -7,8 +7,6 @@ use App\Models\Group; use App\Traits\Memoizes; -use Ds\Set; -use Exception; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\ModelNotFoundException; @@ -61,56 +59,10 @@ public function byIdOrFail(int|string|null $id): Group /** * Get a group by its identifier (e.g. "admin"). - * - * If the requested group doesn't exist and it's one of the privilege - * related groups, a new is created. */ public function byIdentifier(string $id): ?Group { - $group = $this->allByIdentifier()->get($id); - - if ($group === null) { - static $privGroups = new Set([ - 'admin', - 'alumni', - 'announce', - 'bng', - 'bng_limited', - 'bot', - 'default', - 'dev', - 'gmt', - 'loved', - 'nat', - 'no_profile', - ]); - - if (!$privGroups->contains($id)) { - return null; - } - - try { - $group = Group::create([ - 'group_desc' => '', - 'group_name' => $id, - 'group_type' => 2, - 'identifier' => $id, - 'short_name' => $id, - ])->fresh(); - } catch (Exception $ex) { - if (!is_sql_unique_exception($ex)) { - throw $ex; - } - $group = Group::firstWhere(['identifier' => $id]); - } - - // TODO: This shouldn't have to be called here, since it's already - // called by `Group::afterCommit`, but `Group::afterCommit` isn't - // running in tests when creating/saving `Group`s. - $this->resetMemoized(); - } - - return $group; + return $this->allByIdentifier()->get($id); } protected function fetch(): Collection diff --git a/app/Models/Group.php b/app/Models/Group.php index 6d3c76cf11f..aa1c2025893 100644 --- a/app/Models/Group.php +++ b/app/Models/Group.php @@ -35,6 +35,23 @@ */ class Group extends Model implements AfterCommit { + // Identifier of groups which involved in permission checks + // and assumed to always exist in database. + const PRIV_GROUPS = [ + 'admin', + 'alumni', + 'announce', + 'bng', + 'bng_limited', + 'bot', + 'default', + 'dev', + 'gmt', + 'loved', + 'nat', + 'no_profile', + ]; + public $timestamps = false; protected $casts = [ diff --git a/phpunit.dusk.xml b/phpunit.dusk.xml index 60392c93245..0fe4666ae8f 100644 --- a/phpunit.dusk.xml +++ b/phpunit.dusk.xml @@ -8,6 +8,9 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false"> + + + ./tests/Browser diff --git a/phpunit.xml b/phpunit.xml index 18724d60148..4015d372f6c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -4,6 +4,9 @@ bootstrap="vendor/autoload.php" colors="true" > + + + ./tests/ diff --git a/tests/Browser/SanityTest.php b/tests/Browser/SanityTest.php index d386f98887e..1cf3bf25431 100644 --- a/tests/Browser/SanityTest.php +++ b/tests/Browser/SanityTest.php @@ -100,7 +100,6 @@ private static function cleanup() Authorize::truncate(); TopicTrack::truncate(); Genre::truncate(); - Group::truncate(); Language::truncate(); LoginAttempt::truncate(); NewsPost::truncate(); @@ -111,8 +110,6 @@ private static function cleanup() UserNotification::truncate(); UserProfileCustomization::truncate(); UserStatistics\Osu::truncate(); - - app('groups')->resetMemoized(); } private static function createScaffolding() @@ -232,7 +229,7 @@ private static function createScaffolding() ]); // factory for /g/* - self::$scaffolding['group'] = Group::factory()->create(); + self::$scaffolding['group'] = Group::first(); // factory for comments self::$scaffolding['comment'] = Comment::factory()->create([ diff --git a/tests/Controllers/BeatmapsControllerSoloScoresTest.php b/tests/Controllers/BeatmapsControllerSoloScoresTest.php index 9c00f778509..4c746135de0 100644 --- a/tests/Controllers/BeatmapsControllerSoloScoresTest.php +++ b/tests/Controllers/BeatmapsControllerSoloScoresTest.php @@ -164,7 +164,6 @@ public static function tearDownAfterClass(): void Beatmapset::truncate(); Country::truncate(); Genre::truncate(); - Group::truncate(); Language::truncate(); SoloScore::truncate(); User::truncate(); diff --git a/tests/Jobs/RemoveBeatmapsetSoloScoresTest.php b/tests/Jobs/RemoveBeatmapsetSoloScoresTest.php index 342d9ad7bec..51aef7ccba7 100644 --- a/tests/Jobs/RemoveBeatmapsetSoloScoresTest.php +++ b/tests/Jobs/RemoveBeatmapsetSoloScoresTest.php @@ -70,7 +70,6 @@ public function testHandle() Beatmapset::truncate(); Country::truncate(); Genre::truncate(); - Group::truncate(); Language::truncate(); Score::truncate(); ScorePerformance::truncate(); diff --git a/tests/Models/Solo/ScoreEsIndexTest.php b/tests/Models/Solo/ScoreEsIndexTest.php index fcc0a79d133..7b71e711867 100644 --- a/tests/Models/Solo/ScoreEsIndexTest.php +++ b/tests/Models/Solo/ScoreEsIndexTest.php @@ -105,7 +105,6 @@ public static function tearDownAfterClass(): void Beatmapset::truncate(); Country::truncate(); Genre::truncate(); - Group::truncate(); Language::truncate(); Score::truncate(); User::truncate(); diff --git a/tests/SeederExtension.php b/tests/SeederExtension.php new file mode 100644 index 00000000000..765722223fb --- /dev/null +++ b/tests/SeederExtension.php @@ -0,0 +1,38 @@ +. Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +declare(strict_types=1); + +namespace Tests; + +use App\Models\Group; +use PHPUnit\Runner\AfterLastTestHook; +use PHPUnit\Runner\BeforeFirstTestHook; + +class SeederExtension implements AfterLastTestHook, BeforeFirstTestHook +{ + public function executeAfterLastTest(): void + { + TestCase::withDbAccess(function () { + Group::truncate(); + }); + } + + public function executeBeforeFirstTest(): void + { + TestCase::withDbAccess(function () { + Group::truncate(); + foreach (Group::PRIV_GROUPS as $identifier) { + Group::create([ + 'group_desc' => '', + 'group_name' => $identifier, + 'group_type' => 2, + 'identifier' => $identifier, + 'short_name' => $identifier, + ]); + } + }); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 5d05eeff10d..fb3b10e1670 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -32,6 +32,15 @@ class TestCase extends BaseTestCase { use ArraySubsetAsserts, CreatesApplication, DatabaseTransactions; + public static function withDbAccess(callable $callback): void + { + $db = (new static())->createApplication()->make('db'); + + $callback(); + + static::resetAppDb($db); + } + protected static function reindexScores() { $search = new ScoreSearch(); @@ -54,15 +63,6 @@ protected static function resetAppDb(DatabaseManager $database): void } } - protected static function withDbAccess(callable $callback): void - { - $db = (new static())->createApplication()->make('db'); - - $callback(); - - static::resetAppDb($db); - } - protected $connectionsToTransact = [ 'mysql', 'mysql-chat', From b0b0ce7ea0a80858b07a6f8654a97c3e6b41e897 Mon Sep 17 00:00:00 2001 From: nanaya Date: Mon, 6 Nov 2023 20:51:48 +0900 Subject: [PATCH 16/34] Better name --- app/Models/Group.php | 2 +- tests/SeederExtension.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Models/Group.php b/app/Models/Group.php index aa1c2025893..c8a8090b3d9 100644 --- a/app/Models/Group.php +++ b/app/Models/Group.php @@ -37,7 +37,7 @@ class Group extends Model implements AfterCommit { // Identifier of groups which involved in permission checks // and assumed to always exist in database. - const PRIV_GROUPS = [ + const PRIV_IDENTIFIERS = [ 'admin', 'alumni', 'announce', diff --git a/tests/SeederExtension.php b/tests/SeederExtension.php index 765722223fb..41f842a32e2 100644 --- a/tests/SeederExtension.php +++ b/tests/SeederExtension.php @@ -24,7 +24,7 @@ public function executeBeforeFirstTest(): void { TestCase::withDbAccess(function () { Group::truncate(); - foreach (Group::PRIV_GROUPS as $identifier) { + foreach (Group::PRIV_IDENTIFIERS as $identifier) { Group::create([ 'group_desc' => '', 'group_name' => $identifier, From c6c2271baa95f8592aaa80758118376607c9626a Mon Sep 17 00:00:00 2001 From: nanaya Date: Mon, 6 Nov 2023 21:52:15 +0900 Subject: [PATCH 17/34] Move group creation to seeder --- database/seeders/DatabaseSeeder.php | 2 ++ database/seeders/ModelSeeders/GroupSeeder.php | 28 +++++++++++++++++++ tests/SeederExtension.php | 12 ++------ 3 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 database/seeders/ModelSeeders/GroupSeeder.php diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index aab3c70f29b..e88b56e9b29 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -18,6 +18,8 @@ class DatabaseSeeder extends Seeder public function run() { try { + $this->call(ModelSeeders\GroupSeeder::class); + // Miscellaneous Data (e.g. counts) $this->call(ModelSeeders\MiscSeeder::class); diff --git a/database/seeders/ModelSeeders/GroupSeeder.php b/database/seeders/ModelSeeders/GroupSeeder.php new file mode 100644 index 00000000000..ca9bb67a793 --- /dev/null +++ b/database/seeders/ModelSeeders/GroupSeeder.php @@ -0,0 +1,28 @@ +. Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +declare(strict_types=1); + +namespace Database\Seeders\ModelSeeders; + +use App\Models\Group; +use Illuminate\Database\Seeder; + +class GroupSeeder extends Seeder +{ + public function run(): void + { + Group::truncate(); + foreach (Group::PRIV_IDENTIFIERS as $identifier) { + Group::create([ + 'group_desc' => '', + 'group_name' => $identifier, + 'group_type' => 2, + 'identifier' => $identifier, + 'short_name' => $identifier, + ]); + } + } +} diff --git a/tests/SeederExtension.php b/tests/SeederExtension.php index 41f842a32e2..fa86a29d331 100644 --- a/tests/SeederExtension.php +++ b/tests/SeederExtension.php @@ -8,6 +8,7 @@ namespace Tests; use App\Models\Group; +use Database\Seeders\ModelSeeders\GroupSeeder; use PHPUnit\Runner\AfterLastTestHook; use PHPUnit\Runner\BeforeFirstTestHook; @@ -23,16 +24,7 @@ public function executeAfterLastTest(): void public function executeBeforeFirstTest(): void { TestCase::withDbAccess(function () { - Group::truncate(); - foreach (Group::PRIV_IDENTIFIERS as $identifier) { - Group::create([ - 'group_desc' => '', - 'group_name' => $identifier, - 'group_type' => 2, - 'identifier' => $identifier, - 'short_name' => $identifier, - ]); - } + (new GroupSeeder())->run(); }); } } From abc11e8383be26793c29d4e3ff584e27b173a196 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Wed, 8 Nov 2023 16:45:19 +0900 Subject: [PATCH 18/34] forgot the test quota was increased --- tests/Commands/ModdingRankCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Commands/ModdingRankCommandTest.php b/tests/Commands/ModdingRankCommandTest.php index 58484ab78b2..6b9dbbf77ec 100644 --- a/tests/Commands/ModdingRankCommandTest.php +++ b/tests/Commands/ModdingRankCommandTest.php @@ -65,7 +65,7 @@ public function testRankOpenIssueCounts(): void public function testRankQuota(): void { - $this->beatmapset([Ruleset::osu])->count(2)->create(); + $this->beatmapset([Ruleset::osu])->count(3)->create(); $this->expectCountChange(fn () => Beatmapset::qualified()->count(), -2); $this->expectCountChange(fn () => Beatmapset::ranked()->count(), 2); From 1637a12cb2f634ed27910a5c11341c69430293bb Mon Sep 17 00:00:00 2001 From: bakaneko Date: Wed, 8 Nov 2023 17:43:43 +0900 Subject: [PATCH 19/34] larastan infers the actual factory class --- tests/Commands/ModdingRankCommandTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Commands/ModdingRankCommandTest.php b/tests/Commands/ModdingRankCommandTest.php index 6b9dbbf77ec..677797fdf9b 100644 --- a/tests/Commands/ModdingRankCommandTest.php +++ b/tests/Commands/ModdingRankCommandTest.php @@ -10,7 +10,7 @@ use App\Models\BeatmapDiscussion; use App\Models\BeatmapMirror; use App\Models\Beatmapset; -use Database\Factories\Factory; +use Database\Factories\BeatmapsetFactory; use Tests\TestCase; class ModdingRankCommandTest extends TestCase @@ -124,10 +124,10 @@ protected function setUp(): void /** * @param Ruleset[] $rulesets - * @return Factory */ - protected function beatmapset(array $rulesets, int $qualifiedDaysAgo = 2): Factory + protected function beatmapset(array $rulesets, int $qualifiedDaysAgo = 2): BeatmapsetFactory { + $fa = Beatmapset::factory(); $factory = Beatmapset::factory() ->owner() ->qualified(now()->subDays($qualifiedDaysAgo)) From 9526058c88dd1c0cba0f5d21c49bd5a2de98f158 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Wed, 8 Nov 2023 17:51:14 +0900 Subject: [PATCH 20/34] move to scope --- app/Console/Commands/ModdingRankCommand.php | 13 ++----------- app/Models/Beatmapset.php | 10 ++++++++++ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/Console/Commands/ModdingRankCommand.php b/app/Console/Commands/ModdingRankCommand.php index e34aebbcf38..3612e4e84bb 100644 --- a/app/Console/Commands/ModdingRankCommand.php +++ b/app/Console/Commands/ModdingRankCommand.php @@ -52,7 +52,7 @@ public function handle() $this->waitRandom(); if ($this->countOnly) { - $count = $this->toBeRankedQuery($ruleset)->count(); + $count = Beatmapset::toBeRanked($ruleset)->count(); $this->info("{$ruleset->name}: {$count}"); } else { $this->rankAll($ruleset); @@ -82,7 +82,7 @@ private function rankAll(Ruleset $ruleset) $toRankLimit = min(config('osu.beatmapset.rank_per_run'), $rankableQuota); - $toBeRankedQuery = $this->toBeRankedQuery($ruleset); + $toBeRankedQuery = Beatmapset::toBeRanked($ruleset); $rankingQueue = $toBeRankedQuery->count(); $toBeRanked = $toBeRankedQuery @@ -100,15 +100,6 @@ private function rankAll(Ruleset $ruleset) } } - private function toBeRankedQuery(Ruleset $ruleset) - { - return Beatmapset::qualified() - ->withoutTrashed() - ->withModesForRanking($ruleset->value) - ->where('queued_at', '<', now()->subDays(config('osu.beatmapset.minimum_days_for_rank'))) - ->whereDoesntHave('beatmapDiscussions', fn ($query) => $query->openIssues()); - } - private function waitRandom() { if ($this->noWait || $this->countOnly) { diff --git a/app/Models/Beatmapset.php b/app/Models/Beatmapset.php index 18c64a32832..dcc9098eb7c 100644 --- a/app/Models/Beatmapset.php +++ b/app/Models/Beatmapset.php @@ -24,6 +24,7 @@ use App\Libraries\Commentable; use App\Libraries\Elasticsearch\Indexable; use App\Libraries\ImageProcessorService; +use App\Libraries\Ruleset; use App\Libraries\StorageWithUrl; use App\Libraries\Transactions\AfterCommit; use App\Traits\Memoizes; @@ -344,6 +345,15 @@ public function scopeScoreable(Builder $query): void $query->where('approved', '>', 0); } + public function scopeToBeRanked(Builder $query, Ruleset $ruleset) + { + return $query->qualified() + ->withoutTrashed() + ->withModesForRanking($ruleset->value) + ->where('queued_at', '<', now()->subDays(config('osu.beatmapset.minimum_days_for_rank'))) + ->whereDoesntHave('beatmapDiscussions', fn ($q) => $q->openIssues()); + } + public function scopeWithModesForRanking($query, $modeInts) { if (!is_array($modeInts)) { From 4d8e9da0412b6f11e9ff322b10f31fab7650873e Mon Sep 17 00:00:00 2001 From: bakaneko Date: Wed, 8 Nov 2023 18:57:42 +0900 Subject: [PATCH 21/34] print all ranking queue info --- app/Console/Commands/ModdingRankCommand.php | 45 ++++++++++++--------- tests/Commands/ModdingRankCommandTest.php | 16 ++++++-- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/app/Console/Commands/ModdingRankCommand.php b/app/Console/Commands/ModdingRankCommand.php index 3612e4e84bb..85b219f7dcf 100644 --- a/app/Console/Commands/ModdingRankCommand.php +++ b/app/Console/Commands/ModdingRankCommand.php @@ -28,6 +28,21 @@ class ModdingRankCommand extends Command private bool $countOnly = false; private bool $noWait = false; + public static function getStats(Ruleset $ruleset) + { + $rankedTodayCount = Beatmapset::ranked() + ->withoutTrashed() + ->withModesForRanking($ruleset->value) + ->where('approved_date', '>=', now()->subDays()) + ->count(); + + return [ + 'availableQuota' => config('osu.beatmapset.rank_per_day') - $rankedTodayCount, + 'inQueue' => Beatmapset::toBeRanked($ruleset)->count(), + 'rankedToday' => $rankedTodayCount, + ]; + } + /** * Execute the console command. * @@ -52,8 +67,12 @@ public function handle() $this->waitRandom(); if ($this->countOnly) { - $count = Beatmapset::toBeRanked($ruleset)->count(); - $this->info("{$ruleset->name}: {$count}"); + $stats = static::getStats($ruleset); + $this->info($ruleset->name); + foreach ($stats as $key => $value) { + $this->line("{$key}: {$value}"); + } + $this->newLine(); } else { $this->rankAll($ruleset); } @@ -65,32 +84,22 @@ public function handle() private function rankAll(Ruleset $ruleset) { $this->info("Ranking beatmapsets with at least mode: {$ruleset->name}"); + $stats = static::getStats($ruleset); - $rankedTodayCount = Beatmapset::ranked() - ->withoutTrashed() - ->withModesForRanking($ruleset->value) - ->where('approved_date', '>=', now()->subDays()) - ->count(); - - $rankableQuota = config('osu.beatmapset.rank_per_day') - $rankedTodayCount; + $this->info("{$stats['rankedToday']} beatmapsets ranked last 24 hours. Can rank {$stats['availableQuota']} more"); - $this->info("{$rankedTodayCount} beatmapsets ranked last 24 hours. Can rank {$rankableQuota} more"); - - if ($rankableQuota <= 0) { + if ($stats['availableQuota'] <= 0) { return; } - $toRankLimit = min(config('osu.beatmapset.rank_per_run'), $rankableQuota); - - $toBeRankedQuery = Beatmapset::toBeRanked($ruleset); + $toRankLimit = min(config('osu.beatmapset.rank_per_run'), $stats['availableQuota']); - $rankingQueue = $toBeRankedQuery->count(); - $toBeRanked = $toBeRankedQuery + $toBeRanked = Beatmapset::tobeRanked($ruleset) ->orderBy('queued_at', 'ASC') ->limit($toRankLimit) ->get(); - $this->info("{$rankingQueue} beatmapset(s) in ranking queue"); + $this->info("{$stats['inQueue']} beatmapset(s) in ranking queue"); $this->info("Ranking {$toBeRanked->count()} beatmapset(s)"); foreach ($toBeRanked as $beatmapset) { diff --git a/tests/Commands/ModdingRankCommandTest.php b/tests/Commands/ModdingRankCommandTest.php index 677797fdf9b..bb3d6b30558 100644 --- a/tests/Commands/ModdingRankCommandTest.php +++ b/tests/Commands/ModdingRankCommandTest.php @@ -5,6 +5,7 @@ namespace Tests\Commands; +use App\Console\Commands\ModdingRankCommand; use App\Libraries\Ruleset; use App\Models\Beatmap; use App\Models\BeatmapDiscussion; @@ -15,6 +16,15 @@ class ModdingRankCommandTest extends TestCase { + public function testCountOnly(): void + { + $this->beatmapset([Ruleset::osu])->create(); + + $this->expectCountChange(fn () => Beatmapset::ranked()->count(), 0); + + $this->artisan('modding:rank', ['--count-only' => true]); + } + /** * @dataProvider rankDataProvider */ @@ -36,9 +46,8 @@ public function testRankHybrid(array $beatmapsetRulesets, array $expectedCounts) $this->beatmapset($rulesets)->create(); } - $command = $this->artisan('modding:rank', ['--count-only' => true]); foreach (Ruleset::cases() as $ruleset) { - $command->expectsOutputToContain("{$ruleset->name}: {$expectedCounts[$ruleset->value]}"); + $this->assertSame($expectedCounts[$ruleset->value], ModdingRankCommand::getStats($ruleset)['inQueue']); } } @@ -59,8 +68,7 @@ public function testRankOpenIssueCounts(): void ->has(BeatmapDiscussion::factory()->general()->problem()) ->create(); - $command = $this->artisan('modding:rank', ['--count-only' => true]); - $command->expectsOutputToContain(Ruleset::osu->name.': 0'); + $this->assertSame(0, ModdingRankCommand::getStats(Ruleset::osu)['inQueue']); } public function testRankQuota(): void From 70b212031936a5e4e4fffaedb50e37e178803da8 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 9 Nov 2023 17:36:06 +0900 Subject: [PATCH 22/34] cool story --- tests/Commands/ModdingRankCommandTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Commands/ModdingRankCommandTest.php b/tests/Commands/ModdingRankCommandTest.php index 37dbcce41c2..597488862a0 100644 --- a/tests/Commands/ModdingRankCommandTest.php +++ b/tests/Commands/ModdingRankCommandTest.php @@ -135,7 +135,6 @@ protected function setUp(): void */ protected function beatmapset(array $rulesets, int $qualifiedDaysAgo = 2): BeatmapsetFactory { - $fa = Beatmapset::factory(); $factory = Beatmapset::factory() ->owner() ->qualified(now()->subDays($qualifiedDaysAgo)) From 2ea3440e40b7e464b33cd7d410c932e3f10a6eee Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 9 Nov 2023 17:37:21 +0900 Subject: [PATCH 23/34] also should be BeatmapsetFactory --- tests/Models/BeatmapsetTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/Models/BeatmapsetTest.php b/tests/Models/BeatmapsetTest.php index f460cd41aca..c41b4c25e36 100644 --- a/tests/Models/BeatmapsetTest.php +++ b/tests/Models/BeatmapsetTest.php @@ -19,6 +19,7 @@ use App\Models\Notification; use App\Models\User; use App\Models\UserNotification; +use Database\Factories\BeatmapsetFactory; use Database\Factories\Factory; use Queue; use Tests\TestCase; @@ -488,10 +489,7 @@ public function rankWithOpenIssueDataProvider() ]; } - /** - * @return Factory - */ - private function beatmapsetFactory(): Factory + private function beatmapsetFactory(): BeatmapsetFactory { BeatmapMirror::factory()->default()->create(); From d70ecfa519ec18df42abf2ae0a9e64e0b390b3dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 9 Nov 2023 18:06:19 +0900 Subject: [PATCH 24/34] Remove redundant mod validation --- app/Libraries/Mods.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/Libraries/Mods.php b/app/Libraries/Mods.php index cc107c032b0..f75e96ef1e3 100644 --- a/app/Libraries/Mods.php +++ b/app/Libraries/Mods.php @@ -114,8 +114,6 @@ public function assertValidForMultiplayer(int $rulesetId, array $ids, bool $isRe public function excludeModsAlwaysValidForSubmission(int $rulesetId, array $ids): array { - $this->validateSelection($rulesetId, $ids); - $rulesetMods = $this->mods[$rulesetId]; return collect($ids) From b416fbe2a0f4b81150d5e818a233ed358a3fc27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 9 Nov 2023 18:17:22 +0900 Subject: [PATCH 25/34] Simplify method --- app/Libraries/Mods.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/Libraries/Mods.php b/app/Libraries/Mods.php index f75e96ef1e3..e8a3ce97f86 100644 --- a/app/Libraries/Mods.php +++ b/app/Libraries/Mods.php @@ -112,15 +112,9 @@ public function assertValidForMultiplayer(int $rulesetId, array $ids, bool $isRe } } - public function excludeModsAlwaysValidForSubmission(int $rulesetId, array $ids): array + public function excludeModsAlwaysValidForSubmission(int $rulesetId, array $modIds): array { - $rulesetMods = $this->mods[$rulesetId]; - - return collect($ids) - ->filter(function ($id) use ($rulesetMods) { - return !$rulesetMods[$id]['AlwaysValidForSubmission']; - }) - ->all(); + return array_values(array_filter($modIds, fn ($modId) => !$this->mods[$rulesetId][$modId]['AlwaysValidForSubmission'])); } public function idsToBitset($ids): int From 6d9e84259f562b14acf6e0bcb9ed721d9eb36f91 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 9 Nov 2023 18:23:35 +0900 Subject: [PATCH 26/34] fake the jobs --- tests/Commands/ModdingRankCommandTest.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/Commands/ModdingRankCommandTest.php b/tests/Commands/ModdingRankCommandTest.php index 597488862a0..26edbe0e4e2 100644 --- a/tests/Commands/ModdingRankCommandTest.php +++ b/tests/Commands/ModdingRankCommandTest.php @@ -7,11 +7,14 @@ use App\Console\Commands\ModdingRankCommand; use App\Enums\Ruleset; +use App\Jobs\Notifications\BeatmapsetRank; use App\Models\Beatmap; use App\Models\BeatmapDiscussion; use App\Models\BeatmapMirror; use App\Models\Beatmapset; +use Bus; use Database\Factories\BeatmapsetFactory; +use Illuminate\Bus\PendingBatch; use Tests\TestCase; class ModdingRankCommandTest extends TestCase @@ -23,6 +26,8 @@ public function testCountOnly(): void $this->expectCountChange(fn () => Beatmapset::ranked()->count(), 0); $this->artisan('modding:rank', ['--count-only' => true]); + + Bus::assertNotDispatched(BeatmapsetRank::class); } /** @@ -35,6 +40,8 @@ public function testRank(int $qualifiedDaysAgo, int $expected): void $this->expectCountChange(fn () => Beatmapset::ranked()->count(), $expected); $this->artisan('modding:rank', ['--no-wait' => true]); + + Bus::assertDispatched(BeatmapsetRank::class, $expected); } /** @@ -60,6 +67,8 @@ public function testRankOpenIssue(): void $this->expectCountChange(fn () => Beatmapset::ranked()->count(), 0); $this->artisan('modding:rank', ['--no-wait' => true]); + + Bus::assertNotDispatched(BeatmapsetRank::class); } public function testRankOpenIssueCounts(): void @@ -79,6 +88,8 @@ public function testRankQuota(): void $this->expectCountChange(fn () => Beatmapset::ranked()->count(), 2); $this->artisan('modding:rank', ['--no-wait' => true]); + + Bus::assertDispatched(BeatmapsetRank::class); } public function testRankQuotaSeparateRuleset(): void @@ -87,9 +98,12 @@ public function testRankQuotaSeparateRuleset(): void $this->beatmapset([$ruleset])->create(); } - $this->expectCountChange(fn () => Beatmapset::ranked()->count(), count(Ruleset::cases())); + $count = count(Ruleset::cases()); + $this->expectCountChange(fn () => Beatmapset::ranked()->count(), $count); $this->artisan('modding:rank', ['--no-wait' => true]); + + Bus::assertDispatched(BeatmapsetRank::class, $count); } @@ -127,7 +141,7 @@ protected function setUp(): void config()->set('osu.beatmapset.minimum_days_for_rank', 1); config()->set('osu.beatmapset.rank_per_day', 2); - BeatmapMirror::factory()->default()->create(); + Bus::fake(); } /** From a60c45ba53dedc3d2579e10f13e0a31a5e3c6088 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 9 Nov 2023 18:25:01 +0900 Subject: [PATCH 27/34] unneeded count output test --- tests/Commands/ModdingRankCommandTest.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/Commands/ModdingRankCommandTest.php b/tests/Commands/ModdingRankCommandTest.php index 26edbe0e4e2..577041248b5 100644 --- a/tests/Commands/ModdingRankCommandTest.php +++ b/tests/Commands/ModdingRankCommandTest.php @@ -71,15 +71,6 @@ public function testRankOpenIssue(): void Bus::assertNotDispatched(BeatmapsetRank::class); } - public function testRankOpenIssueCounts(): void - { - $this->beatmapset([Ruleset::osu]) - ->has(BeatmapDiscussion::factory()->general()->problem()) - ->create(); - - $this->assertSame(0, ModdingRankCommand::getStats(Ruleset::osu)['inQueue']); - } - public function testRankQuota(): void { $this->beatmapset([Ruleset::osu])->count(3)->create(); From 5f181d05f322257da10a7b75200a75a81933e713 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 9 Nov 2023 18:27:02 +0900 Subject: [PATCH 28/34] explicitly use the counting versions --- tests/Commands/ModdingRankCommandTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Commands/ModdingRankCommandTest.php b/tests/Commands/ModdingRankCommandTest.php index 577041248b5..cfb2b18ef30 100644 --- a/tests/Commands/ModdingRankCommandTest.php +++ b/tests/Commands/ModdingRankCommandTest.php @@ -41,7 +41,7 @@ public function testRank(int $qualifiedDaysAgo, int $expected): void $this->artisan('modding:rank', ['--no-wait' => true]); - Bus::assertDispatched(BeatmapsetRank::class, $expected); + Bus::assertDispatchedTimes(BeatmapsetRank::class, $expected); } /** @@ -94,7 +94,7 @@ public function testRankQuotaSeparateRuleset(): void $this->artisan('modding:rank', ['--no-wait' => true]); - Bus::assertDispatched(BeatmapsetRank::class, $count); + Bus::assertDispatchedTimes(BeatmapsetRank::class, $count); } From e6dec62284e78963ed692a4475998a9fc9f290a4 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 9 Nov 2023 18:47:29 +0900 Subject: [PATCH 29/34] assert the correct job --- tests/Commands/ModdingRankCommandTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Commands/ModdingRankCommandTest.php b/tests/Commands/ModdingRankCommandTest.php index cfb2b18ef30..67b7f4bd357 100644 --- a/tests/Commands/ModdingRankCommandTest.php +++ b/tests/Commands/ModdingRankCommandTest.php @@ -7,6 +7,7 @@ use App\Console\Commands\ModdingRankCommand; use App\Enums\Ruleset; +use App\Jobs\CheckBeatmapsetCovers; use App\Jobs\Notifications\BeatmapsetRank; use App\Models\Beatmap; use App\Models\BeatmapDiscussion; @@ -27,6 +28,7 @@ public function testCountOnly(): void $this->artisan('modding:rank', ['--count-only' => true]); + Bus::assertNotDispatched(CheckBeatmapsetCovers::class); Bus::assertNotDispatched(BeatmapsetRank::class); } @@ -41,6 +43,7 @@ public function testRank(int $qualifiedDaysAgo, int $expected): void $this->artisan('modding:rank', ['--no-wait' => true]); + Bus::assertDispatchedTimes(CheckBeatmapsetCovers::class, $expected); Bus::assertDispatchedTimes(BeatmapsetRank::class, $expected); } @@ -68,6 +71,7 @@ public function testRankOpenIssue(): void $this->artisan('modding:rank', ['--no-wait' => true]); + Bus::assertNotDispatched(CheckBeatmapsetCovers::class); Bus::assertNotDispatched(BeatmapsetRank::class); } @@ -80,6 +84,7 @@ public function testRankQuota(): void $this->artisan('modding:rank', ['--no-wait' => true]); + Bus::assertDispatched(CheckBeatmapsetCovers::class); Bus::assertDispatched(BeatmapsetRank::class); } @@ -94,6 +99,7 @@ public function testRankQuotaSeparateRuleset(): void $this->artisan('modding:rank', ['--no-wait' => true]); + Bus::assertDispatchedTimes(CheckBeatmapsetCovers::class, $count); Bus::assertDispatchedTimes(BeatmapsetRank::class, $count); } From b31a5db6e3b316991b35721872be203bc5a3a7bd Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 9 Nov 2023 19:12:20 +0900 Subject: [PATCH 30/34] cleanup some other things --- tests/Commands/ModdingRankCommandTest.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/Commands/ModdingRankCommandTest.php b/tests/Commands/ModdingRankCommandTest.php index 67b7f4bd357..132751b976b 100644 --- a/tests/Commands/ModdingRankCommandTest.php +++ b/tests/Commands/ModdingRankCommandTest.php @@ -11,11 +11,9 @@ use App\Jobs\Notifications\BeatmapsetRank; use App\Models\Beatmap; use App\Models\BeatmapDiscussion; -use App\Models\BeatmapMirror; use App\Models\Beatmapset; use Bus; use Database\Factories\BeatmapsetFactory; -use Illuminate\Bus\PendingBatch; use Tests\TestCase; class ModdingRankCommandTest extends TestCase @@ -138,7 +136,7 @@ protected function setUp(): void config()->set('osu.beatmapset.minimum_days_for_rank', 1); config()->set('osu.beatmapset.rank_per_day', 2); - Bus::fake(); + Bus::fake([BeatmapsetRank::class, CheckBeatmapsetCovers::class]); } /** @@ -148,8 +146,7 @@ protected function beatmapset(array $rulesets, int $qualifiedDaysAgo = 2): Beatm { $factory = Beatmapset::factory() ->owner() - ->qualified(now()->subDays($qualifiedDaysAgo)) - ->state(['download_disabled' => true]); + ->qualified(now()->subDays($qualifiedDaysAgo)); foreach ($rulesets as $ruleset) { $factory = $factory->has(Beatmap::factory()->ruleset($ruleset)); From 66a637b2e8e5ae9dfb5c3df6aa55b933255b7413 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 9 Nov 2023 19:16:27 +0900 Subject: [PATCH 31/34] also fake job for BeatmapsetTest --- tests/Models/BeatmapsetTest.php | 52 ++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/tests/Models/BeatmapsetTest.php b/tests/Models/BeatmapsetTest.php index c41b4c25e36..4a27c14a39b 100644 --- a/tests/Models/BeatmapsetTest.php +++ b/tests/Models/BeatmapsetTest.php @@ -7,6 +7,7 @@ use App\Enums\Ruleset; use App\Exceptions\AuthorizationException; +use App\Jobs\CheckBeatmapsetCovers; use App\Jobs\Notifications\BeatmapsetDisqualify; use App\Jobs\Notifications\BeatmapsetResetNominations; use App\Models\Beatmap; @@ -19,6 +20,7 @@ use App\Models\Notification; use App\Models\User; use App\Models\UserNotification; +use Bus; use Database\Factories\BeatmapsetFactory; use Database\Factories\Factory; use Queue; @@ -43,6 +45,8 @@ public function testLove() $this->assertSame($userNotifications + 1, UserNotification::count()); $this->assertTrue($beatmapset->fresh()->isLoved()); $this->assertSame('loved', $beatmapset->beatmaps()->first()->status()); + + Bus::assertDispatched(CheckBeatmapsetCovers::class); } public function testLoveBeatmapApprovedStates(): void @@ -66,6 +70,8 @@ public function testLoveBeatmapApprovedStates(): void $this->assertSame('graveyard', $pendingBeatmap->fresh()->status()); $this->assertSame('graveyard', $wipBeatmap->fresh()->status()); $this->assertSame('ranked', $rankedBeatmap->fresh()->status()); + + Bus::assertDispatched(CheckBeatmapsetCovers::class); } // region single-playmode beatmap sets @@ -116,6 +122,8 @@ public function testQualify() $this->assertSame($notifications + 1, Notification::count()); $this->assertSame($userNotifications + 1, UserNotification::count()); $this->assertTrue($beatmapset->fresh()->isQualified()); + + Bus::assertDispatched(CheckBeatmapsetCovers::class); } public function testLimitedBNGQualifyingNominationBNGNominated() @@ -131,6 +139,8 @@ public function testLimitedBNGQualifyingNominationBNGNominated() $this->assertTrue($result['result']); $this->assertTrue($beatmapset->fresh()->isQualified()); + + Bus::assertDispatched(CheckBeatmapsetCovers::class); } public function testLimitedBNGQualifyingNominationNATNominated() @@ -146,6 +156,8 @@ public function testLimitedBNGQualifyingNominationNATNominated() $this->assertTrue($result['result']); $this->assertTrue($beatmapset->fresh()->isQualified()); + + Bus::assertDispatched(CheckBeatmapsetCovers::class); } public function testLimitedBNGQualifyingNominationLimitedBNGNominated() @@ -158,6 +170,8 @@ public function testLimitedBNGQualifyingNominationLimitedBNGNominated() $this->assertFalse($beatmapset->isQualified()); $beatmapset->nominate($nominator); $this->assertFalse($beatmapset->isQualified()); + + Bus::assertNotDispatched(CheckBeatmapsetCovers::class); } public function testNominateWithDefaultMetadata() { @@ -197,6 +211,12 @@ public function testRank(string $state, bool $success): void $this->assertSame($success, $res); $this->assertSame($success, $beatmapset->fresh()->isRanked()); + + if ($success) { + Bus::assertDispatched(CheckBeatmapsetCovers::class); + } else { + Bus::assertNotDispatched(CheckBeatmapsetCovers::class); + } } /** @@ -210,6 +230,8 @@ public function testRankWithOpenIssue(string $type): void $this->assertTrue($beatmapset->isQualified()); $this->assertFalse($beatmapset->rank()); + + Bus::assertNotDispatched(CheckBeatmapsetCovers::class); } public function testGlobalScopeActive() @@ -287,6 +309,8 @@ public function testHybridLegacyQualify(): void $this->assertSame($notifications + 1, Notification::count()); $this->assertSame($userNotifications + 1, UserNotification::count()); $this->assertTrue($beatmapset->fresh()->isQualified()); + + Bus::assertDispatched(CheckBeatmapsetCovers::class); } public function testHybridNominateWithNullPlaymode(): void @@ -308,6 +332,8 @@ public function testHybridNominateWithNullPlaymode(): void $this->assertSame($notifications, Notification::count()); $this->assertSame($userNotifications, UserNotification::count()); $this->assertTrue($beatmapset->fresh()->isPending()); + + Bus::assertNotDispatched(CheckBeatmapsetCovers::class); } public function testHybridNominateWithNoPlaymodePermission(): void @@ -329,6 +355,8 @@ public function testHybridNominateWithNoPlaymodePermission(): void $this->assertSame($notifications, Notification::count()); $this->assertSame($userNotifications, UserNotification::count()); $this->assertTrue($beatmapset->fresh()->isPending()); + + Bus::assertNotDispatched(CheckBeatmapsetCovers::class); } public function testHybridNominateWithPlaymodePermissionSingleMode(): void @@ -348,6 +376,8 @@ public function testHybridNominateWithPlaymodePermissionSingleMode(): void $this->assertSame($notifications + 1, Notification::count()); $this->assertSame($userNotifications + 1, UserNotification::count()); $this->assertTrue($beatmapset->fresh()->isPending()); + + Bus::assertNotDispatched(CheckBeatmapsetCovers::class); } public function testHybridNominateWithPlaymodePermissionTooMany(): void @@ -365,6 +395,8 @@ public function testHybridNominateWithPlaymodePermissionTooMany(): void $this->assertFalse($result['result']); $this->assertSame($result['message'], osu_trans('beatmaps.nominations.too_many')); $this->assertTrue($beatmapset->fresh()->isPending()); + + Bus::assertNotDispatched(CheckBeatmapsetCovers::class); } public function testHybridNominateWithPlaymodePermissionMultipleModes(): void @@ -384,6 +416,8 @@ public function testHybridNominateWithPlaymodePermissionMultipleModes(): void $this->assertSame($notifications + 1, Notification::count()); $this->assertSame($userNotifications + 1, UserNotification::count()); $this->assertTrue($beatmapset->fresh()->isPending()); + + Bus::assertNotDispatched(CheckBeatmapsetCovers::class); } public function testHybridNominationBNGQualifyingBNGNominatedPartial(): void @@ -398,6 +432,8 @@ public function testHybridNominationBNGQualifyingBNGNominatedPartial(): void $this->assertTrue($result['result']); $this->assertFalse($beatmapset->fresh()->isQualified()); + + Bus::assertNotDispatched(CheckBeatmapsetCovers::class); } public function testHybridNominationLimitedBNGQualifyingLimitedBNGNominated(): void @@ -413,6 +449,8 @@ public function testHybridNominationLimitedBNGQualifyingLimitedBNGNominated(): v $this->assertFalse($result['result']); $this->assertSame($result['message'], osu_trans('beatmapsets.nominate.full_bn_required')); $this->assertTrue($beatmapset->fresh()->isPending()); + + Bus::assertNotDispatched(CheckBeatmapsetCovers::class); } public function testHybridNominationLimitedBNGQualifyingBNGNominated(): void @@ -427,6 +465,8 @@ public function testHybridNominationLimitedBNGQualifyingBNGNominated(): void $this->assertTrue($result['result']); $this->assertTrue($beatmapset->fresh()->isQualified()); + + Bus::assertDispatched(CheckBeatmapsetCovers::class); } public function testHybridNominationBNGQualifyingLimitedBNGNominated(): void @@ -441,6 +481,8 @@ public function testHybridNominationBNGQualifyingLimitedBNGNominated(): void $this->assertTrue($result['result']); $this->assertTrue($beatmapset->fresh()->isQualified()); + + Bus::assertDispatched(CheckBeatmapsetCovers::class); } //end region @@ -491,23 +533,17 @@ public function rankWithOpenIssueDataProvider() private function beatmapsetFactory(): BeatmapsetFactory { - BeatmapMirror::factory()->default()->create(); - return Beatmapset::factory() ->owner() ->pending() - ->state(['download_disabled' => true]) ->has(Beatmap::factory()->state(fn (array $attr, Beatmapset $set) => ['user_id' => $set->user_id])); } private function createHybridBeatmapset($rulesets = [Ruleset::osu, Ruleset::taiko]): Beatmapset { - BeatmapMirror::factory()->default()->create(); - $beatmapset = Beatmapset::factory() ->owner() - ->pending() - ->state(['download_disabled' => true]); + ->pending(); foreach ($rulesets as $ruleset) { $beatmapset = $beatmapset->has( @@ -535,5 +571,7 @@ protected function setUp(): void Genre::factory()->create(['genre_id' => Genre::UNSPECIFIED]); Language::factory()->create(['language_id' => Language::UNSPECIFIED]); + + Bus::fake([CheckBeatmapsetCovers::class]); } } From bb6ee4a948c306276acce272705e98620b52aafa Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 9 Nov 2023 19:22:35 +0900 Subject: [PATCH 32/34] use DateTimeInterface instead --- database/factories/BeatmapsetFactory.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/database/factories/BeatmapsetFactory.php b/database/factories/BeatmapsetFactory.php index 21e699807e5..c25f2b44840 100644 --- a/database/factories/BeatmapsetFactory.php +++ b/database/factories/BeatmapsetFactory.php @@ -13,7 +13,6 @@ use App\Models\Genre; use App\Models\Language; use App\Models\User; -use Carbon\Carbon; class BeatmapsetFactory extends Factory { @@ -71,7 +70,7 @@ public function pending() ]); } - public function qualified(?Carbon $approvedAt = null) + public function qualified(?\DateTimeInterface $approvedAt = null) { $approvedAt ??= now(); From 0e4d11b5e64b905f4c9788329e80c791691319e8 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 9 Nov 2023 19:23:38 +0900 Subject: [PATCH 33/34] lint fix --- tests/Models/BeatmapsetTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Models/BeatmapsetTest.php b/tests/Models/BeatmapsetTest.php index 4a27c14a39b..2100e92068d 100644 --- a/tests/Models/BeatmapsetTest.php +++ b/tests/Models/BeatmapsetTest.php @@ -12,7 +12,6 @@ use App\Jobs\Notifications\BeatmapsetResetNominations; use App\Models\Beatmap; use App\Models\BeatmapDiscussion; -use App\Models\BeatmapMirror; use App\Models\Beatmapset; use App\Models\BeatmapsetNomination; use App\Models\Genre; @@ -22,7 +21,6 @@ use App\Models\UserNotification; use Bus; use Database\Factories\BeatmapsetFactory; -use Database\Factories\Factory; use Queue; use Tests\TestCase; From 18310bce86b600fbd219d6f57f07c372b2a49285 Mon Sep 17 00:00:00 2001 From: Sam Verhaegen Date: Thu, 9 Nov 2023 12:29:07 +0200 Subject: [PATCH 34/34] add `title` and `source` beatmapset filters --- app/Libraries/Search/BeatmapsetQueryParser.php | 6 ++++++ app/Libraries/Search/BeatmapsetSearch.php | 2 ++ app/Libraries/Search/BeatmapsetSearchParams.php | 2 ++ app/Libraries/Search/BeatmapsetSearchRequestParams.php | 2 ++ 4 files changed, 12 insertions(+) diff --git a/app/Libraries/Search/BeatmapsetQueryParser.php b/app/Libraries/Search/BeatmapsetQueryParser.php index 4f948e0556a..cde2db1e04d 100644 --- a/app/Libraries/Search/BeatmapsetQueryParser.php +++ b/app/Libraries/Search/BeatmapsetQueryParser.php @@ -70,6 +70,12 @@ public static function parse(?string $query): array case 'artist': $option = static::makeTextOption($op, $m['value']); break; + case 'source': + $option = static::makeTextOption($op, $m['value']); + break; + case 'title': + $option = static::makeTextOption($op, $m['value']); + break; case 'created': $option = static::makeDateRangeOption($op, $m['value']); break; diff --git a/app/Libraries/Search/BeatmapsetSearch.php b/app/Libraries/Search/BeatmapsetSearch.php index 15e710d6727..08b888acf52 100644 --- a/app/Libraries/Search/BeatmapsetSearch.php +++ b/app/Libraries/Search/BeatmapsetSearch.php @@ -89,6 +89,8 @@ public function getQuery() $this->addSimpleFilters($query, $nested); $this->addCreatorFilter($query, $nested); $this->addTextFilter($query, 'artist', ['artist', 'artist_unicode']); + $this->addTextFilter($query, 'source', ['source']); + $this->addTextFilter($query, 'title', ['title', 'title_unicode']); $query->filter([ 'nested' => [ diff --git a/app/Libraries/Search/BeatmapsetSearchParams.php b/app/Libraries/Search/BeatmapsetSearchParams.php index 718f21cf67e..3dd28c822e4 100644 --- a/app/Libraries/Search/BeatmapsetSearchParams.php +++ b/app/Libraries/Search/BeatmapsetSearchParams.php @@ -39,7 +39,9 @@ class BeatmapsetSearchParams extends SearchParams public bool $showFollows = false; public bool $showRecommended = false; public bool $showSpotlights = false; + public ?string $source = null; public ?string $status = null; + public ?string $title = null; public ?array $statusRange = null; public ?array $hitLength = null; public ?array $updated = null; diff --git a/app/Libraries/Search/BeatmapsetSearchRequestParams.php b/app/Libraries/Search/BeatmapsetSearchRequestParams.php index 7d35cdfa3b8..5a7caec403a 100644 --- a/app/Libraries/Search/BeatmapsetSearchRequestParams.php +++ b/app/Libraries/Search/BeatmapsetSearchRequestParams.php @@ -225,8 +225,10 @@ private function parseQuery(): void 'length' => 'hitLength', 'od' => 'accuracy', 'ranked' => 'ranked', + 'source' => 'source', 'stars' => 'difficultyRating', 'status' => 'statusRange', + 'title' => 'title', 'updated' => 'updated', ];