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\Models\Multiplayer;
9
10use App\Exceptions\InvariantException;
11use App\Models\Model;
12use App\Models\ScoreToken;
13use App\Models\Solo\Score;
14use App\Models\User;
15use Illuminate\Database\Eloquent\Relations\BelongsTo;
16use Illuminate\Database\Eloquent\Relations\HasOne;
17
18/**
19 * @property PlaylistItem $playlistItem
20 * @property int $playlist_item_id
21 * @property Score $score
22 * @property int $score_id
23 * @property User $user
24 * @property int $user_id
25 */
26class ScoreLink extends Model
27{
28 public static function complete(ScoreToken $token, array $params): static
29 {
30 return \DB::transaction(function () use ($params, $token) {
31 // multiplayer scores are always preserved.
32 $score = Score::createFromJsonOrExplode([...$params, 'preserve' => true]);
33
34 $playlistItem = $token->playlistItem()->firstOrFail();
35 $requiredMods = array_column($playlistItem->required_mods, 'acronym');
36 $mods = array_column($score->data->mods, 'acronym');
37 $mods = app('mods')->excludeModsAlwaysValidForSubmission($playlistItem->ruleset_id, $mods);
38
39 if (!empty($requiredMods)) {
40 if (!empty(array_diff($requiredMods, $mods))) {
41 throw new InvariantException('This play does not include the mods required.');
42 }
43 }
44
45 $allowedMods = array_column($playlistItem->allowed_mods, 'acronym');
46 if (!empty(array_diff($mods, $requiredMods, $allowedMods))) {
47 throw new InvariantException('This play includes mods that are not allowed.');
48 }
49
50 $token->score()->associate($score)->saveOrExplode();
51
52 $ret = (new static())
53 ->playlistItem()->associate($playlistItem)
54 ->score()->associate($score)
55 ->user()->associate($token->user);
56 $ret->saveOrExplode();
57
58 return $ret;
59 });
60 }
61
62 public $incrementing = false;
63 public $timestamps = false;
64
65 protected $primaryKey = 'score_id';
66 protected $table = 'multiplayer_playlist_item_scores';
67
68 public function playlistItem(): BelongsTo
69 {
70 return $this->belongsTo(PlaylistItem::class, 'playlist_item_id');
71 }
72
73 public function playlistItemUserHighScore(): HasOne
74 {
75 return $this->hasOne(PlaylistItemUserHighScore::class);
76 }
77
78 public function score(): BelongsTo
79 {
80 return $this->belongsTo(Score::class, 'score_id');
81 }
82
83 public function user(): BelongsTo
84 {
85 return $this->belongsTo(User::class, 'user_id');
86 }
87
88 public function getAttribute($key)
89 {
90 return match ($key) {
91 'playlist_item_id',
92 'score_id',
93 'user_id' => $this->getRawAttribute($key),
94
95 'playlistItem',
96 'playlistItemUserHighScore',
97 'score',
98 'user' => $this->getRelationValue($key),
99 };
100 }
101
102 public function position(): ?int
103 {
104 $score = $this->score;
105
106 if ($score === null) {
107 return null;
108 }
109
110 $query = PlaylistItemUserHighScore
111 ::where('playlist_item_id', $this->playlist_item_id)
112 ->whereHas('user', fn ($userQuery) => $userQuery->default())
113 ->cursorSort('score_asc', [
114 'total_score' => $score->total_score,
115 'score_id' => $this->getKey(),
116 ]);
117
118 return 1 + $query->count();
119 }
120}