the browser-facing portion of osu!
at master 292 lines 9.8 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\Transformers; 9 10use App\Libraries\Search\ScoreSearchParams; 11use App\Models\Beatmap; 12use App\Models\DeletedUser; 13use App\Models\LegacyMatch; 14use App\Models\Multiplayer\PlaylistItemUserHighScore; 15use App\Models\Multiplayer\ScoreLink as MultiplayerScoreLink; 16use App\Models\Score\Best\Model as ScoreBest; 17use App\Models\Score\Model as ScoreModel; 18use App\Models\Solo\Score as SoloScore; 19use League\Fractal\Resource\Item; 20 21class ScoreTransformer extends TransformerAbstract 22{ 23 const MULTIPLAYER_BASE_INCLUDES = ['user.country', 'user.cover']; 24 // warning: the preload is actually for PlaylistItemUserHighScore, not for Score 25 const MULTIPLAYER_BASE_PRELOAD = [ 26 'scoreLink.playlistItem', 27 'scoreLink.score', 28 'scoreLink.score.processHistory', 29 'scoreLink.user.country', 30 ]; 31 32 const TYPE_LEGACY = 'legacy'; 33 const TYPE_SOLO = 'solo'; 34 35 // TODO: user include is deprecated. 36 const USER_PROFILE_INCLUDES = ['beatmap', 'beatmapset', 'user']; 37 const USER_PROFILE_INCLUDES_PRELOAD = [ 38 'beatmap', 39 'beatmap.beatmapset', 40 'processHistory', 41 // it's for user profile so the user is already available 42 // 'user', 43 ]; 44 45 protected array $availableIncludes = [ 46 'beatmap', 47 'beatmapset', 48 'current_user_attributes', 49 'match', 50 'rank_country', 51 'rank_global', 52 'user', 53 'weight', 54 55 // Only for MultiplayerScoreLink 56 'position', 57 'scores_around', 58 ]; 59 60 protected array $defaultIncludes = [ 61 'current_user_attributes', 62 ]; 63 64 private string $transformFunction; 65 66 public static function newSolo(): static 67 { 68 return new static(static::TYPE_SOLO); 69 } 70 71 public function __construct(?string $type = null) 72 { 73 $type ??= is_api_request() && api_version() < 20220705 74 ? static::TYPE_LEGACY 75 : static::TYPE_SOLO; 76 77 switch ($type) { 78 case static::TYPE_LEGACY: 79 $this->transformFunction = 'transformLegacy'; 80 break; 81 case static::TYPE_SOLO: 82 $this->transformFunction = 'transformSolo'; 83 break; 84 } 85 } 86 87 public function transform(LegacyMatch\Score|MultiplayerScoreLink|ScoreModel|SoloScore $score) 88 { 89 $fn = $this->transformFunction; 90 91 return $this->$fn($score); 92 } 93 94 public function transformSolo(MultiplayerScoreLink|ScoreModel|SoloScore $score) 95 { 96 $extraAttributes = []; 97 98 if ($score instanceof MultiplayerScoreLink) { 99 $extraAttributes['playlist_item_id'] = $score->playlist_item_id; 100 $extraAttributes['room_id'] = $score->playlistItem->room_id; 101 $extraAttributes['solo_score_id'] = $score->score_id; 102 $score = $score->score; 103 } 104 105 if ($score instanceof SoloScore) { 106 $extraAttributes['classic_total_score'] = $score->getClassicTotalScore(); 107 $extraAttributes['preserve'] = $score->preserve; 108 $extraAttributes['processed'] = $score->isProcessed(); 109 $extraAttributes['ranked'] = $score->ranked; 110 } 111 112 $hasReplay = $score->has_replay; 113 $isPerfectCombo = $score->is_perfect_combo; 114 115 return [ 116 ...$extraAttributes, 117 ...$score->data->jsonSerialize(), 118 'beatmap_id' => $score->beatmap_id, 119 'best_id' => $score->best_id, 120 'id' => $score->getKey(), 121 'rank' => $score->rank, 122 'type' => $score->getMorphClass(), 123 'user_id' => $score->user_id, 124 'accuracy' => $score->accuracy, 125 'build_id' => $score->build_id, 126 'ended_at' => $score->ended_at_json, 127 'has_replay' => $hasReplay, 128 'is_perfect_combo' => $isPerfectCombo, 129 'legacy_perfect' => $score->legacy_perfect ?? $isPerfectCombo, 130 'legacy_score_id' => $score->legacy_score_id, 131 'legacy_total_score' => $score->legacy_total_score, 132 'max_combo' => $score->max_combo, 133 'passed' => $score->passed, 134 'pp' => $score->pp, 135 'ruleset_id' => $score->ruleset_id, 136 'started_at' => $score->started_at_json, 137 'total_score' => $score->total_score, 138 // TODO: remove this redundant field sometime after 2024-02 139 'replay' => $hasReplay, 140 ]; 141 142 return $ret; 143 } 144 145 public function transformLegacy(LegacyMatch\Score|ScoreModel|SoloScore $score) 146 { 147 $best = null; 148 149 if ($score instanceof ScoreModel) { 150 // this `best` relation is also used by `current_user_attributes` include. 151 $best = $score->best; 152 } elseif ($score instanceof SoloScore) { 153 $soloScore = $score; 154 $score = $soloScore->makeLegacyEntry(); 155 $best = $score; 156 157 if ($soloScore->isLegacy()) { 158 $id = $soloScore->legacy_score_id; 159 if ($id > 0) { 160 // To be used later for best_id 161 $score->score_id = $id; 162 } 163 } else { 164 $id = $soloScore->getKey(); 165 $type = $soloScore->getMorphClass(); 166 } 167 } 168 169 $mode = $score->getMode(); 170 171 $statistics = [ 172 'count_100' => $score->count100, 173 'count_300' => $score->count300, 174 'count_50' => $score->count50, 175 'count_geki' => $score->countgeki, 176 'count_katu' => $score->countkatu, 177 'count_miss' => $score->countmiss, 178 ]; 179 180 return [ 181 'accuracy' => $score->accuracy(), 182 'best_id' => $best?->getKey(), 183 'created_at' => $score->date_json, 184 'id' => $id ?? $score->getKey(), 185 'max_combo' => $score->maxcombo, 186 'mode' => $mode, 187 'mode_int' => Beatmap::modeInt($mode), 188 'mods' => $score->enabled_mods, 189 'passed' => $score->pass, 190 'perfect' => $perfect ?? $score->perfect, 191 'pp' => $best?->pp, 192 'rank' => $score->rank, 193 'replay' => $best?->has_replay ?? false, 194 'score' => $score->score, 195 'statistics' => $statistics, 196 'type' => $type ?? $score->getMorphClass(), 197 'user_id' => $score->user_id, 198 ]; 199 } 200 201 public function includeBeatmap(LegacyMatch\Score|ScoreModel|SoloScore $score) 202 { 203 $beatmap = $score->beatmap; 204 205 if ($score->getMode() !== $beatmap->mode) { 206 $beatmap->convert = true; 207 $beatmap->playmode = Beatmap::MODES[$score->getMode()]; 208 } 209 210 return $this->item($beatmap, new BeatmapTransformer()); 211 } 212 213 public function includeBeatmapset(LegacyMatch\Score|ScoreModel|SoloScore $score) 214 { 215 return $this->item($score->beatmap->beatmapset, new BeatmapsetCompactTransformer()); 216 } 217 218 public function includeCurrentUserAttributes(LegacyMatch\Score|MultiplayerScoreLink|ScoreModel|SoloScore $score): Item 219 { 220 return $this->item($score, new Score\CurrentUserAttributesTransformer()); 221 } 222 223 public function includeMatch(LegacyMatch\Score $score) 224 { 225 return $this->primitive([ 226 'slot' => $score->slot, 227 'team' => $score->team, 228 'pass' => $score->pass, 229 ]); 230 } 231 232 public function includePosition(MultiplayerScoreLink $scoreLink) 233 { 234 return $this->primitive($scoreLink->position()); 235 } 236 237 public function includeScoresAround(MultiplayerScoreLink $scoreLink) 238 { 239 static $limit = 10; 240 static $transformer; 241 $transformer ??= static::newSolo(); 242 243 return $this->primitive(array_map( 244 function ($item) use ($limit, $transformer) { 245 [$highScores, $hasMore] = $item['query'] 246 ->with(static::MULTIPLAYER_BASE_PRELOAD) 247 ->limit($limit) 248 ->getWithHasMore(); 249 250 return [ 251 'scores' => json_collection($highScores->pluck('scoreLink'), $transformer, static::MULTIPLAYER_BASE_INCLUDES), 252 'params' => ['limit' => $limit, 'sort' => $item['cursorHelper']->getSortName()], 253 ...cursor_for_response($item['cursorHelper']->next($highScores, $hasMore)), 254 ]; 255 }, 256 PlaylistItemUserHighScore::scoresAround($scoreLink), 257 )); 258 } 259 260 public function includeRankCountry(ScoreBest|SoloScore $score) 261 { 262 return $this->primitive($score->userRank([ 263 'type' => 'country', 264 'is_legacy' => ScoreSearchParams::showLegacyForUser(\Auth::user()), 265 ])); 266 } 267 268 public function includeRankGlobal(ScoreBest|SoloScore $score) 269 { 270 return $this->primitive($score->userRank([ 271 'is_legacy' => ScoreSearchParams::showLegacyForUser(\Auth::user()), 272 ])); 273 } 274 275 public function includeUser(LegacyMatch\Score|MultiplayerScoreLink|ScoreModel|SoloScore $score) 276 { 277 return $this->item( 278 $score->user ?? new DeletedUser(['user_id' => $score->user_id]), 279 new UserCompactTransformer() 280 ); 281 } 282 283 public function includeWeight(LegacyMatch\Score|ScoreModel|SoloScore $score) 284 { 285 if (($score instanceof ScoreBest || $score instanceof SoloScore) && $score->weight !== null) { 286 return $this->primitive([ 287 'percentage' => $score->weight * 100, 288 'pp' => $score->weightedPp(), 289 ]); 290 } 291 } 292}