diff --git a/app/Http/Controllers/BeatmapsController.php b/app/Http/Controllers/BeatmapsController.php index 97158c45737..097852301e7 100644 --- a/app/Http/Controllers/BeatmapsController.php +++ b/app/Http/Controllers/BeatmapsController.php @@ -399,7 +399,7 @@ public function soloScores($id) 'type' => $type, 'user' => $currentUser, ]); - $scores = $esFetch->all()->loadMissing(['beatmap', 'performance', 'user.country', 'user.userProfileCustomization']); + $scores = $esFetch->all()->loadMissing(['beatmap', 'user.country', 'user.userProfileCustomization']); $userScore = $esFetch->userBest(); $scoreTransformer = new ScoreTransformer(ScoreTransformer::TYPE_SOLO); diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php index a55dc88afac..df41471a29b 100644 --- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php +++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php @@ -202,15 +202,13 @@ public function update($roomId, $playlistItemId, $tokenId) }); $score = $scoreLink->score; - $transformer = ScoreTransformer::newSolo(); if ($score->wasRecentlyCreated) { - $scoreJson = json_item($score, $transformer); - $score::queueForProcessing($scoreJson); + $score->queueForProcessing(); } return json_item( $scoreLink, - $transformer, + ScoreTransformer::newSolo(), [ ...ScoreTransformer::MULTIPLAYER_BASE_INCLUDES, 'position', diff --git a/app/Http/Controllers/Solo/ScoresController.php b/app/Http/Controllers/Solo/ScoresController.php index 6800c80051d..f5509401a63 100644 --- a/app/Http/Controllers/Solo/ScoresController.php +++ b/app/Http/Controllers/Solo/ScoresController.php @@ -41,11 +41,10 @@ public function store($beatmapId, $tokenId) return $score; }); - $scoreJson = json_item($score, new ScoreTransformer(ScoreTransformer::TYPE_SOLO)); if ($score->wasRecentlyCreated) { - $score::queueForProcessing($scoreJson); + $score->queueForProcessing(); } - return $scoreJson; + return json_item($score, new ScoreTransformer(ScoreTransformer::TYPE_SOLO)); } } diff --git a/app/Jobs/RemoveBeatmapsetSoloScores.php b/app/Jobs/RemoveBeatmapsetSoloScores.php index b70d45947e4..cfe16299e1a 100644 --- a/app/Jobs/RemoveBeatmapsetSoloScores.php +++ b/app/Jobs/RemoveBeatmapsetSoloScores.php @@ -11,7 +11,6 @@ use App\Models\Beatmap; use App\Models\Beatmapset; use App\Models\Solo\Score; -use App\Models\Solo\ScorePerformance; use DB; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -68,7 +67,6 @@ private function deleteScores(Collection $scores): void $scoresQuery->update(['preserve' => false]); $this->scoreSearch->queueForIndex($this->schemas, $ids); DB::transaction(function () use ($ids, $scoresQuery): void { - ScorePerformance::whereKey($ids)->delete(); $scoresQuery->delete(); }); } diff --git a/app/Libraries/Search/ScoreSearch.php b/app/Libraries/Search/ScoreSearch.php index c7125d1ce28..08f17c1e9e1 100644 --- a/app/Libraries/Search/ScoreSearch.php +++ b/app/Libraries/Search/ScoreSearch.php @@ -68,8 +68,8 @@ public function getQuery(): BoolQuery $beforeTotalScore = $this->params->beforeTotalScore; if ($beforeTotalScore === null && $this->params->beforeScore !== null) { $beforeTotalScore = $this->params->beforeScore->isLegacy() - ? $this->params->beforeScore->data->legacyTotalScore - : $this->params->beforeScore->data->totalScore; + ? $this->params->beforeScore->legacy_total_score + : $this->params->beforeScore->total_score; } if ($beforeTotalScore !== null) { $scoreQuery = (new BoolQuery())->shouldMatch(1); diff --git a/app/Models/Multiplayer/PlaylistItemUserHighScore.php b/app/Models/Multiplayer/PlaylistItemUserHighScore.php index 05617852f3d..8adf655d761 100644 --- a/app/Models/Multiplayer/PlaylistItemUserHighScore.php +++ b/app/Models/Multiplayer/PlaylistItemUserHighScore.php @@ -71,7 +71,7 @@ public static function scoresAround(ScoreLink $scoreLink): array { $placeholder = new static([ 'score_id' => $scoreLink->getKey(), - 'total_score' => $scoreLink->score->data->totalScore, + 'total_score' => $scoreLink->score->total_score, ]); static $typeOptions = [ @@ -117,10 +117,10 @@ public function updateWithScoreLink(ScoreLink $scoreLink): void $score = $scoreLink->score; $this->fill([ - 'accuracy' => $score->data->accuracy, + 'accuracy' => $score->accuracy, 'pp' => $score->pp, 'score_id' => $scoreLink->getKey(), - 'total_score' => $score->data->totalScore, + 'total_score' => $score->total_score, ])->save(); } } diff --git a/app/Models/Multiplayer/ScoreLink.php b/app/Models/Multiplayer/ScoreLink.php index 8bca690c1e2..f2977be647f 100644 --- a/app/Models/Multiplayer/ScoreLink.php +++ b/app/Models/Multiplayer/ScoreLink.php @@ -109,7 +109,7 @@ public function position(): ?int $query = PlaylistItemUserHighScore ::where('playlist_item_id', $this->playlist_item_id) ->cursorSort('score_asc', [ - 'total_score' => $score->data->totalScore, + 'total_score' => $score->total_score, 'score_id' => $this->getKey(), ]); diff --git a/app/Models/Multiplayer/UserScoreAggregate.php b/app/Models/Multiplayer/UserScoreAggregate.php index db0a9146378..9569542252f 100644 --- a/app/Models/Multiplayer/UserScoreAggregate.php +++ b/app/Models/Multiplayer/UserScoreAggregate.php @@ -83,7 +83,7 @@ public function addScoreLink(ScoreLink $scoreLink, ?PlaylistItemUserHighScore $h $scoreLink->playlist_item_id, ); - if ($score->data->passed && $score->data->totalScore > $highestScore->total_score) { + if ($score->passed && $score->total_score > $highestScore->total_score) { $this->updateUserTotal($scoreLink, $highestScore); $highestScore->updateWithScoreLink($scoreLink); } @@ -134,7 +134,7 @@ public function recalculate() $scoreLinks = ScoreLink ::whereHas('playlistItem', fn ($q) => $q->where('room_id', $this->room_id)) ->where('user_id', $this->user_id) - ->with('score.performance') + ->with('score') ->get(); foreach ($scoreLinks as $scoreLink) { $this->addScoreLink( @@ -221,8 +221,8 @@ private function updateUserTotal(ScoreLink $currentScoreLink, PlaylistItemUserHi $current = $currentScoreLink->score; - $this->total_score += $current->data->totalScore; - $this->accuracy += $current->data->accuracy; + $this->total_score += $current->total_score; + $this->accuracy += $current->accuracy; $this->pp += $current->pp; $this->completed++; $this->last_score_id = $currentScoreLink->getKey(); diff --git a/app/Models/Score/Best/Model.php b/app/Models/Score/Best/Model.php index b656d04067b..a8bc4b743d0 100644 --- a/app/Models/Score/Best/Model.php +++ b/app/Models/Score/Best/Model.php @@ -83,14 +83,18 @@ public function getAttribute($key) 'date_json' => $this->getJsonTimeFast($key), 'best' => $this, - 'data' => $this->getData(), 'enabled_mods' => $this->getEnabledModsAttribute($this->getRawAttribute('enabled_mods')), 'pass' => true, + 'best_id' => $this->getKey(), + 'has_replay' => $this->replay, + 'beatmap', 'replayViewCount', 'reportedIn', 'user' => $this->getRelationValue($key), + + default => $this->getNewScoreAttribute($key), }; } diff --git a/app/Models/Score/Model.php b/app/Models/Score/Model.php index a9befb727b0..7f48e9991a8 100644 --- a/app/Models/Score/Model.php +++ b/app/Models/Score/Model.php @@ -5,6 +5,7 @@ namespace App\Models\Score; +use App\Enums\Ruleset; use App\Exceptions\ClassNotFoundException; use App\Libraries\Mods; use App\Models\Beatmap; @@ -146,13 +147,36 @@ public function getAttribute($key) 'date_json' => $this->getJsonTimeFast($key), - 'data' => $this->getData(), 'enabled_mods' => $this->getEnabledModsAttribute($this->getRawAttribute('enabled_mods')), + 'best_id' => $this->getRawAttribute('high_score_id'), + 'has_replay' => $this->best?->replay, + 'pp' => $this->best?->pp, + 'beatmap', 'best', 'replayViewCount', 'user' => $this->getRelationValue($key), + + default => $this->getNewScoreAttribute($key), + }; + } + + public function getNewScoreAttribute(string $key) + { + return match ($key) { + 'accuracy' => $this->accuracy(), + 'build_id' => null, + 'data' => $this->getData(), + 'ended_at_json' => $this->date_json, + 'legacy_perfect' => $this->perfect, + 'legacy_score_id' => $this->getKey(), + 'legacy_total_score' => $this->score, + 'max_combo' => $this->maxcombo, + 'passed' => $this->pass, + 'ruleset_id' => Ruleset::tryFromName($this->getMode())->value, + 'started_at_json' => null, + 'total_score' => $this->score, }; } @@ -161,28 +185,29 @@ public function getMode(): string return snake_case(get_class_basename(static::class)); } - protected function getData() + public function getData(): ScoreData { $mods = array_map(fn ($m) => ['acronym' => $m, 'settings' => []], $this->enabled_mods); + $statistics = [ 'miss' => $this->countmiss, 'great' => $this->count300, ]; - $ruleset = $this->getMode(); + $ruleset = Ruleset::tryFromName($this->getMode()); switch ($ruleset) { - case 'osu': + case Ruleset::osu: $statistics['ok'] = $this->count100; $statistics['meh'] = $this->count50; break; - case 'taiko': + case Ruleset::taiko: $statistics['ok'] = $this->count100; break; - case 'fruits': + case Ruleset::catch: $statistics['large_tick_hit'] = $this->count100; $statistics['small_tick_hit'] = $this->count50; $statistics['small_tick_miss'] = $this->countkatu; break; - case 'mania': + case Ruleset::mania: $statistics['perfect'] = $this->countgeki; $statistics['good'] = $this->countkatu; $statistics['ok'] = $this->count100; @@ -190,18 +215,6 @@ protected function getData() break; } - return new ScoreData([ - 'accuracy' => $this->accuracy(), - 'beatmap_id' => $this->beatmap_id, - 'ended_at' => $this->date_json, - 'max_combo' => $this->maxcombo, - 'mods' => $mods, - 'passed' => $this->pass, - 'rank' => $this->rank, - 'ruleset_id' => Beatmap::modeInt($ruleset), - 'statistics' => $statistics, - 'total_score' => $this->score, - 'user_id' => $this->user_id, - ]); + return new ScoreData(compact('mods', 'statistics')); } } diff --git a/app/Models/Solo/Score.php b/app/Models/Solo/Score.php index 1c4841b188c..b4884978306 100644 --- a/app/Models/Solo/Score.php +++ b/app/Models/Solo/Score.php @@ -7,6 +7,8 @@ namespace App\Models\Solo; +use App\Enums\ScoreRank; +use App\Exceptions\InvariantException; use App\Libraries\Score\UserRank; use App\Libraries\Search\ScoreSearchParams; use App\Models\Beatmap; @@ -16,21 +18,31 @@ use App\Models\ScoreToken; use App\Models\Traits; use App\Models\User; -use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use LaravelRedis; use Storage; /** + * @property float $accuracy * @property int $beatmap_id - * @property \Carbon\Carbon|null $created_at - * @property string|null $created_at_json + * @property int $build_id * @property ScoreData $data + * @property \Carbon\Carbon|null $ended_at + * @property string|null $ended_at_json * @property bool $has_replay * @property int $id + * @property int $legacy_score_id + * @property int $legacy_total_score + * @property int $max_combo + * @property bool $passed + * @property float $pp * @property bool $preserve + * @property string $rank * @property bool $ranked * @property int $ruleset_id + * @property \Carbon\Carbon|null $started_at + * @property string|null $started_at_json + * @property int $total_score * @property int $unix_updated_at * @property User $user * @property int $user_id @@ -45,26 +57,36 @@ class Score extends Model implements Traits\ReportableInterface protected $casts = [ 'data' => ScoreData::class, + 'ended_at' => 'datetime', 'has_replay' => 'boolean', + 'passed' => 'boolean', 'preserve' => 'boolean', + 'ranked' => 'boolean', + 'started_at' => 'datetime', ]; - public static function createFromJsonOrExplode(array $params) + public static function createFromJsonOrExplode(array $params): static { - $score = new static([ - 'beatmap_id' => $params['beatmap_id'], - 'ruleset_id' => $params['ruleset_id'], - 'user_id' => $params['user_id'], - 'data' => $params, - ]); + $params['data'] = [ + 'maximum_statistics' => $params['maximum_statistics'] ?? [], + 'mods' => $params['mods'] ?? [], + 'statistics' => $params['statistics'] ?? [], + ]; + unset( + $params['maximum_statistics'], + $params['mods'], + $params['statistics'], + ); + + $score = new static($params); - $score->data->assertCompleted(); + $score->assertCompleted(); // this should potentially just be validation rather than applying this logic here, but // older lazer builds potentially submit incorrect details here (and we still want to // accept their scores. - if (!$score->data->passed) { - $score->data->rank = 'F'; + if (!$score->passed) { + $score->rank = 'F'; } $score->saveOrExplode(); @@ -72,46 +94,32 @@ public static function createFromJsonOrExplode(array $params) return $score; } - public static function extractParams(array $params, ScoreToken|MultiplayerScoreLink $scoreToken): array + public static function extractParams(array $rawParams, ScoreToken|MultiplayerScoreLink $scoreToken): array { - return [ - ...get_params($params, null, [ - 'accuracy:float', - 'max_combo:int', - 'maximum_statistics:array', - 'passed:bool', - 'rank:string', - 'statistics:array', - 'total_score:int', - ]), - 'beatmap_id' => $scoreToken->beatmap_id, - 'build_id' => $scoreToken->build_id, - 'ended_at' => json_time(Carbon::now()), - 'mods' => app('mods')->parseInputArray($scoreToken->ruleset_id, get_arr($params['mods'] ?? null) ?? []), - 'ruleset_id' => $scoreToken->ruleset_id, - 'started_at' => $scoreToken->created_at_json, - 'user_id' => $scoreToken->user_id, - ]; - } + $params = get_params($rawParams, null, [ + 'accuracy:float', + 'max_combo:int', + 'maximum_statistics:array', + 'mods:array', + 'passed:bool', + 'rank:string', + 'statistics:array', + 'total_score:int', + ]); - /** - * Queue the item for score processing - * - * @param array $scoreJson JSON of the score generated using ScoreTransformer of type Solo - */ - public static function queueForProcessing(array $scoreJson): void - { - LaravelRedis::lpush(static::PROCESSING_QUEUE, json_encode([ - 'Score' => [ - 'beatmap_id' => $scoreJson['beatmap_id'], - 'id' => $scoreJson['id'], - 'ruleset_id' => $scoreJson['ruleset_id'], - 'user_id' => $scoreJson['user_id'], - // TODO: processor is currently order dependent and requires - // this to be located at the end - 'data' => json_encode($scoreJson), - ], - ])); + $params['maximum_statistics'] ??= []; + $params['statistics'] ??= []; + + $params['mods'] = app('mods')->parseInputArray($scoreToken->ruleset_id, $params['mods'] ?? []); + + $params['beatmap_id'] = $scoreToken->beatmap_id; + $params['build_id'] = $scoreToken->build_id; + $params['ended_at'] = new \DateTime(); + $params['ruleset_id'] = $scoreToken->ruleset_id; + $params['started_at'] = $scoreToken->created_at; + $params['user_id'] = $scoreToken->user_id; + + return $params; } public function beatmap() @@ -119,11 +127,6 @@ public function beatmap() return $this->belongsTo(Beatmap::class, 'beatmap_id'); } - public function performance() - { - return $this->hasOne(ScorePerformance::class, 'score_id'); - } - public function user() { return $this->belongsTo(User::class, 'user_id'); @@ -147,22 +150,37 @@ public function scopeIndexable(Builder $query): Builder public function getAttribute($key) { return match ($key) { + 'accuracy', 'beatmap_id', + 'build_id', 'id', + 'legacy_score_id', + 'legacy_total_score', + 'max_combo', + 'pp', 'ruleset_id', + 'total_score', 'unix_updated_at', 'user_id' => $this->getRawAttribute($key), + 'rank' => $this->getRawAttribute($key) ?? 'F', + 'data' => $this->getClassCastableAttributeValue($key, $this->getRawAttribute($key)), 'has_replay', - 'preserve', - 'ranked' => (bool) $this->getRawAttribute($key), + 'passed', + 'preserve' => (bool) $this->getRawAttribute($key), + + 'ranked' => (bool) ($this->getRawAttribute($key) ?? true), - 'created_at' => $this->getTimeFast($key), - 'created_at_json' => $this->getJsonTimeFast($key), + 'ended_at', + 'started_at' => $this->getTimeFast($key), - 'pp' => $this->performance?->pp, + 'ended_at_json', + 'started_at_json' => $this->getJsonTimeFast($key), + + 'best_id' => null, + 'legacy_perfect' => null, 'beatmap', 'performance', @@ -171,6 +189,23 @@ public function getAttribute($key) }; } + public function assertCompleted(): void + { + if (ScoreRank::tryFrom($this->rank ?? '') === null) { + throw new InvariantException("'{$this->rank}' is not a valid rank."); + } + + foreach (['total_score', 'accuracy', 'max_combo', 'passed'] as $field) { + if (!present($this->$field)) { + throw new InvariantException("field missing: '{$field}'"); + } + } + + if ($this->data->statistics->isEmpty()) { + throw new InvariantException("field cannot be empty: 'statistics'"); + } + } + public function createLegacyEntryOrExplode() { $score = $this->makeLegacyEntry(); @@ -193,12 +228,12 @@ public function getReplayFile(): ?string public function isLegacy(): bool { - return $this->data->buildId === null; + return $this->legacy_score_id !== null; } public function legacyScore(): ?LegacyScore\Best\Model { - $id = $this->data->legacyScoreId; + $id = $this->legacy_score_id; return $id === null ? null @@ -216,11 +251,11 @@ public function makeLegacyEntry(): LegacyScore\Model 'beatmapset_id' => $this->beatmap?->beatmapset_id ?? 0, 'countmiss' => $statistics->miss, 'enabled_mods' => app('mods')->idsToBitset(array_column($data->mods, 'acronym')), - 'maxcombo' => $data->maxCombo, - 'pass' => $data->passed, - 'perfect' => $data->passed && $statistics->miss + $statistics->large_tick_miss === 0, - 'rank' => $data->rank, - 'score' => $data->totalScore, + 'maxcombo' => $this->max_combo, + 'pass' => $this->passed, + 'perfect' => $this->passed && $statistics->miss + $statistics->large_tick_miss === 0, + 'rank' => $this->rank, + 'score' => $this->total_score, 'scorechecksum' => "\0", 'user_id' => $this->user_id, ]); @@ -253,6 +288,13 @@ public function makeLegacyEntry(): LegacyScore\Model return $score; } + public function queueForProcessing(): void + { + LaravelRedis::lpush(static::PROCESSING_QUEUE, json_encode([ + 'Score' => $this->getAttributes(), + ])); + } + public function trashed(): bool { return false; diff --git a/app/Models/Solo/ScoreData.php b/app/Models/Solo/ScoreData.php index ebebeaf61fd..51ed8bc0b98 100644 --- a/app/Models/Solo/ScoreData.php +++ b/app/Models/Solo/ScoreData.php @@ -7,30 +7,15 @@ namespace App\Models\Solo; -use App\Enums\ScoreRank; -use App\Exceptions\InvariantException; use Illuminate\Contracts\Database\Eloquent\Castable; use Illuminate\Contracts\Database\Eloquent\CastsAttributes; use JsonSerializable; class ScoreData implements Castable, JsonSerializable { - public float $accuracy; - public int $beatmapId; - public ?int $buildId; - public string $endedAt; - public ?int $legacyScoreId; - public ?int $legacyTotalScore; - public int $maxCombo; public ScoreDataStatistics $maximumStatistics; public array $mods; - public bool $passed; - public string $rank; - public int $rulesetId; - public ?string $startedAt; public ScoreDataStatistics $statistics; - public int $totalScore; - public int $userId; public function __construct(array $data) { @@ -51,22 +36,9 @@ public function __construct(array $data) } } - $this->accuracy = $data['accuracy'] ?? 0; - $this->beatmapId = $data['beatmap_id']; - $this->buildId = $data['build_id'] ?? null; - $this->endedAt = $data['ended_at']; - $this->legacyScoreId = $data['legacy_score_id'] ?? null; - $this->legacyTotalScore = $data['legacy_total_score'] ?? null; - $this->maxCombo = $data['max_combo'] ?? 0; $this->maximumStatistics = new ScoreDataStatistics($data['maximum_statistics'] ?? []); $this->mods = $mods; - $this->passed = $data['passed'] ?? false; - $this->rank = $data['rank'] ?? 'F'; - $this->rulesetId = $data['ruleset_id']; - $this->startedAt = $data['started_at'] ?? null; $this->statistics = new ScoreDataStatistics($data['statistics'] ?? []); - $this->totalScore = $data['total_score'] ?? 0; - $this->userId = $data['user_id']; } public static function castUsing(array $arguments) @@ -75,25 +47,13 @@ public static function castUsing(array $arguments) { public function get($model, $key, $value, $attributes) { - $dataJson = json_decode($value, true); - $dataJson['beatmap_id'] ??= $attributes['beatmap_id']; - $dataJson['ended_at'] ??= $model->created_at_json; - $dataJson['ruleset_id'] ??= $attributes['ruleset_id']; - $dataJson['user_id'] ??= $attributes['user_id']; - - return new ScoreData($dataJson); + return new ScoreData(json_decode($value, true)); } public function set($model, $key, $value, $attributes) { if (!($value instanceof ScoreData)) { - $value = new ScoreData([ - 'beatmap_id' => $attributes['beatmap_id'] ?? null, - 'ended_at' => $attributes['created_at'] ?? null, - 'ruleset_id' => $attributes['ruleset_id'] ?? null, - 'user_id' => $attributes['user_id'] ?? null, - ...$value, - ]); + $value = new ScoreData($value); } return ['data' => json_encode($value)]; @@ -101,50 +61,12 @@ public function set($model, $key, $value, $attributes) }; } - public function assertCompleted(): void - { - if (ScoreRank::tryFrom($this->rank) === null) { - throw new InvariantException("'{$this->rank}' is not a valid rank."); - } - - foreach (['totalScore', 'accuracy', 'maxCombo', 'passed'] as $field) { - if (!present($this->$field)) { - throw new InvariantException("field missing: '{$field}'"); - } - } - - if ($this->statistics->isEmpty()) { - throw new InvariantException("field cannot be empty: 'statistics'"); - } - } - public function jsonSerialize(): array { - $ret = [ - 'accuracy' => $this->accuracy, - 'beatmap_id' => $this->beatmapId, - 'build_id' => $this->buildId, - 'ended_at' => $this->endedAt, - 'legacy_score_id' => $this->legacyScoreId, - 'legacy_total_score' => $this->legacyTotalScore, - 'max_combo' => $this->maxCombo, + return [ 'maximum_statistics' => $this->maximumStatistics, 'mods' => $this->mods, - 'passed' => $this->passed, - 'rank' => $this->rank, - 'ruleset_id' => $this->rulesetId, - 'started_at' => $this->startedAt, 'statistics' => $this->statistics, - 'total_score' => $this->totalScore, - 'user_id' => $this->userId, ]; - - foreach ($ret as $field => $value) { - if ($value === null) { - unset($ret[$field]); - } - } - - return $ret; } } diff --git a/app/Models/Solo/ScorePerformance.php b/app/Models/Solo/ScorePerformance.php deleted file mode 100644 index b30a1f9a410..00000000000 --- a/app/Models/Solo/ScorePerformance.php +++ /dev/null @@ -1,21 +0,0 @@ -. Licensed under the GNU Affero General Public License v3.0. -// See the LICENCE file in the repository root for full licence text. - -namespace App\Models\Solo; - -use App\Models\Model; - -/** - * @property int $score_id - * @property float|null $pp - */ -class ScorePerformance extends Model -{ - public $incrementing = false; - public $timestamps = false; - - protected $primaryKey = 'score_id'; - protected $table = 'score_performance'; -} diff --git a/app/Transformers/ScoreTransformer.php b/app/Transformers/ScoreTransformer.php index 8526ad7ea48..e6ccdd177f9 100644 --- a/app/Transformers/ScoreTransformer.php +++ b/app/Transformers/ScoreTransformer.php @@ -22,7 +22,7 @@ class ScoreTransformer extends TransformerAbstract const MULTIPLAYER_BASE_INCLUDES = ['user.country', 'user.cover']; // warning: the preload is actually for PlaylistItemUserHighScore, not for Score const MULTIPLAYER_BASE_PRELOAD = [ - 'scoreLink.score.performance', + 'scoreLink.score', 'scoreLink.user.country', 'scoreLink.user.userProfileCustomization', ]; @@ -90,43 +90,44 @@ public function transform(LegacyMatch\Score|MultiplayerScoreLink|ScoreModel|Solo public function transformSolo(MultiplayerScoreLink|ScoreModel|SoloScore $score) { - if ($score instanceof ScoreModel) { - $legacyPerfect = $score->perfect; - $best = $score->best; - - if ($best !== null) { - $bestId = $best->getKey(); - $pp = $best->pp; - $hasReplay = $best->replay; - } - } else { - if ($score instanceof MultiplayerScoreLink) { - $multiplayerAttributes = [ - 'playlist_item_id' => $score->playlist_item_id, - 'room_id' => $score->playlistItem->room_id, - 'solo_score_id' => $score->score_id, - ]; - $score = $score->score; - } + $extraAttributes = []; - $pp = $score->pp; - $hasReplay = $score->has_replay; + if ($score instanceof MultiplayerScoreLink) { + $extraAttributes['playlist_item_id'] = $score->playlist_item_id; + $extraAttributes['room_id'] = $score->playlistItem->room_id; + $extraAttributes['solo_score_id'] = $score->score_id; + $score = $score->score; } - $hasReplay ??= false; + $hasReplay = $score->has_replay; return [ + ...$extraAttributes, ...$score->data->jsonSerialize(), - ...($multiplayerAttributes ?? []), - 'best_id' => $bestId ?? null, - 'has_replay' => $hasReplay, + 'beatmap_id' => $score->beatmap_id, + 'best_id' => $score->best_id, 'id' => $score->getKey(), - 'legacy_perfect' => $legacyPerfect ?? null, - 'pp' => $pp ?? null, + 'rank' => $score->rank, + 'type' => $score->getMorphClass(), + 'user_id' => $score->user_id, + 'accuracy' => $score->accuracy, + 'build_id' => $score->build_id, + 'ended_at' => $score->ended_at_json, + 'has_replay' => $hasReplay, + 'legacy_perfect' => $score->legacy_perfect, + 'legacy_score_id' => $score->legacy_score_id, + 'legacy_total_score' => $score->legacy_total_score, + 'max_combo' => $score->max_combo, + 'passed' => $score->passed, + 'pp' => $score->pp, + 'ruleset_id' => $score->ruleset_id, + 'started_at' => $score->started_at_json, + 'total_score' => $score->total_score, // TODO: remove this redundant field sometime after 2024-02 'replay' => $hasReplay, - 'type' => $score->getMorphClass(), ]; + + return $ret; } public function transformLegacy(LegacyMatch\Score|ScoreModel|SoloScore $score) @@ -146,7 +147,7 @@ public function transformLegacy(LegacyMatch\Score|ScoreModel|SoloScore $score) $soloScore = $score; $score = $soloScore->makeLegacyEntry(); $score->score_id = $soloScore->getKey(); - $createdAt = $soloScore->created_at_json; + $createdAt = $soloScore->ended_at_json; $type = $soloScore->getMorphClass(); $pp = $soloScore->pp; } else { diff --git a/database/factories/Solo/ScoreFactory.php b/database/factories/Solo/ScoreFactory.php index f7c39b3233a..de75879064f 100644 --- a/database/factories/Solo/ScoreFactory.php +++ b/database/factories/Solo/ScoreFactory.php @@ -7,6 +7,7 @@ namespace Database\Factories\Solo; +use App\Enums\ScoreRank; use App\Models\Beatmap; use App\Models\Solo\Score; use App\Models\User; @@ -19,7 +20,12 @@ class ScoreFactory extends Factory public function definition(): array { return [ + 'accuracy' => fn (): float => $this->faker->randomFloat(1, 0, 1), 'beatmap_id' => Beatmap::factory()->ranked(), + 'ended_at' => new \DateTime(), + 'pp' => fn (): float => $this->faker->randomFloat(4, 0, 1000), + 'rank' => fn () => array_rand_val(ScoreRank::cases())->value, + 'total_score' => fn (): int => $this->faker->randomNumber(7), 'user_id' => User::factory(), // depends on beatmap_id @@ -41,19 +47,11 @@ private function makeData(?array $overrides = null): callable { return fn (array $attr): array => array_map( fn ($value) => is_callable($value) ? $value($attr) : $value, - array_merge([ - 'accuracy' => fn (): float => $this->faker->randomFloat(1, 0, 1), - 'beatmap_id' => $attr['beatmap_id'], - 'ended_at' => fn (): string => json_time(now()), - 'max_combo' => fn (): int => rand(1, Beatmap::find($attr['beatmap_id'])->countNormal), + [ + 'statistics' => ['great' => 1], 'mods' => [], - 'passed' => true, - 'rank' => fn (): string => array_rand_val(['A', 'S', 'B', 'SH', 'XH', 'X']), - 'ruleset_id' => $attr['ruleset_id'], - 'started_at' => fn (): string => json_time(now()->subSeconds(600)), - 'total_score' => fn (): int => $this->faker->randomNumber(7), - 'user_id' => $attr['user_id'], - ], $overrides ?? []), + ...($overrides ?? []), + ], ); } } diff --git a/database/migrations/2024_01_12_115738_update_scores_table_final.php b/database/migrations/2024_01_12_115738_update_scores_table_final.php new file mode 100644 index 00000000000..8a93c4edf2c --- /dev/null +++ b/database/migrations/2024_01_12_115738_update_scores_table_final.php @@ -0,0 +1,94 @@ +. Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +declare(strict_types=1); + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + private static function resetView(): void + { + DB::statement('DROP VIEW scores'); + DB::statement('CREATE VIEW scores AS SELECT * FROM solo_scores'); + } + + public function up(): void + { + Schema::drop('solo_scores'); + DB::statement("CREATE TABLE `solo_scores` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `ruleset_id` smallint unsigned NOT NULL, + `beatmap_id` mediumint unsigned NOT NULL, + `has_replay` tinyint NOT NULL DEFAULT '0', + `preserve` tinyint NOT NULL DEFAULT '0', + `ranked` tinyint NOT NULL DEFAULT '1', + `rank` char(2) NOT NULL DEFAULT '', + `passed` tinyint NOT NULL DEFAULT '0', + `accuracy` float NOT NULL DEFAULT '0', + `max_combo` int unsigned NOT NULL DEFAULT '0', + `total_score` int unsigned NOT NULL DEFAULT '0', + `data` json NOT NULL, + `pp` float DEFAULT NULL, + `legacy_score_id` bigint unsigned DEFAULT NULL, + `legacy_total_score` int unsigned NOT NULL DEFAULT '0', + `started_at` timestamp NULL DEFAULT NULL, + `ended_at` timestamp NOT NULL, + `unix_updated_at` int unsigned NOT NULL DEFAULT (unix_timestamp()), + `build_id` smallint unsigned DEFAULT NULL, + PRIMARY KEY (`id`,`preserve`,`unix_updated_at`), + KEY `user_ruleset_index` (`user_id`,`ruleset_id`), + KEY `beatmap_user_index` (`beatmap_id`,`user_id`), + KEY `legacy_score_lookup` (`ruleset_id`,`legacy_score_id`) + )"); + + DB::statement('DROP VIEW score_legacy_id_map'); + Schema::drop('solo_scores_legacy_id_map'); + + DB::statement('DROP VIEW score_performance'); + Schema::drop('solo_scores_performance'); + + static::resetView(); + } + + public function down(): void + { + Schema::drop('solo_scores'); + DB::statement("CREATE TABLE `solo_scores` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `beatmap_id` mediumint unsigned NOT NULL, + `ruleset_id` smallint unsigned NOT NULL, + `data` json NOT NULL, + `has_replay` tinyint DEFAULT '0', + `preserve` tinyint NOT NULL DEFAULT '0', + `ranked` tinyint NOT NULL DEFAULT '1', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `unix_updated_at` int unsigned NOT NULL DEFAULT (unix_timestamp()), + PRIMARY KEY (`id`,`preserve`,`unix_updated_at`), + KEY `user_ruleset_index` (`user_id`,`ruleset_id`), + KEY `beatmap_user_index` (`beatmap_id`,`user_id`) + )"); + + DB::statement('CREATE TABLE `solo_scores_legacy_id_map` ( + `ruleset_id` smallint unsigned NOT NULL, + `old_score_id` bigint unsigned NOT NULL, + `score_id` bigint unsigned NOT NULL, + PRIMARY KEY (`ruleset_id`,`old_score_id`) + )'); + DB::statement('CREATE VIEW score_legacy_id_map AS SELECT * FROM solo_scores_legacy_id_map'); + + DB::statement('CREATE TABLE `solo_scores_performance` ( + `score_id` bigint unsigned NOT NULL, + `pp` float DEFAULT NULL, + PRIMARY KEY (`score_id`) + )'); + DB::statement('CREATE VIEW score_performance AS SELECT * FROM solo_scores_performance'); + + static::resetView(); + } +}; diff --git a/docker-compose.yml b/docker-compose.yml index c2e1ddab56a..6e7fa320e9a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -154,7 +154,7 @@ services: - "${NGINX_PORT:-8080}:80" score-indexer: - image: pppy/osu-elastic-indexer:99cd549c5c5c959ff6b2728b76af603dda4c85cb + image: pppy/osu-elastic-indexer:master command: ["queue", "watch"] depends_on: redis: @@ -168,7 +168,7 @@ services: SCHEMA: "${SCHEMA:-1}" score-indexer-test: - image: pppy/osu-elastic-indexer:99cd549c5c5c959ff6b2728b76af603dda4c85cb + image: pppy/osu-elastic-indexer:master command: ["queue", "watch"] depends_on: redis: diff --git a/resources/js/interfaces/solo-score-json.ts b/resources/js/interfaces/solo-score-json.ts index d33708bdaa1..fa83b534ceb 100644 --- a/resources/js/interfaces/solo-score-json.ts +++ b/resources/js/interfaces/solo-score-json.ts @@ -34,7 +34,7 @@ type SoloScoreJsonDefaultAttributes = { has_replay: boolean; id: number; legacy_score_id: number | null; - legacy_total_score: number | null; + legacy_total_score: number; max_combo: number; mods: ScoreModJson[]; passed: boolean; diff --git a/resources/js/utils/score-helper.ts b/resources/js/utils/score-helper.ts index d258d63722d..9f3bf6d3fd4 100644 --- a/resources/js/utils/score-helper.ts +++ b/resources/js/utils/score-helper.ts @@ -123,5 +123,7 @@ export function scoreUrl(score: SoloScoreJson) { } export function totalScore(score: SoloScoreJson) { - return score.legacy_total_score ?? score.total_score; + return score.legacy_score_id == null + ? score.total_score + : score.legacy_total_score; } diff --git a/tests/Controllers/BeatmapsControllerSoloScoresTest.php b/tests/Controllers/BeatmapsControllerSoloScoresTest.php index eedbaf7c9f8..c632fe41952 100644 --- a/tests/Controllers/BeatmapsControllerSoloScoresTest.php +++ b/tests/Controllers/BeatmapsControllerSoloScoresTest.php @@ -41,108 +41,115 @@ public static function setUpBeforeClass(): void $countryAcronym = static::$user->country_acronym; static::$scores = []; - $scoreFactory = SoloScore::factory(); - foreach (['solo' => 0, 'legacy' => null] as $type => $buildId) { - $defaultData = ['build_id' => $buildId]; + $scoreFactory = SoloScore::factory()->state(['build_id' => 0]); + foreach (['solo' => null, 'legacy' => 1] as $type => $legacyScoreId) { + $scoreFactory = $scoreFactory->state([ + 'legacy_score_id' => $legacyScoreId, + ]); - static::$scores = array_merge(static::$scores, [ - "{$type}:user" => $scoreFactory->withData($defaultData, ['total_score' => 1100])->create([ + static::$scores = [ + ...static::$scores, + "{$type}:user" => $scoreFactory->create([ 'beatmap_id' => static::$beatmap, 'preserve' => true, + 'total_score' => 1100, 'user_id' => static::$user, ]), - "{$type}:userMods" => $scoreFactory->withData($defaultData, [ - 'total_score' => 1050, + "{$type}:userMods" => $scoreFactory->withData([ 'mods' => static::defaultMods(['DT', 'HD']), ])->create([ 'beatmap_id' => static::$beatmap, 'preserve' => true, + 'total_score' => 1050, 'user_id' => static::$user, ]), - "{$type}:userModsNC" => $scoreFactory->withData($defaultData, [ - 'total_score' => 1050, + "{$type}:userModsNC" => $scoreFactory->withData([ 'mods' => static::defaultMods(['NC']), ])->create([ 'beatmap_id' => static::$beatmap, 'preserve' => true, + 'total_score' => 1050, 'user_id' => static::$user, ]), - "{$type}:otherUserModsNCPFHigherScore" => $scoreFactory->withData($defaultData, [ - 'total_score' => 1010, + "{$type}:otherUserModsNCPFHigherScore" => $scoreFactory->withData([ 'mods' => static::defaultMods(['NC', 'PF']), ])->create([ 'beatmap_id' => static::$beatmap, 'preserve' => true, + 'total_score' => 1010, 'user_id' => static::$otherUser, ]), - "{$type}:userModsLowerScore" => $scoreFactory->withData($defaultData, [ - 'total_score' => 1000, + "{$type}:userModsLowerScore" => $scoreFactory->withData([ 'mods' => static::defaultMods(['DT', 'HD']), ])->create([ 'beatmap_id' => static::$beatmap, 'preserve' => true, + 'total_score' => 1000, 'user_id' => static::$user, ]), - "{$type}:friend" => $scoreFactory->withData($defaultData, ['total_score' => 1000])->create([ + "{$type}:friend" => $scoreFactory->create([ 'beatmap_id' => static::$beatmap, 'preserve' => true, + 'total_score' => 1000, 'user_id' => $friend, ]), // With preference mods - "{$type}:otherUser" => $scoreFactory->withData($defaultData, [ - 'total_score' => 1000, + "{$type}:otherUser" => $scoreFactory->withData([ 'mods' => static::defaultMods(['PF']), ])->create([ 'beatmap_id' => static::$beatmap, 'preserve' => true, + 'total_score' => 1000, 'user_id' => static::$otherUser, ]), - "{$type}:otherUserMods" => $scoreFactory->withData($defaultData, [ - 'total_score' => 1000, + "{$type}:otherUserMods" => $scoreFactory->withData([ 'mods' => static::defaultMods(['HD', 'PF', 'NC']), ])->create([ 'beatmap_id' => static::$beatmap, 'preserve' => true, + 'total_score' => 1000, 'user_id' => static::$otherUser, ]), - "{$type}:otherUserModsExtraNonPreferences" => $scoreFactory->withData($defaultData, [ - 'total_score' => 1000, + "{$type}:otherUserModsExtraNonPreferences" => $scoreFactory->withData([ 'mods' => static::defaultMods(['DT', 'HD', 'HR']), ])->create([ 'beatmap_id' => static::$beatmap, 'preserve' => true, + 'total_score' => 1000, 'user_id' => static::$otherUser, ]), - "{$type}:otherUserModsUnrelated" => $scoreFactory->withData($defaultData, [ - 'total_score' => 1000, + "{$type}:otherUserModsUnrelated" => $scoreFactory->withData([ 'mods' => static::defaultMods(['FL']), ])->create([ 'beatmap_id' => static::$beatmap, 'preserve' => true, + 'total_score' => 1000, 'user_id' => static::$otherUser, ]), // Same total score but achieved later so it should come up after earlier score - "{$type}:otherUser2Later" => $scoreFactory->withData($defaultData, ['total_score' => 1000])->create([ + "{$type}:otherUser2Later" => $scoreFactory->create([ 'beatmap_id' => static::$beatmap, 'preserve' => true, + 'total_score' => 1000, 'user_id' => User::factory()->state(['country_acronym' => Country::factory()]), ]), - "{$type}:otherUser3SameCountry" => $scoreFactory->withData($defaultData, ['total_score' => 1000])->create([ + "{$type}:otherUser3SameCountry" => $scoreFactory->create([ 'beatmap_id' => static::$beatmap, 'preserve' => true, + 'total_score' => 1000, 'user_id' => User::factory()->state(['country_acronym' => $countryAcronym]), ]), // Non-preserved score should be filtered out - "{$type}:nonPreserved" => $scoreFactory->withData($defaultData)->create([ + "{$type}:nonPreserved" => $scoreFactory->create([ 'beatmap_id' => static::$beatmap, 'preserve' => false, 'user_id' => User::factory()->state(['country_acronym' => Country::factory()]), ]), // Unrelated score - "{$type}:unrelated" => $scoreFactory->withData($defaultData)->create([ + "{$type}:unrelated" => $scoreFactory->create([ 'user_id' => User::factory()->state(['country_acronym' => Country::factory()]), ]), - ]); + ]; } UserRelation::create([ diff --git a/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php b/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php index d9efb5b7fb9..d42526b930c 100644 --- a/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php +++ b/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php @@ -23,19 +23,19 @@ public function testIndex() $scoreLinks[] = ScoreLink ::factory() ->state(['playlist_item_id' => $playlist]) - ->completed([], ['passed' => true, 'total_score' => 30]) + ->completed(['passed' => true, 'total_score' => 30]) ->create(); $scoreLinks[] = $userScoreLink = ScoreLink ::factory() ->state([ 'playlist_item_id' => $playlist, 'user_id' => $user, - ])->completed([], ['passed' => true, 'total_score' => 20]) + ])->completed(['passed' => true, 'total_score' => 20]) ->create(); $scoreLinks[] = ScoreLink ::factory() ->state(['playlist_item_id' => $playlist]) - ->completed([], ['passed' => true, 'total_score' => 10]) + ->completed(['passed' => true, 'total_score' => 10]) ->create(); foreach ($scoreLinks as $scoreLink) { @@ -65,19 +65,19 @@ public function testShow() $scoreLinks[] = ScoreLink ::factory() ->state(['playlist_item_id' => $playlist]) - ->completed([], ['passed' => true, 'total_score' => 30]) + ->completed(['passed' => true, 'total_score' => 30]) ->create(); $scoreLinks[] = $userScoreLink = ScoreLink ::factory() ->state([ 'playlist_item_id' => $playlist, 'user_id' => $user, - ])->completed([], ['passed' => true, 'total_score' => 20]) + ])->completed(['passed' => true, 'total_score' => 20]) ->create(); $scoreLinks[] = ScoreLink ::factory() ->state(['playlist_item_id' => $playlist]) - ->completed([], ['passed' => true, 'total_score' => 10]) + ->completed(['passed' => true, 'total_score' => 10]) ->create(); foreach ($scoreLinks as $scoreLink) { diff --git a/tests/Controllers/PasswordResetControllerTest.php b/tests/Controllers/PasswordResetControllerTest.php index 579bab20592..55fd6c6a30a 100644 --- a/tests/Controllers/PasswordResetControllerTest.php +++ b/tests/Controllers/PasswordResetControllerTest.php @@ -17,6 +17,8 @@ class PasswordResetControllerTest extends TestCase { + private string $origCacheDefault; + private static function randomPassword(): string { return str_random(10); @@ -283,6 +285,15 @@ protected function setUp(): void { parent::setUp(); $this->withoutMiddleware(ThrottleRequests::class); + // There's no easy way to clear data cache in redis otherwise + $this->origCacheDefault = $GLOBALS['cfg']['cache']['default']; + config_set('cache.default', 'array'); + } + + protected function tearDown(): void + { + parent::tearDown(); + config_set('cache.default', $this->origCacheDefault); } private function generateKey(User $user): string diff --git a/tests/Controllers/ScoresControllerTest.php b/tests/Controllers/ScoresControllerTest.php index ec4a585a24f..3620fa5b494 100644 --- a/tests/Controllers/ScoresControllerTest.php +++ b/tests/Controllers/ScoresControllerTest.php @@ -33,8 +33,8 @@ public function testDownload() public function testDownloadSoloScore() { $soloScore = SoloScore::factory() - ->withData(['legacy_score_id' => $this->score->getKey()]) ->create([ + 'legacy_score_id' => $this->score->getKey(), 'ruleset_id' => Beatmap::MODES[$this->score->getMode()], 'has_replay' => true, ]); diff --git a/tests/Jobs/RemoveBeatmapsetSoloScoresTest.php b/tests/Jobs/RemoveBeatmapsetSoloScoresTest.php index e378f83e857..df4d3cb592e 100644 --- a/tests/Jobs/RemoveBeatmapsetSoloScoresTest.php +++ b/tests/Jobs/RemoveBeatmapsetSoloScoresTest.php @@ -16,7 +16,6 @@ use App\Models\Group; use App\Models\Language; use App\Models\Solo\Score; -use App\Models\Solo\ScorePerformance; use App\Models\User; use App\Models\UserGroup; use App\Models\UserGroupEvent; @@ -36,9 +35,6 @@ public function testHandle() fn (): Score => $this->createScore($beatmapset), array_fill(0, 10, null), ); - foreach ($scores as $i => $score) { - $score->performance()->create(['pp' => rand(0, 1000)]); - } $userAdditionalScores = array_map( fn (Score $score) => $this->createScore($beatmapset, $score->user_id, $score->ruleset_id), $scores, @@ -48,12 +44,10 @@ public function testHandle() // These scores shouldn't be deleted for ($i = 0; $i < 10; $i++) { - $score = $this->createScore($beatmapset); - $score->performance()->create(['pp' => rand(0, 1000)]); + $this->createScore($beatmapset); } $this->expectCountChange(fn () => Score::count(), count($scores) * -2, 'removes scores'); - $this->expectCountChange(fn () => ScorePerformance::count(), count($scores) * -1, 'removes score performances'); static::reindexScores(); @@ -71,7 +65,6 @@ public function testHandle() Genre::truncate(); Language::truncate(); Score::select()->delete(); // TODO: revert to truncate after the table is actually renamed - ScorePerformance::select()->delete(); // TODO: revert to truncate after the table is actually renamed User::truncate(); UserGroup::truncate(); UserGroupEvent::truncate(); diff --git a/tests/Models/ContestTest.php b/tests/Models/ContestTest.php index acad06b2096..6fb7ed5e8db 100644 --- a/tests/Models/ContestTest.php +++ b/tests/Models/ContestTest.php @@ -78,7 +78,7 @@ public function testAssertVoteRequirementPlaylistBeatmapsets( MultiplayerScoreLink::factory()->state([ 'playlist_item_id' => $playlistItem, 'user_id' => $userId, - ])->completed([], [ + ])->completed([ 'ended_at' => $endedAt, 'passed' => $passed, ])->create(); diff --git a/tests/Models/Multiplayer/ScoreLinkTest.php b/tests/Models/Multiplayer/ScoreLinkTest.php index cc1e09f7800..efc84c75220 100644 --- a/tests/Models/Multiplayer/ScoreLinkTest.php +++ b/tests/Models/Multiplayer/ScoreLinkTest.php @@ -12,11 +12,26 @@ use App\Models\Multiplayer\PlaylistItem; use App\Models\Multiplayer\ScoreLink; use App\Models\ScoreToken; -use Carbon\Carbon; use Tests\TestCase; class ScoreLinkTest extends TestCase { + private static array $commonScoreParams; + + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + static::$commonScoreParams = [ + 'accuracy' => 0.5, + 'ended_at' => new \DateTime(), + 'max_combo' => 1, + 'statistics' => [ + 'great' => 1, + ], + 'total_score' => 1, + ]; + } + public function testRequiredModsMissing() { $playlistItem = PlaylistItem::factory()->create([ @@ -32,14 +47,10 @@ public function testRequiredModsMissing() $this->expectException(InvariantException::class); $this->expectExceptionMessage('This play does not include the mods required.'); ScoreLink::complete($scoreToken, [ + ...static::$commonScoreParams, 'beatmap_id' => $playlistItem->beatmap_id, 'ruleset_id' => $playlistItem->ruleset_id, 'user_id' => $scoreToken->user_id, - 'ended_at' => json_date(Carbon::now()), - 'mods' => [], - 'statistics' => [ - 'great' => 1, - ], ]); } @@ -57,14 +68,11 @@ public function testRequiredModsPresent() $this->expectNotToPerformAssertions(); ScoreLink::complete($scoreToken, [ + ...static::$commonScoreParams, 'beatmap_id' => $playlistItem->beatmap_id, + 'mods' => [['acronym' => 'HD']], 'ruleset_id' => $playlistItem->ruleset_id, 'user_id' => $scoreToken->user_id, - 'ended_at' => json_date(Carbon::now()), - 'mods' => [['acronym' => 'HD']], - 'statistics' => [ - 'great' => 1, - ], ]); } @@ -85,17 +93,14 @@ public function testExpectedAllowedMod() $this->expectNotToPerformAssertions(); ScoreLink::complete($scoreToken, [ + ...static::$commonScoreParams, 'beatmap_id' => $playlistItem->beatmap_id, - 'ruleset_id' => $playlistItem->ruleset_id, - 'user_id' => $scoreToken->user_id, - 'ended_at' => json_date(Carbon::now()), 'mods' => [ ['acronym' => 'DT'], ['acronym' => 'HD'], ], - 'statistics' => [ - 'great' => 1, - ], + 'ruleset_id' => $playlistItem->ruleset_id, + 'user_id' => $scoreToken->user_id, ]); } @@ -117,17 +122,14 @@ public function testUnexpectedAllowedMod() $this->expectException(InvariantException::class); $this->expectExceptionMessage('This play includes mods that are not allowed.'); ScoreLink::complete($scoreToken, [ + ...static::$commonScoreParams, 'beatmap_id' => $playlistItem->beatmap_id, - 'ruleset_id' => $playlistItem->ruleset_id, - 'user_id' => $scoreToken->user_id, - 'ended_at' => json_date(Carbon::now()), 'mods' => [ ['acronym' => 'DT'], ['acronym' => 'HD'], ], - 'statistics' => [ - 'great' => 1, - ], + 'ruleset_id' => $playlistItem->ruleset_id, + 'user_id' => $scoreToken->user_id, ]); } @@ -142,14 +144,11 @@ public function testUnexpectedModWhenNoModsAreAllowed() $this->expectException(InvariantException::class); $this->expectExceptionMessage('This play includes mods that are not allowed.'); ScoreLink::complete($scoreToken, [ + ...static::$commonScoreParams, 'beatmap_id' => $playlistItem->beatmap_id, + 'mods' => [['acronym' => 'HD']], 'ruleset_id' => $playlistItem->ruleset_id, 'user_id' => $scoreToken->user_id, - 'ended_at' => json_date(Carbon::now()), - 'mods' => [['acronym' => 'HD']], - 'statistics' => [ - 'great' => 1, - ], ]); } @@ -170,14 +169,11 @@ public function testUnexpectedModAcceptedIfAlwaysValidForSubmission() $this->expectNotToPerformAssertions(); ScoreLink::complete($scoreToken, [ + ...static::$commonScoreParams, 'beatmap_id' => $playlistItem->beatmap_id, + 'mods' => [['acronym' => 'TD']], 'ruleset_id' => $playlistItem->ruleset_id, 'user_id' => $scoreToken->user_id, - 'ended_at' => json_date(Carbon::now()), - 'mods' => [['acronym' => 'TD']], - 'statistics' => [ - 'great' => 1, - ], ]); } } diff --git a/tests/Models/Multiplayer/UserScoreAggregateTest.php b/tests/Models/Multiplayer/UserScoreAggregateTest.php index 37ded3882f9..6e4bc8f48cf 100644 --- a/tests/Models/Multiplayer/UserScoreAggregateTest.php +++ b/tests/Models/Multiplayer/UserScoreAggregateTest.php @@ -240,8 +240,9 @@ private function addPlay(User $user, PlaylistItem $playlistItem, array $params): [ 'beatmap_id' => $playlistItem->beatmap_id, 'ended_at' => json_time(new \DateTime()), - 'ruleset_id' => $playlistItem->ruleset_id, + 'max_combo' => 1, 'statistics' => ['good' => 1], + 'ruleset_id' => $playlistItem->ruleset_id, 'user_id' => $user->getKey(), ...$params, ], diff --git a/tests/Models/Solo/ScoreEsIndexTest.php b/tests/Models/Solo/ScoreEsIndexTest.php index 2b7fcf6d117..1b53c276887 100644 --- a/tests/Models/Solo/ScoreEsIndexTest.php +++ b/tests/Models/Solo/ScoreEsIndexTest.php @@ -40,7 +40,6 @@ public static function setUpBeforeClass(): void static::$beatmap = Beatmap::factory()->qualified()->create(); $scoreFactory = Score::factory()->state(['preserve' => true]); - $defaultData = ['build_id' => 1]; $mods = [ ['acronym' => 'DT', 'settings' => []], @@ -51,43 +50,44 @@ public static function setUpBeforeClass(): void ]; static::$scores = [ - 'otherUser' => $scoreFactory->withData($defaultData, [ - 'total_score' => 1150, + 'otherUser' => $scoreFactory->withData([ 'mods' => $unrelatedMods, ])->create([ 'beatmap_id' => static::$beatmap, + 'total_score' => 1150, 'user_id' => $otherUser, ]), - 'otherUserMods' => $scoreFactory->withData($defaultData, [ - 'total_score' => 1140, + 'otherUserMods' => $scoreFactory->withData([ 'mods' => $mods, ])->create([ 'beatmap_id' => static::$beatmap, + 'total_score' => 1140, 'user_id' => $otherUser, ]), - 'otherUser2' => $scoreFactory->withData($defaultData, [ - 'total_score' => 1150, + 'otherUser2' => $scoreFactory->withData([ 'mods' => $mods, ])->create([ 'beatmap_id' => static::$beatmap, + 'total_score' => 1150, 'user_id' => User::factory()->state(['country_acronym' => Country::factory()]), ]), - 'otherUser3SameCountry' => $scoreFactory->withData($defaultData, [ - 'total_score' => 1130, + 'otherUser3SameCountry' => $scoreFactory->withData([ 'mods' => $unrelatedMods, ])->create([ 'beatmap_id' => static::$beatmap, + 'total_score' => 1130, 'user_id' => User::factory()->state(['country_acronym' => static::$user->country_acronym]), ]), - 'user' => $scoreFactory->withData($defaultData, ['total_score' => 1100])->create([ + 'user' => $scoreFactory->create([ 'beatmap_id' => static::$beatmap, + 'total_score' => 1100, 'user_id' => static::$user, ]), - 'userMods' => $scoreFactory->withData($defaultData, [ - 'total_score' => 1050, + 'userMods' => $scoreFactory->withData([ 'mods' => $mods, ])->create([ 'beatmap_id' => static::$beatmap, + 'total_score' => 1050, 'user_id' => static::$user, ]), ]; diff --git a/tests/Models/Solo/ScoreTest.php b/tests/Models/Solo/ScoreTest.php index 5f300cc4dfb..550bc08dd04 100644 --- a/tests/Models/Solo/ScoreTest.php +++ b/tests/Models/Solo/ScoreTest.php @@ -50,8 +50,8 @@ public function testLegacyPassScoreRetainsRank() 'user_id' => 1, ]); - $this->assertTrue($score->data->passed); - $this->assertSame($score->data->rank, 'S'); + $this->assertTrue($score->passed); + $this->assertSame($score->rank, 'S'); $legacy = $score->createLegacyEntryOrExplode(); @@ -75,8 +75,8 @@ public function testLegacyFailScoreIsRankF() 'user_id' => 1, ]); - $this->assertFalse($score->data->passed); - $this->assertSame($score->data->rank, 'F'); + $this->assertFalse($score->passed); + $this->assertSame($score->rank, 'F'); $legacy = $score->createLegacyEntryOrExplode(); @@ -132,13 +132,15 @@ public function testLegacyScoreHitCountsFromStudlyCaseStatistics() public function testModsPropertyType() { - $score = new Score(['data' => [ + $score = new Score([ 'beatmap_id' => 0, + 'data' => [ + 'mods' => [['acronym' => 'DT']], + ], 'ended_at' => json_time(now()), - 'mods' => [['acronym' => 'DT']], 'ruleset_id' => 0, 'user_id' => 0, - ]]); + ]); $this->assertTrue($score->data->mods[0] instanceof stdClass, 'mods entry should be of type stdClass'); } @@ -147,8 +149,7 @@ public function testWeightedPp(): void { $pp = 10; $weight = 0.5; - $score = Score::factory()->create(); - $score->performance()->create(['pp' => $pp]); + $score = Score::factory()->create(['pp' => $pp]); $score->weight = $weight; $this->assertSame($score->weightedPp(), $pp * $weight); @@ -156,7 +157,7 @@ public function testWeightedPp(): void public function testWeightedPpWithoutPerformance(): void { - $score = Score::factory()->create(); + $score = Score::factory()->create(['pp' => null]); $score->weight = 0.5; $this->assertNull($score->weightedPp()); @@ -164,8 +165,7 @@ public function testWeightedPpWithoutPerformance(): void public function testWeightedPpWithoutWeight(): void { - $score = Score::factory()->create(); - $score->performance()->create(['pp' => 10]); + $score = Score::factory()->create(['pp' => 10]); $this->assertNull($score->weightedPp()); }