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}