the browser-facing portion of osu!
at master 136 lines 4.4 kB view raw
1<?php 2 3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 4// See the LICENCE file in the repository root for full licence text. 5 6declare(strict_types=1); 7 8namespace App\Libraries\Beatmapset; 9 10use App\Models\Beatmap; 11use App\Models\Beatmapset; 12use Ds\Set; 13 14class BeatmapsetMainRuleset 15{ 16 /** @var ?Set<string> $eligibleRulesets */ 17 private ?Set $eligibleRulesets = null; 18 19 public function __construct(private Beatmapset $beatmapset) 20 { 21 $values = $beatmapset->eligible_main_rulesets; 22 23 if ($values !== null) { 24 $this->eligibleRulesets = new Set($values); 25 } else { 26 $this->populateEligibleRulesets(); 27 } 28 } 29 30 /** 31 * Gets all the Rulesets that are eligible to be the main ruleset. 32 * This will additionally query the current beatmapset nominations if necessary. 33 * 34 * @return Set<string> 35 */ 36 public function currentEligible(): Set 37 { 38 $mainRuleset = $this->mainRuleset(); 39 40 return $mainRuleset === null ? $this->eligibleRulesets : new Set([$mainRuleset]); 41 } 42 43 public function currentEligibleSorted(): array 44 { 45 $array = $this->currentEligible()->toArray(); 46 sort($array); 47 48 return $array; 49 } 50 51 private function baseQuery() 52 { 53 return $this->beatmapset->beatmaps() 54 ->groupBy('playmode') 55 ->select('playmode', \DB::raw('count(*) as total')) 56 ->orderBy('total', 'desc') 57 ->orderBy('playmode', 'asc'); 58 } 59 60 private function mainRuleset(): ?string 61 { 62 if ($this->eligibleRulesets->count() === 1) { 63 return $this->eligibleRulesets->first(); 64 } 65 66 // First mode with more nominations that others becomes main mode. 67 // Implicity implies that limited BN nominations becomes main mode. 68 $nominations = $this->beatmapset->beatmapsetNominations()->current()->orderBy('id', 'asc')->get(); 69 $nominationsByRuleset = []; 70 71 foreach ($nominations as $nomination) { 72 $rulesets = $nomination->modes ?? $this->beatmapset->playmodesStr(); 73 foreach ($rulesets as $ruleset) { 74 if ($this->eligibleRulesets->contains($ruleset)) { 75 $nominationsByRuleset[$ruleset] ??= 0; 76 $nominationsByRuleset[$ruleset]++; 77 } 78 } 79 80 // bailout as soon as there is a clear winner 81 $nominatedRulesetsCount = count($nominationsByRuleset); 82 if ($nominatedRulesetsCount === 1) { 83 return array_keys($nominationsByRuleset)[0]; 84 } else if ($nominatedRulesetsCount > 1) { 85 arsort($nominationsByRuleset); 86 $values = array_values($nominationsByRuleset); 87 if ($values[0] > $values[1]) { 88 return array_keys($nominationsByRuleset)[0]; 89 } 90 } 91 } 92 93 return null; 94 } 95 96 private function populateEligibleRulesets(): void 97 { 98 $this->eligibleRulesets = new Set(); 99 $groups = $this->baseQuery()->get()->map->getAttributes(); 100 $groupsCount = $groups->count(); 101 102 // where's the beatmaps??? 103 if ($groupsCount === 0) { 104 return; 105 } 106 107 // clear winner in playmode counts exists. 108 if ($groups->count() === 1 || $groups[0]['total'] > $groups[1]['total']) { 109 $this->eligibleRulesets->add(Beatmap::modeStr($groups[0]['playmode'])); 110 111 return; 112 } 113 114 // maps by host mapper 115 $groupedHostOnly = $this->baseQuery()->where('user_id', $this->beatmapset->user_id)->get()->map->getAttributes(); 116 117 // clear mode with most maps by host 118 if ( 119 $groupedHostOnly->count() === 1 120 || $groupedHostOnly->count() > 1 121 && $groupedHostOnly[0]['total'] > $groupedHostOnly[1]['total'] 122 ) { 123 $this->eligibleRulesets->add(Beatmap::modeStr($groupedHostOnly[0]['playmode'])); 124 125 return; 126 } 127 128 // filter out to only modes with same highest counts. 129 $this->eligibleRulesets->add( 130 ...$groupedHostOnly 131 ->filter(fn ($group) => $group['total'] === $groupedHostOnly[0]['total']) 132 ->map(fn ($group) => Beatmap::modeStr($group['playmode'])) 133 ->toArray() 134 ); 135 } 136}