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\Traits;
7
8use App\Libraries\Score\FetchDedupedScores;
9use App\Libraries\Search\ScoreSearchParams;
10use App\Models\Beatmap;
11use App\Models\Solo\Score;
12use Illuminate\Database\Eloquent\Collection;
13
14trait UserScoreable
15{
16 private array $beatmapBestScoreIds = [];
17 private array $beatmapBestScores = [];
18
19 public function aggregatedScoresBest(string $mode, null | true $legacyOnly, int $size): array
20 {
21 return (new FetchDedupedScores('beatmap_id', ScoreSearchParams::fromArray([
22 'exclude_without_pp' => true,
23 'is_legacy' => $legacyOnly,
24 'limit' => $size,
25 'ruleset_id' => Beatmap::MODES[$mode],
26 'sort' => 'pp_desc',
27 'user_id' => $this->getKey(),
28 ]), "aggregatedScoresBest_{$mode}"))->all();
29 }
30
31 public function beatmapBestScoreIds(string $mode, null | true $legacyOnly)
32 {
33 $key = $mode.'-'.($legacyOnly ? '1' : '0');
34
35 if (!isset($this->beatmapBestScoreIds[$key])) {
36 // aggregations do not support regular pagination.
37 // always fetching 100 to cache; we're not supporting beyond 100, either.
38 $this->beatmapBestScoreIds[$key] = cache_remember_mutexed(
39 "search-cache:beatmapBestScoresSolo:{$this->getKey()}:{$key}",
40 $GLOBALS['cfg']['osu']['scores']['es_cache_duration'],
41 [],
42 function () use ($key, $legacyOnly, $mode) {
43 $this->beatmapBestScores[$key] = $this->aggregatedScoresBest($mode, $legacyOnly, 100);
44
45 return array_column($this->beatmapBestScores[$key], 'id');
46 },
47 function () {
48 // TODO: propagate a more useful message back to the client
49 // for now we just mark the exception as handled.
50 return true;
51 }
52 );
53 }
54
55 return $this->beatmapBestScoreIds[$key];
56 }
57
58 public function beatmapBestScores(string $mode, int $limit, int $offset, array $with, null | true $legacyOnly): Collection
59 {
60 $ids = $this->beatmapBestScoreIds($mode, $legacyOnly);
61 $key = $mode.'-'.($legacyOnly ? '1' : '0');
62
63 if (isset($this->beatmapBestScores[$key])) {
64 $results = new Collection(array_slice($this->beatmapBestScores[$key], $offset, $limit));
65 } else {
66 $ids = array_slice($ids, $offset, $limit);
67 $results = Score::whereKey($ids)->orderByField('id', $ids)->default()->get();
68 }
69
70 $results->load($with);
71
72 // fill in positions for weighting
73 // also preload the user relation
74 $position = $offset;
75 foreach ($results as $result) {
76 $result->weight = pow(0.95, $position);
77 $result->setRelation('user', $this);
78 $position++;
79 }
80
81 return $results;
82 }
83}