the browser-facing portion of osu!

Fix scores set with unranked mods showing as processing indefinitely (again)

Closes https://github.com/ppy/osu-web/issues/10911 once more.

The actual fix is the inclusion of and check for existence of the
`score_process_history` row as it is the most reliable indicator of
processing having concluded.

This also includes a drive-by inclusion of a `preserve === false` check
that won't really do anything right now, but will if / once
https://github.com/ppy/osu-queue-score-statistics/issues/141 is
resolved.

The goal here is to only show pp values on scores that contribute to the
user's total. The query determining can be found here:

https://github.com/ppy/osu-queue-score-statistics/blob/f65dec335a914ee171d69981719d6ccb1d3f0ebe/osu.Server.Queues.ScoreStatisticsProcessor/Processors/UserTotalPerformanceProcessor.cs#L66-L76

+35 -4
+5
app/Models/Solo/Score.php
··· 148 148 return $this->belongsTo(User::class, 'user_id'); 149 149 } 150 150 151 + public function processHistory() 152 + { 153 + return $this->hasOne(ScoreProcessHistory::class, 'score_id'); 154 + } 155 + 151 156 public function scopeDefault(Builder $query): Builder 152 157 { 153 158 return $query->whereHas('beatmap.beatmapset');
+25
app/Models/Solo/ScoreProcessHistory.php
··· 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 + 6 + declare(strict_types=1); 7 + 8 + namespace App\Models\Solo; 9 + 10 + use App\Models\Model; 11 + 12 + /** 13 + * @property int $score_id 14 + * @property int $processed_version 15 + * @property \Carbon\Carbon $processed_at 16 + */ 17 + class ScoreProcessHistory extends Model 18 + { 19 + protected $table = 'score_process_history'; 20 + 21 + public function score() 22 + { 23 + return $this->belongsTo(Score::class, 'score_id'); 24 + } 25 + }
+1
app/Transformers/ScoreTransformer.php
··· 102 102 if ($score instanceof SoloScore) { 103 103 $extraAttributes['ranked'] = $score->ranked; 104 104 $extraAttributes['preserve'] = $score->preserve; 105 + $extraAttributes['processed'] = $score->processHistory()->first() !== null; 105 106 } 106 107 107 108 $hasReplay = $score->has_replay;
+1
resources/js/interfaces/solo-score-json.ts
··· 43 43 preserve?: boolean; 44 44 rank: Rank; 45 45 ranked?: boolean; 46 + processed?: boolean; 46 47 ruleset_id: number; 47 48 started_at: string | null; 48 49 statistics: Partial<Record<SoloScoreStatisticsAttribute, number>>;
+3 -3
resources/js/scores/pp-value.tsx
··· 21 21 if (!isBest && !isSolo) { 22 22 title = trans('scores.status.non_best'); 23 23 content = '-'; 24 - } else if (props.score.ranked === false) { 24 + } else if (props.score.ranked === false || props.score.preserve === false) { 25 25 title = trans('scores.status.no_pp'); 26 26 content = '-'; 27 27 } else if (props.score.pp == null) { 28 - if (isSolo && !props.score.passed) { 29 - title = trans('scores.status.non_passing'); 28 + if (isSolo && props.score.processed === true) { 29 + title = trans('scores.status.no_pp'); 30 30 content = '-'; 31 31 } else { 32 32 title = trans('scores.status.processing');
-1
resources/lang/en/scores.php
··· 25 25 26 26 'status' => [ 27 27 'non_best' => 'Only personal best scores award pp', 28 - 'non_passing' => 'Only passing scores award pp', 29 28 'no_pp' => 'pp is not awarded for this score', 30 29 'processing' => 'This score is still being calculated and will be displayed soon', 31 30 'no_rank' => 'This score has no rank as it is unranked or marked for deletion',