the browser-facing portion of osu!
at master 6.3 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 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}