the browser-facing portion of osu!
at master 5.1 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; 7 8use App\Libraries\Search\ScoreSearch; 9use App\Libraries\Search\ScoreSearchParams; 10use App\Models\Traits\WithDbCursorHelper; 11use Ds\Set; 12 13/** 14 * @property string $author 15 * @property \Carbon\Carbon $date 16 * @property bool $hidden 17 * @property \Illuminate\Database\Eloquent\Collection $items BeatmapPackItem 18 * @property string $name 19 * @property int $pack_id 20 * @property int|null $playmode 21 * @property string $tag 22 * @property string $url 23 */ 24class BeatmapPack extends Model 25{ 26 use WithDbCursorHelper; 27 28 protected const DEFAULT_SORT = 'id_desc'; 29 protected const SORTS = [ 30 'id_desc' => [ 31 ['column' => 'pack_id', 'order' => 'DESC'], 32 ], 33 ]; 34 35 const DEFAULT_TYPE = 'standard'; 36 37 // also display order for listing page 38 const TAG_MAPPINGS = [ 39 'standard' => 'S', 40 'featured' => 'F', 41 'tournament' => 'P', // since 'T' is taken and 'P' goes for 'pool' 42 'loved' => 'L', 43 'chart' => 'R', 44 'theme' => 'T', 45 'artist' => 'A', 46 ]; 47 48 protected $table = 'osu_beatmappacks'; 49 protected $primaryKey = 'pack_id'; 50 51 protected $casts = [ 52 'date' => 'datetime', 53 'hidden' => 'boolean', 54 'no_diff_reduction' => 'boolean', 55 ]; 56 57 public $timestamps = false; 58 59 public static function getPacks($type) 60 { 61 $tag = static::TAG_MAPPINGS[$type] ?? null; 62 63 if ($tag === null) { 64 return null; 65 } 66 67 return static::default()->where('tag', 'like', "{$tag}%")->orderBy('pack_id', 'desc'); 68 } 69 70 public function scopeDefault($query) 71 { 72 $query->where(['hidden' => false]); 73 } 74 75 public function items() 76 { 77 return $this->hasMany(BeatmapPackItem::class); 78 } 79 80 public function beatmapsets() 81 { 82 return $this->hasManyThrough( 83 Beatmapset::class, 84 BeatmapPackItem::class, 85 'pack_id', 86 'beatmapset_id', 87 null, 88 'beatmapset_id', 89 ); 90 } 91 92 public function getRouteKeyName(): string 93 { 94 return 'tag'; 95 } 96 97 public function userCompletionData($user, ?bool $isLegacy) 98 { 99 if ($user !== null) { 100 $userId = $user->getKey(); 101 102 $beatmaps = Beatmap 103 ::whereIn('beatmapset_id', $this->items()->select('beatmapset_id')) 104 ->select(['beatmap_id', 'beatmapset_id', 'playmode']) 105 ->get(); 106 $beatmapsetIdsByBeatmapId = []; 107 foreach ($beatmaps as $beatmap) { 108 $beatmapsetIdsByBeatmapId[$beatmap->beatmap_id] = $beatmap->beatmapset_id; 109 } 110 $params = [ 111 'beatmap_ids' => array_keys($beatmapsetIdsByBeatmapId), 112 'exclude_converts' => $this->playmode === null, 113 'is_legacy' => $isLegacy, 114 'limit' => 0, 115 'ruleset_id' => $this->playmode, 116 'user_id' => $userId, 117 ]; 118 if ($this->no_diff_reduction) { 119 $params['exclude_mods'] = app('mods')->difficultyReductionIds->toArray(); 120 if ($isLegacy !== true) { 121 // the intended meaning of this check is that the scores should not include mods 122 // that disqualify them from granting pp. 123 // mods are not the only reason why pp might be missing, but it's the best that we have for now. 124 // see also: https://github.com/ppy/osu-queue-score-statistics/pull/234 125 $params['exclude_without_pp'] = true; 126 } 127 } 128 129 static $aggName = 'by_beatmap'; 130 131 $search = new ScoreSearch(ScoreSearchParams::fromArray($params)); 132 $search->size(0); 133 $search->setAggregations([$aggName => [ 134 'terms' => [ 135 'field' => 'beatmap_id', 136 'size' => max(1, count($params['beatmap_ids'])), 137 ], 138 'aggs' => [ 139 'scores' => [ 140 'top_hits' => [ 141 'size' => 1, 142 ], 143 ], 144 ], 145 ]]); 146 $response = $search->response(); 147 $search->assertNoError(); 148 $completedBeatmapIds = array_map( 149 fn (array $hit): int => (int) $hit['key'], 150 $response->aggregations($aggName)['buckets'], 151 ); 152 $completedBeatmapsetIds = (new Set(array_map( 153 fn (int $beatmapId): int => $beatmapsetIdsByBeatmapId[$beatmapId], 154 $completedBeatmapIds, 155 )))->toArray(); 156 $completed = count($completedBeatmapsetIds) === count(array_unique($beatmapsetIdsByBeatmapId)); 157 } 158 159 return [ 160 'completed' => $completed ?? false, 161 'beatmapset_ids' => $completedBeatmapsetIds ?? [], 162 ]; 163 } 164}