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
6namespace App\Models\Score;
7
8use App\Enums\Ruleset;
9use App\Exceptions\ClassNotFoundException;
10use App\Libraries\Mods;
11use App\Models\Beatmap;
12use App\Models\Model as BaseModel;
13use App\Models\Solo\ScoreData;
14use App\Models\Traits\Scoreable;
15use App\Models\User;
16
17/**
18 * @property Beatmap $beatmap
19 * @property User $user
20 */
21abstract class Model extends BaseModel
22{
23 use Scoreable;
24
25 public $timestamps = false;
26
27 protected $casts = [
28 'date' => 'datetime',
29 'pass' => 'bool',
30 'perfect' => 'bool',
31 'replay' => 'bool', // for best model
32 ];
33 protected $primaryKey = 'score_id';
34
35 public static function getClassByRulesetId(int $rulesetId): ?string
36 {
37 $ruleset = Beatmap::modeStr($rulesetId);
38
39 if ($ruleset !== null) {
40 return static::getClass($ruleset);
41 }
42
43 return null;
44 }
45
46 public static function getClass(string $ruleset): string
47 {
48 if (!Beatmap::isModeValid($ruleset)) {
49 throw new ClassNotFoundException();
50 }
51
52 return get_class_namespace(static::class).'\\'.studly_case($ruleset);
53 }
54
55 public function scopeDefault($query)
56 {
57 return $query
58 ->whereHas('beatmap.beatmapset')
59 ->orderBy('score_id', 'desc');
60 }
61
62 public function scopeForUser($query, User $user)
63 {
64 return $query->where('user_id', $user->user_id);
65 }
66
67 public function scopeIncludeFails($query, bool $include)
68 {
69 if ($include) {
70 return $query;
71 }
72
73 return $query->where('pass', true);
74 }
75
76 public function scopeVisibleUsers($query)
77 {
78 return $query->whereHas('user', function ($userQuery) {
79 $userQuery->default();
80 });
81 }
82
83 public function scopeWithMods($query, $modsArray)
84 {
85 return $query->where(function ($q) use ($modsArray) {
86 $bitset = app('mods')->idsToBitset($modsArray);
87 $preferenceMask = ~Mods::LEGACY_PREFERENCE_MODS_BITSET;
88
89 if (in_array('NM', $modsArray, true)) {
90 $q->orWhereRaw('enabled_mods & ? = 0', [$preferenceMask]);
91 }
92
93 if ($bitset > 0) {
94 $q->orWhereRaw('enabled_mods & ? = ?', [$preferenceMask | $bitset, $bitset]);
95 }
96 });
97 }
98
99 public function scopeWithoutMods($query, $modsArray)
100 {
101 $bitset = app('mods')->idsToBitset($modsArray);
102
103 return $query->whereRaw('enabled_mods & ? = 0', $bitset);
104 }
105
106 public function beatmap()
107 {
108 return $this->belongsTo(Beatmap::class, 'beatmap_id');
109 }
110
111 public function best()
112 {
113 $basename = get_class_basename(static::class);
114
115 return $this->belongsTo("App\\Models\\Score\\Best\\{$basename}", 'high_score_id', 'score_id');
116 }
117
118 public function user()
119 {
120 return $this->belongsTo(User::class, 'user_id');
121 }
122
123 public function getAttribute($key)
124 {
125 return match ($key) {
126 'beatmap_id',
127 'beatmapset_id',
128 'count100',
129 'count300',
130 'count50',
131 'countgeki',
132 'countkatu',
133 'countmiss',
134 'high_score_id',
135 'maxcombo',
136 'rank',
137 'score',
138 'score_id',
139 'scorechecksum',
140 'user_id' => $this->getRawAttribute($key),
141
142 'hidden',
143 'pass',
144 'perfect' => (bool) $this->getRawAttribute($key),
145
146 'date' => $this->getTimeFast($key),
147
148 'date_json' => $this->getJsonTimeFast($key),
149
150 'enabled_mods' => $this->getEnabledModsAttribute($this->getRawAttribute('enabled_mods')),
151
152 'best_id' => $this->getRawAttribute('high_score_id'),
153 'has_replay' => $this->best?->replay,
154 'pp' => $this->best?->pp,
155
156 'beatmap',
157 'best',
158 'replayViewCount',
159 'user' => $this->getRelationValue($key),
160
161 default => $this->getNewScoreAttribute($key),
162 };
163 }
164
165 public function getNewScoreAttribute(string $key)
166 {
167 return match ($key) {
168 'accuracy' => $this->accuracy(),
169 'build_id' => null,
170 'data' => $this->getData(),
171 'ended_at_json' => $this->date_json,
172 'is_perfect_combo' => $this->perfect,
173 'legacy_perfect' => $this->perfect,
174 'legacy_score_id' => $this->getKey(),
175 'legacy_total_score' => $this->score,
176 'max_combo' => $this->maxcombo,
177 'passed' => $this->pass,
178 'ruleset_id' => Ruleset::tryFromName($this->getMode())->value,
179 'started_at_json' => null,
180 'total_score' => $this->score,
181 };
182 }
183
184 public function getMode(): string
185 {
186 return snake_case(get_class_basename(static::class));
187 }
188
189 public function getData(): ScoreData
190 {
191 $mods = array_map(fn ($m) => ['acronym' => $m, 'settings' => []], $this->enabled_mods);
192
193 $statistics = [
194 'miss' => $this->countmiss,
195 'great' => $this->count300,
196 ];
197 $ruleset = Ruleset::tryFromName($this->getMode());
198 switch ($ruleset) {
199 case Ruleset::osu:
200 $statistics['ok'] = $this->count100;
201 $statistics['meh'] = $this->count50;
202 break;
203 case Ruleset::taiko:
204 $statistics['ok'] = $this->count100;
205 break;
206 case Ruleset::catch:
207 $statistics['large_tick_hit'] = $this->count100;
208 $statistics['small_tick_hit'] = $this->count50;
209 $statistics['small_tick_miss'] = $this->countkatu;
210 break;
211 case Ruleset::mania:
212 $statistics['perfect'] = $this->countgeki;
213 $statistics['good'] = $this->countkatu;
214 $statistics['ok'] = $this->count100;
215 $statistics['meh'] = $this->count50;
216 break;
217 }
218
219 return new ScoreData(compact('mods', 'statistics'));
220 }
221}