From b008e65f9001b55a5e7d253f42a8fdb639710982 Mon Sep 17 00:00:00 2001 From: Xetera Date: Thu, 22 Apr 2021 20:57:15 +0300 Subject: [PATCH 001/470] feature: added english rank_summary translations --- resources/lang/en/users.php | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lang/en/users.php b/resources/lang/en/users.php index 4197692922c..3cc90ad1700 100644 --- a/resources/lang/en/users.php +++ b/resources/lang/en/users.php @@ -141,6 +141,7 @@ ], 'show' => [ 'age' => ':age years old', + 'rank_summary' => ':username is rank :global worldwide and rank :local in :country.', 'change_avatar' => 'change your avatar!', 'first_members' => 'Here since the beginning', 'is_developer' => 'osu!developer', From ad5f2c37047bc802e31de1919d810752bcf8ca0f Mon Sep 17 00:00:00 2001 From: Xetera Date: Thu, 22 Apr 2021 20:57:44 +0300 Subject: [PATCH 002/470] feature: updated user profile template --- resources/views/users/show.blade.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/resources/views/users/show.blade.php b/resources/views/users/show.blade.php index 227b8546a0c..ed6f6abedf9 100644 --- a/resources/views/users/show.blade.php +++ b/resources/views/users/show.blade.php @@ -3,8 +3,19 @@ See the LICENCE file in the repository root for full licence text. --}} @extends('master', [ + 'canonicalUrl' => $user->url(), 'titlePrepend' => blade_safe(str_replace(' ', ' ', e($user->username))), - 'pageDescription' => page_description($user->username), + 'pageDescription' => trans('users.show.rank_summary', [ + 'username' => $user->username, + 'global' => $data["statistics"]["global_rank"], + 'local' => $data["statistics"]["country_rank"], + 'country' => $data["country"]["name"], + ]), + 'opengraph' => [ + 'title' => $user->username . "'s Profile", + 'image' => str_replace('http://localhost:8080', 'https://ffvyiglk.tunnelto.dev', $data["avatar_url"]) + // 'image' => json_encode($data), + ] ]) @section('content') From 22429d4971f34474d8e80c23d9207e513bd75388 Mon Sep 17 00:00:00 2001 From: Xetera Date: Thu, 22 Apr 2021 21:00:30 +0300 Subject: [PATCH 003/470] refactor: moved hardcoded title to translations --- resources/views/users/show.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/users/show.blade.php b/resources/views/users/show.blade.php index ed6f6abedf9..d4834439c1a 100644 --- a/resources/views/users/show.blade.php +++ b/resources/views/users/show.blade.php @@ -12,7 +12,7 @@ 'country' => $data["country"]["name"], ]), 'opengraph' => [ - 'title' => $user->username . "'s Profile", + 'title' => trans('users.show.title', ["username" => $user->username]), 'image' => str_replace('http://localhost:8080', 'https://ffvyiglk.tunnelto.dev', $data["avatar_url"]) // 'image' => json_encode($data), ] From 52ed55cc99048fe11bc274696e694165963ef467 Mon Sep 17 00:00:00 2001 From: Xetera Date: Thu, 22 Apr 2021 21:04:58 +0300 Subject: [PATCH 004/470] refactor: cleaned up template --- resources/views/users/show.blade.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/views/users/show.blade.php b/resources/views/users/show.blade.php index d4834439c1a..1a14db4d3ff 100644 --- a/resources/views/users/show.blade.php +++ b/resources/views/users/show.blade.php @@ -13,8 +13,7 @@ ]), 'opengraph' => [ 'title' => trans('users.show.title', ["username" => $user->username]), - 'image' => str_replace('http://localhost:8080', 'https://ffvyiglk.tunnelto.dev', $data["avatar_url"]) - // 'image' => json_encode($data), + 'image' => $data["avatar_url"] ] ]) From 47cea08f6d73ef6347015aa34d49e76caca08fcf Mon Sep 17 00:00:00 2001 From: Xetera Date: Sat, 24 Apr 2021 03:19:58 +0300 Subject: [PATCH 005/470] refactor: changed description formatting --- resources/views/users/show.blade.php | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/resources/views/users/show.blade.php b/resources/views/users/show.blade.php index 1a14db4d3ff..ea82e06cf0a 100644 --- a/resources/views/users/show.blade.php +++ b/resources/views/users/show.blade.php @@ -2,21 +2,27 @@ Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. See the LICENCE file in the repository root for full licence text. --}} + +@php + $userData = $jsonChunks["user"]; + $stats = $userData["statistics"]; + $globalRankLabel = trans('users.show.rank.global_simple'); + $globalRankValue = number_format($stats["global_rank"]); + $countryRankLabel = trans('users.show.rank.country_simple'); + $countryRankValue = number_format($stats["country_rank"]); +@endphp + @extends('master', [ 'canonicalUrl' => $user->url(), 'titlePrepend' => blade_safe(str_replace(' ', ' ', e($user->username))), - 'pageDescription' => trans('users.show.rank_summary', [ - 'username' => $user->username, - 'global' => $data["statistics"]["global_rank"], - 'local' => $data["statistics"]["country_rank"], - 'country' => $data["country"]["name"], - ]), + 'pageDescription' => "{$globalRankLabel}: #{$globalRankValue}\n{$countryRankLabel}: #{$countryRankValue}", 'opengraph' => [ 'title' => trans('users.show.title', ["username" => $user->username]), - 'image' => $data["avatar_url"] + 'image' => $userData["avatar_url"] ] ]) + @section('content') @if (Auth::user() && Auth::user()->isAdmin() && $user->isRestricted()) @include('objects._notification_banner', [ @@ -29,6 +35,7 @@
@endsection + @section ("script") @parent From b09612429e48b6c67bf9da33bdd915cadda35b1c Mon Sep 17 00:00:00 2001 From: Xetera Date: Sat, 24 Apr 2021 03:46:32 +0300 Subject: [PATCH 006/470] cleanup: extra spacing --- resources/views/users/show.blade.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/views/users/show.blade.php b/resources/views/users/show.blade.php index ea82e06cf0a..dc21bf15b54 100644 --- a/resources/views/users/show.blade.php +++ b/resources/views/users/show.blade.php @@ -22,7 +22,6 @@ ] ]) - @section('content') @if (Auth::user() && Auth::user()->isAdmin() && $user->isRestricted()) @include('objects._notification_banner', [ @@ -35,7 +34,6 @@
@endsection - @section ("script") @parent From bd1c78eb6d370563313722c29c53339841e44caf Mon Sep 17 00:00:00 2001 From: Xetera Date: Sat, 24 Apr 2021 03:48:28 +0300 Subject: [PATCH 007/470] cleanup: removed unused translation --- resources/lang/en/users.php | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/lang/en/users.php b/resources/lang/en/users.php index 3cc90ad1700..4197692922c 100644 --- a/resources/lang/en/users.php +++ b/resources/lang/en/users.php @@ -141,7 +141,6 @@ ], 'show' => [ 'age' => ':age years old', - 'rank_summary' => ':username is rank :global worldwide and rank :local in :country.', 'change_avatar' => 'change your avatar!', 'first_members' => 'Here since the beginning', 'is_developer' => 'osu!developer', From ab4404590b2e335e33fd2021d403adaa6b83057e Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 6 Nov 2022 12:24:17 -0800 Subject: [PATCH 008/470] Clean up GithubUser model and transformer --- app/Models/GithubUser.php | 52 ++++++++++++++++------ app/Transformers/GithubUserTransformer.php | 8 ++-- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/app/Models/GithubUser.php b/app/Models/GithubUser.php index b293e7fbf83..5fa43465bb0 100644 --- a/app/Models/GithubUser.php +++ b/app/Models/GithubUser.php @@ -3,21 +3,28 @@ // Copyright (c) ppy Pty Ltd . 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); + namespace App\Models; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; + /** * @property int|null $canonical_id - * @property \Illuminate\Database\Eloquent\Collection $changelogEntries ChangelogEntry + * @property-read \Illuminate\Database\Eloquent\Collection $changelogEntries * @property \Carbon\Carbon|null $created_at + * @property-read string|null $created_at_json * @property int $id * @property \Carbon\Carbon|null $updated_at - * @property User $user + * @property-read string|null $updated_at_json + * @property-read User|null $user * @property int|null $user_id * @property string|null $username */ class GithubUser extends Model { - public static function importFromGithub($data) + public static function importFromGithub(array $data): static { $githubUser = static::where('canonical_id', '=', $data['id'])->first(); @@ -39,39 +46,58 @@ public static function importFromGithub($data) return $githubUser; } - public function user() + public function changelogEntries(): HasMany { - return $this->belongsTo(User::class, 'user_id'); + return $this->hasMany(ChangelogEntry::class); } - public function changelogEntries() + public function user(): BelongsTo { - return $this->hasMany(ChangelogEntry::class); + return $this->belongsTo(User::class, 'user_id'); } - public function displayName() + public function displayName(): string { return presence($this->username) - ?? optional($this->user)->username + ?? $this->osuUsername() ?? '[no name]'; } - public function githubUrl() + public function githubUrl(): ?string { if (present($this->username)) { return "https://github.com/{$this->username}"; } } - public function userUrl() + public function osuUsername(): ?string + { + return $this->user?->username; + } + + public function userUrl(): ?string { if ($this->user_id !== null) { return route('users.show', $this->user_id); } } - public function url() + public function getAttribute($key) { - return $this->githubUrl() ?? $this->userUrl(); + return match ($key) { + 'canonical_id', + 'id', + 'user_id', + 'username' => $this->getRawAttribute($key), + + 'created_at', + 'updated_at' => $this->getTimeFast($key), + + 'created_at_json', + 'updated_at_json' => $this->getJsonTimeFast($key), + + 'changelogEntries', + 'user' => $this->getRelationValue($key), + }; } } diff --git a/app/Transformers/GithubUserTransformer.php b/app/Transformers/GithubUserTransformer.php index 14470631888..291743f891a 100644 --- a/app/Transformers/GithubUserTransformer.php +++ b/app/Transformers/GithubUserTransformer.php @@ -3,19 +3,21 @@ // Copyright (c) ppy Pty Ltd . 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); + namespace App\Transformers; use App\Models\GithubUser; class GithubUserTransformer extends TransformerAbstract { - public function transform(GithubUser $githubUser) + public function transform(GithubUser $githubUser): array { return [ - 'id' => $githubUser->getKey(), 'display_name' => $githubUser->displayName(), 'github_url' => $githubUser->githubUrl(), - 'osu_username' => optional($githubUser->user)->username, + 'id' => $githubUser->getKey(), + 'osu_username' => $githubUser->osuUsername(), 'user_id' => $githubUser->user_id, 'user_url' => $githubUser->userUrl(), ]; From d478ea9078a9c803a40a533cb118e63bdd821fb8 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 6 Nov 2022 12:24:18 -0800 Subject: [PATCH 009/470] Add User associate option to `GithubUser::importFromGithub()` --- app/Models/GithubUser.php | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/app/Models/GithubUser.php b/app/Models/GithubUser.php index 5fa43465bb0..764206d569d 100644 --- a/app/Models/GithubUser.php +++ b/app/Models/GithubUser.php @@ -24,26 +24,25 @@ */ class GithubUser extends Model { - public static function importFromGithub(array $data): static + public static function importFromGithub(array $apiUser, ?User $user = null): static { - $githubUser = static::where('canonical_id', '=', $data['id'])->first(); + $params = [ + 'canonical_id' => $apiUser['id'], + 'username' => $apiUser['login'], + ]; + if ($user !== null) { + $params['user_id'] = $user->getKey(); + } + + $githubUser = static::where('canonical_id', $params['canonical_id'])->first() + ?? static::where('username', $params['username'])->last(); - if (isset($githubUser)) { - $githubUser->update(['username' => $data['login']]); + if ($githubUser === null) { + return static::create($params); } else { - $githubUser = static::where('username', '=', $data['login'])->last(); - - if (isset($githubUser)) { - $githubUser->update(['canonical_id' => $data['id']]); - } else { - $githubUser = static::create([ - 'canonical_id' => $data['id'], - 'username' => $data['login'], - ]); - } + $githubUser->update($params); + return $githubUser; } - - return $githubUser; } public function changelogEntries(): HasMany From 72dd9ce34e7c017c1861fd29e125433cb8520bd2 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 6 Nov 2022 12:24:18 -0800 Subject: [PATCH 010/470] Add endpoints to link and unlink GitHub accounts from users --- .env.example | 4 + .../Account/GithubUsersController.php | 100 ++++++++++++++++++ config/osu.php | 4 + routes/web.php | 3 + 4 files changed, 111 insertions(+) create mode 100644 app/Http/Controllers/Account/GithubUsersController.php diff --git a/.env.example b/.env.example index ab6adaadaee..197e21ac5ca 100644 --- a/.env.example +++ b/.env.example @@ -104,6 +104,10 @@ PUSHER_SECRET= # GITHUB_TOKEN= +# GitHub client for users to associate their GitHub accounts +# GITHUB_CLIENT_ID= +# GITHUB_CLIENT_SECRET= + # DATADOG_ENABLED=true # DATADOG_PREFIX=osu.web # DATADOG_API_KEY= diff --git a/app/Http/Controllers/Account/GithubUsersController.php b/app/Http/Controllers/Account/GithubUsersController.php new file mode 100644 index 00000000000..b09d30d940b --- /dev/null +++ b/app/Http/Controllers/Account/GithubUsersController.php @@ -0,0 +1,100 @@ +. 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); + +namespace App\Http\Controllers\Account; + +use App\Http\Controllers\Controller; +use App\Models\GithubUser; +use Github\Client as GithubClient; +use GuzzleHttp\Client as HttpClient; + +class GithubUsersController extends Controller +{ + public function __construct() + { + $this->middleware('auth'); + $this->middleware('verify-user'); + + parent::__construct(); + } + + public function callback() + { + $params = get_params(request()->all(), null, [ + 'code:string', + 'state:string', + ]); + + abort_unless(isset($params['code']), 422, 'Invalid code.'); + abort_unless( + isset($params['state']) && $params['state'] === session()->pull('github_auth_state'), + 403, + 'Invalid state.', + ); + + $tokenResponseBody = (new HttpClient()) + ->request('POST', 'https://github.com/login/oauth/access_token', [ + 'query' => [ + 'client_id' => config('osu.github.client_id'), + 'client_secret' => config('osu.github.client_secret'), + 'code' => $params['code'], + ], + ]) + ->getBody() + ->getContents(); + parse_str($tokenResponseBody, $tokenResponseBodyParams); + $token = $tokenResponseBodyParams['access_token'] ?? null; + + abort_if($token === null, 500, 'Invalid response from GitHub API.'); + + $githubClient = new GithubClient(); + $githubClient->authenticate($token, null, GithubClient::AUTH_ACCESS_TOKEN); + $githubApiUser = $githubClient->currentUser()->show(); + + GithubUser::importFromGithub($githubApiUser, auth()->user()); + + return redirect(route('account.edit').'#github'); + } + + public function create() + { + abort_if( + config('osu.github.client_id') === null || config('osu.github.client_secret') === null, + 404, + ); + abort_if( + auth()->user()->githubUsers()->count() >= 10, + 403, + 'Too many GitHub accounts.', + ); + + $state = bin2hex(random_bytes(32)); + session()->put('github_auth_state', $state); + + return redirect('https://github.com/login/oauth/authorize?'.http_build_query([ + 'allow_signup' => 'false', + 'client_id' => config('osu.github.client_id'), + 'scope' => '', + 'state' => $state, + ])); + } + + public function destroy(int $id) + { + $githubUser = auth()->user()->githubUsers()->findOrFail($id); + + abort_if( + $githubUser->canonical_id === null || $githubUser->username === null, + 422, + 'Cannot unassociate user from GitHub user without a valid ID or username.', + ); + + $githubUser->update(['user_id' => null]); + + return response(null, 204); + } +} diff --git a/config/osu.php b/config/osu.php index cfd52460fb0..2f3c33c7e50 100644 --- a/config/osu.php +++ b/config/osu.php @@ -123,6 +123,10 @@ 'git-sha' => presence(env('GIT_SHA')) ?? (file_exists(__DIR__.'/../version') ? trim(file_get_contents(__DIR__.'/../version')) : null) ?? 'unknown-version', + 'github' => [ + 'client_id' => presence(env('GITHUB_CLIENT_ID')), + 'client_secret' => presence(env('GITHUB_CLIENT_SECRET')), + ], 'is_development_deploy' => get_bool(env('IS_DEVELOPMENT_DEPLOY')) ?? true, 'landing' => [ 'video_url' => env('LANDING_VIDEO_URL', 'https://assets.ppy.sh/media/landing.mp4'), diff --git a/routes/web.php b/routes/web.php index 70def786b13..ec1bd4d906a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -214,6 +214,9 @@ Route::get('verify', 'AccountController@verifyLink'); Route::post('verify', 'AccountController@verify')->name('verify'); Route::put('/', 'AccountController@update')->name('update'); + + Route::get('github-users/callback', 'Account\GithubUsersController@callback'); + Route::resource('github-users', 'Account\GithubUsersController', ['only' => ['create', 'destroy']]); }); Route::get('quick-search', 'HomeController@quickSearch')->name('quick-search'); From 63178a8cf4c28967886c833d4218e3b9a22631d3 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 6 Nov 2022 12:24:18 -0800 Subject: [PATCH 011/470] Add GitHub account options to account edit page --- app/Http/Controllers/AccountController.php | 9 +++ app/Transformers/GithubUserTransformer.php | 1 + resources/assets/less/bem-index.less | 1 + resources/assets/less/bem/github-user.less | 15 +++++ .../assets/lib/account-edit/github-user.tsx | 64 +++++++++++++++++++ .../assets/lib/account-edit/github-users.tsx | 53 +++++++++++++++ .../assets/lib/entrypoints/account-edit.tsx | 7 +- .../assets/lib/interfaces/github-user-json.ts | 17 +++++ resources/lang/en/accounts.php | 7 ++ .../accounts/_edit_github_users.blade.php | 24 +++++++ resources/views/accounts/edit.blade.php | 8 +++ 11 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 resources/assets/less/bem/github-user.less create mode 100644 resources/assets/lib/account-edit/github-user.tsx create mode 100644 resources/assets/lib/account-edit/github-users.tsx create mode 100644 resources/assets/lib/interfaces/github-user-json.ts create mode 100644 resources/views/accounts/_edit_github_users.blade.php diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 27635db31e0..37184dddc96 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -113,10 +113,19 @@ public function edit() $notificationOptions = $user->notificationOptions->keyBy('name'); + $githubUsers = json_collection( + $user + ->githubUsers() + ->whereNotNull(['canonical_id', 'username']) + ->get(), + 'GithubUser', + ); + return ext_view('accounts.edit', compact( 'authorizedClients', 'blocks', 'currentSessionId', + 'githubUsers', 'notificationOptions', 'ownClients', 'sessions' diff --git a/app/Transformers/GithubUserTransformer.php b/app/Transformers/GithubUserTransformer.php index 291743f891a..cb8bec41606 100644 --- a/app/Transformers/GithubUserTransformer.php +++ b/app/Transformers/GithubUserTransformer.php @@ -16,6 +16,7 @@ public function transform(GithubUser $githubUser): array return [ 'display_name' => $githubUser->displayName(), 'github_url' => $githubUser->githubUrl(), + 'github_username' => $githubUser->username, 'id' => $githubUser->getKey(), 'osu_username' => $githubUser->osuUsername(), 'user_id' => $githubUser->user_id, diff --git a/resources/assets/less/bem-index.less b/resources/assets/less/bem-index.less index e5e911710b8..9146f5ffd32 100644 --- a/resources/assets/less/bem-index.less +++ b/resources/assets/less/bem-index.less @@ -182,6 +182,7 @@ @import "bem/gallery-thumbnails"; @import "bem/game-mode"; @import "bem/game-mode-link"; +@import "bem/github-user"; @import "bem/grid"; @import "bem/grid-cell"; @import "bem/grid-items"; diff --git a/resources/assets/less/bem/github-user.less b/resources/assets/less/bem/github-user.less new file mode 100644 index 00000000000..c3bf787d90f --- /dev/null +++ b/resources/assets/less/bem/github-user.less @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +.github-user { + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid @osu-colour-b5; + margin-bottom: 10px; + padding-bottom: 10px; + + &__name { + font-size: @font-size--title-small-3; + } +} diff --git a/resources/assets/lib/account-edit/github-user.tsx b/resources/assets/lib/account-edit/github-user.tsx new file mode 100644 index 00000000000..fd5dc4e924f --- /dev/null +++ b/resources/assets/lib/account-edit/github-user.tsx @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +import BigButton from 'components/big-button'; +import { GithubUserJsonForAccountEdit } from 'interfaces/github-user-json'; +import { route } from 'laroute'; +import { action, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { onErrorWithCallback } from 'utils/ajax'; + +interface Props { + onDelete: (id: number) => void; + user: GithubUserJsonForAccountEdit; +} + +@observer +export default class GithubUser extends React.Component { + @observable private deleting = false; + private xhr?: JQuery.jqXHR; + + constructor(props: Props) { + super(props); + makeObservable(this); + } + + componentWillUnmount() { + this.xhr?.abort(); + } + + render() { + return ( + + ); + } + + @action + private onDeleteButtonClick = () => { + this.xhr?.abort(); + this.deleting = true; + + this.xhr = $.ajax( + route('account.github-users.destroy', { github_user: this.props.user.id }), + { method: 'DELETE' }, + ) + .done(() => this.props.onDelete(this.props.user.id)) + .fail(onErrorWithCallback(this.onDeleteButtonClick)) + .always(action(() => this.deleting = false)); + }; +} diff --git a/resources/assets/lib/account-edit/github-users.tsx b/resources/assets/lib/account-edit/github-users.tsx new file mode 100644 index 00000000000..74e99689746 --- /dev/null +++ b/resources/assets/lib/account-edit/github-users.tsx @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +import BigButton from 'components/big-button'; +import { GithubUserJsonForAccountEdit } from 'interfaces/github-user-json'; +import { route } from 'laroute'; +import { action, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import GithubUser from './github-user'; + +interface Props { + users: GithubUserJsonForAccountEdit[]; +} + +@observer +export default class GithubUsers extends React.Component { + @observable private users: GithubUserJsonForAccountEdit[]; + + constructor(props: Props) { + super(props); + + this.users = props.users; + + makeObservable(this); + } + + render() { + return ( + <> + {this.users.length === 0 + ?
{osu.trans('accounts.github_users.none')}
+ : this.users.map((user) => ( + + ))} + + + ); + } + + @action + private onDelete = (id: number) => { + this.users = this.users.filter((user) => user.id !== id); + }; +} diff --git a/resources/assets/lib/entrypoints/account-edit.tsx b/resources/assets/lib/entrypoints/account-edit.tsx index 919250dfd24..d4ca2d06b65 100644 --- a/resources/assets/lib/entrypoints/account-edit.tsx +++ b/resources/assets/lib/entrypoints/account-edit.tsx @@ -1,13 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. // See the LICENCE file in the repository root for full licence text. +import GithubUsers from 'account-edit/github-users'; import { ClientJson } from 'interfaces/client-json'; import { OwnClientJson } from 'interfaces/own-client-json'; import { AuthorizedClients } from 'oauth/authorized-clients'; import { OwnClients } from 'oauth/own-clients'; import core from 'osu-core-singleton'; import * as React from 'react'; -import { parseJsonNullable } from 'utils/json'; +import { parseJson, parseJsonNullable } from 'utils/json'; core.reactTurbolinks.register('authorized-clients', () => { const json = parseJsonNullable('json-authorized-clients', true); @@ -18,6 +19,10 @@ core.reactTurbolinks.register('authorized-clients', () => { return ; }); +core.reactTurbolinks.register('github-users', () => ( + +)); + core.reactTurbolinks.register('own-clients', () => { const json = parseJsonNullable('json-own-clients', true); if (json != null) { diff --git a/resources/assets/lib/interfaces/github-user-json.ts b/resources/assets/lib/interfaces/github-user-json.ts new file mode 100644 index 00000000000..f6293aeadf9 --- /dev/null +++ b/resources/assets/lib/interfaces/github-user-json.ts @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +export type GithubUserJsonForAccountEdit = GithubUserJson & { + github_url: string; + github_username: string; +}; + +export default interface GithubUserJson { + display_name: string; + github_url: string | null; + github_username: string | null; + id: number; + osu_username: string | null; + user_id: number | null; + user_url: string | null; +} diff --git a/resources/lang/en/accounts.php b/resources/lang/en/accounts.php index 627d07d9fa9..dd534823187 100644 --- a/resources/lang/en/accounts.php +++ b/resources/lang/en/accounts.php @@ -46,6 +46,13 @@ ], ], + 'github_users' => [ + 'accounts' => 'accounts', + 'link' => 'Link GitHub account', + 'none' => 'No GitHub accounts', + 'title' => 'GitHub', + ], + 'notifications' => [ 'beatmapset_discussion_qualified_problem' => 'receive notifications for new problems on qualified beatmaps of the following modes', 'beatmapset_disqualify' => 'receive notifications for when beatmaps of the following modes are disqualified', diff --git a/resources/views/accounts/_edit_github_users.blade.php b/resources/views/accounts/_edit_github_users.blade.php new file mode 100644 index 00000000000..6f75f11fe02 --- /dev/null +++ b/resources/views/accounts/_edit_github_users.blade.php @@ -0,0 +1,24 @@ +{{-- + Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. + See the LICENCE file in the repository root for full licence text. +--}} + diff --git a/resources/views/accounts/edit.blade.php b/resources/views/accounts/edit.blade.php index d642afc39b9..184f54f8e89 100644 --- a/resources/views/accounts/edit.blade.php +++ b/resources/views/accounts/edit.blade.php @@ -163,6 +163,10 @@ class="js-account-edit-avatar__button fileupload"
@include('accounts._edit_oauth')
+ +
+ @include('accounts._edit_github_users') +
@endsection @section("script") @@ -170,6 +174,10 @@ class="js-account-edit-avatar__button fileupload" {!! json_encode($authorizedClients) !!} + + From 7581e68537b51a6515d664db664c467987e4bf32 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 6 Nov 2022 12:24:18 -0800 Subject: [PATCH 012/470] Only show GitHub menu if client info is set --- .../Account/GithubUsersController.php | 5 +---- app/Http/Controllers/AccountController.php | 19 ++++++++++++------- app/Models/GithubUser.php | 6 ++++++ resources/views/accounts/edit.blade.php | 16 ++++++++++------ 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/app/Http/Controllers/Account/GithubUsersController.php b/app/Http/Controllers/Account/GithubUsersController.php index b09d30d940b..16b1a1b07cc 100644 --- a/app/Http/Controllers/Account/GithubUsersController.php +++ b/app/Http/Controllers/Account/GithubUsersController.php @@ -62,10 +62,7 @@ public function callback() public function create() { - abort_if( - config('osu.github.client_id') === null || config('osu.github.client_secret') === null, - 404, - ); + abort_unless(GithubUser::canAuthenticate(), 404); abort_if( auth()->user()->githubUsers()->count() >= 10, 403, diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 37184dddc96..f8effb7b328 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -11,6 +11,7 @@ use App\Libraries\UserVerificationState; use App\Mail\UserEmailUpdated; use App\Mail\UserPasswordUpdated; +use App\Models\GithubUser; use App\Models\OAuth\Client; use App\Models\UserAccountHistory; use App\Models\UserNotificationOption; @@ -113,13 +114,17 @@ public function edit() $notificationOptions = $user->notificationOptions->keyBy('name'); - $githubUsers = json_collection( - $user - ->githubUsers() - ->whereNotNull(['canonical_id', 'username']) - ->get(), - 'GithubUser', - ); + if (GithubUser::canAuthenticate()) { + $githubUsers = json_collection( + $user + ->githubUsers() + ->whereNotNull(['canonical_id', 'username']) + ->get(), + 'GithubUser', + ); + } else { + $githubUsers = null; + } return ext_view('accounts.edit', compact( 'authorizedClients', diff --git a/app/Models/GithubUser.php b/app/Models/GithubUser.php index 764206d569d..d648f4a08dd 100644 --- a/app/Models/GithubUser.php +++ b/app/Models/GithubUser.php @@ -24,6 +24,12 @@ */ class GithubUser extends Model { + public static function canAuthenticate(): bool + { + return config('osu.github.client_id') !== null + && config('osu.github.client_secret') !== null; + } + public static function importFromGithub(array $apiUser, ?User $user = null): static { $params = [ diff --git a/resources/views/accounts/edit.blade.php b/resources/views/accounts/edit.blade.php index 184f54f8e89..52939d6823e 100644 --- a/resources/views/accounts/edit.blade.php +++ b/resources/views/accounts/edit.blade.php @@ -164,9 +164,11 @@ class="js-account-edit-avatar__button fileupload" @include('accounts._edit_oauth') -
- @include('accounts._edit_github_users') -
+ @if (\App\Models\GithubUser::canAuthenticate()) +
+ @include('accounts._edit_github_users') +
+ @endif @endsection @section("script") @@ -174,9 +176,11 @@ class="js-account-edit-avatar__button fileupload" {!! json_encode($authorizedClients) !!} - + @if (\App\Models\GithubUser::canAuthenticate()) + + @endif @if (\App\Models\GithubUser::canAuthenticate()) - @endif From 542425814e2ae6d218b06c7622098edf75635070 Mon Sep 17 00:00:00 2001 From: clayton Date: Fri, 9 Dec 2022 18:10:51 -0800 Subject: [PATCH 025/470] Use consistent order for FA tracks --- app/Http/Controllers/ArtistsController.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/ArtistsController.php b/app/Http/Controllers/ArtistsController.php index fb5407f115e..2a2438ad44b 100644 --- a/app/Http/Controllers/ArtistsController.php +++ b/app/Http/Controllers/ArtistsController.php @@ -40,7 +40,10 @@ public function show($id) ->where('visible', true) ->orderBy('id', 'desc') ->with(['tracks' => function ($query) { - $query->orderBy('display_order', 'ASC'); + $query + ->orderBy('display_order', 'asc') + ->orderBy('exclusive', 'desc') + ->orderBy('id', 'desc'); }]) ->with('tracks.artist') ->get(); @@ -49,6 +52,8 @@ public function show($id) ->tracks() ->whereNull('album_id') ->with('artist') + ->orderBy('display_order', 'asc') + ->orderBy('exclusive', 'desc') ->orderBy('id', 'desc') ->get(); From 4f2f872aa3441c06134856a2cd150541cefb9611 Mon Sep 17 00:00:00 2001 From: RockRoller01 Date: Fri, 19 May 2023 04:11:47 +0200 Subject: [PATCH 026/470] dim forum entry content for old posts --- app/Models/Forum/Topic.php | 5 +++++ config/osu.php | 1 + resources/css/bem/forum-topic-entry.less | 7 +++++++ resources/views/forum/forums/_topic.blade.php | 1 + 4 files changed, 14 insertions(+) diff --git a/app/Models/Forum/Topic.php b/app/Models/Forum/Topic.php index 9aa811f627b..6f3f41a7c2f 100644 --- a/app/Models/Forum/Topic.php +++ b/app/Models/Forum/Topic.php @@ -482,6 +482,11 @@ public function deletedPostsCount() }); } + public function isOld() + { + return $this->topic_last_post_time < Carbon::now()->subMonths(config('osu.forum.old_months')); + } + public function isLocked() { // not checking STATUS_LOCK because there's another diff --git a/config/osu.php b/config/osu.php index 74e0948949b..b752b5c2e08 100644 --- a/config/osu.php +++ b/config/osu.php @@ -113,6 +113,7 @@ 'max_post_length' => get_int(env('FORUM_POST_MAX_LENGTH')) ?? 60000, 'minimum_plays' => get_int(env('FORUM_POST_MINIMUM_PLAYS')) ?? 200, 'necropost_months' => 6, + 'old_months' => 1, 'poll_edit_hours' => get_int(env('FORUM_POLL_EDIT_HOURS')) ?? 1, 'double_post_time' => [ diff --git a/resources/css/bem/forum-topic-entry.less b/resources/css/bem/forum-topic-entry.less index 7c456b104f8..d84fb5729a8 100644 --- a/resources/css/bem/forum-topic-entry.less +++ b/resources/css/bem/forum-topic-entry.less @@ -23,6 +23,10 @@ margin-right: -@forum-item-overflow-desktop; padding-left: @forum-item-overflow-desktop; } + + &--old { + color: darkgray; + } &--deleted { opacity: 0.5; @@ -176,6 +180,9 @@ color: @osu-colour-c1; word-wrap: break-word; + .@{_top}--old & { + color: grey; + } .link-hover({ color: @osu-colour-c1; }); diff --git a/resources/views/forum/forums/_topic.blade.php b/resources/views/forum/forums/_topic.blade.php index 3c9804f9146..ea521ba73c2 100644 --- a/resources/views/forum/forums/_topic.blade.php +++ b/resources/views/forum/forums/_topic.blade.php @@ -9,6 +9,7 @@ class=" forum-topic-entry {{ $topic->trashed() ? 'forum-topic-entry--deleted' : '' }} + {{ $topic->isOld() ? 'forum-topic-entry--old' : '' }} clickable-row js-forum-topic-entry " From 465c6151ee59d359fde5e093de249ba0de1bd440 Mon Sep 17 00:00:00 2001 From: clayton Date: Fri, 19 May 2023 19:32:27 -0700 Subject: [PATCH 027/470] Fix file path for github user component --- resources/js/entrypoints/account-edit.tsx | 2 +- .../account-edit/github-user.tsx => js/github-user/index.tsx} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename resources/{assets/lib/account-edit/github-user.tsx => js/github-user/index.tsx} (96%) diff --git a/resources/js/entrypoints/account-edit.tsx b/resources/js/entrypoints/account-edit.tsx index 9f701094755..ab9ded7e745 100644 --- a/resources/js/entrypoints/account-edit.tsx +++ b/resources/js/entrypoints/account-edit.tsx @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. // See the LICENCE file in the repository root for full licence text. -import GithubUser from 'account-edit/github-user'; +import GithubUser from 'github-user'; import { ClientJson } from 'interfaces/client-json'; import { OwnClientJson } from 'interfaces/own-client-json'; import LegacyApiKey from 'legacy-api-key'; diff --git a/resources/assets/lib/account-edit/github-user.tsx b/resources/js/github-user/index.tsx similarity index 96% rename from resources/assets/lib/account-edit/github-user.tsx rename to resources/js/github-user/index.tsx index 177f19908a4..773cdbc7636 100644 --- a/resources/assets/lib/account-edit/github-user.tsx +++ b/resources/js/github-user/index.tsx @@ -15,7 +15,7 @@ interface Props { } @observer -export default class GithubUsers extends React.Component { +export default class GithubUser extends React.Component { @observable private deleting = false; @observable private user: GithubUserJson | null | undefined; private xhr?: JQuery.jqXHR; From a90f572e658faa8179bf444deb136d5fefe5baf0 Mon Sep 17 00:00:00 2001 From: clayton Date: Fri, 19 May 2023 19:32:27 -0700 Subject: [PATCH 028/470] Prevent re-assigning same GitHub account --- app/Http/Controllers/Account/GithubUsersController.php | 4 +++- resources/lang/en/accounts.php | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Account/GithubUsersController.php b/app/Http/Controllers/Account/GithubUsersController.php index 97d58a99f7b..8612fbc22b8 100644 --- a/app/Http/Controllers/Account/GithubUsersController.php +++ b/app/Http/Controllers/Account/GithubUsersController.php @@ -45,7 +45,9 @@ public function callback() $apiUser = $client->currentUser()->show(); $user = GithubUser::firstWhere('canonical_id', $apiUser['id']); - abort_if($user === null, 422, osu_trans('accounts.github_user.error_no_contribution')); + abort_if($user === null, 422, osu_trans('accounts.github_user.error.no_contribution')); + + abort_if($user->user_id !== null, 422, osu_trans('accounts.github_user.error.already_linked')); $user->update([ 'user_id' => auth()->id(), diff --git a/resources/lang/en/accounts.php b/resources/lang/en/accounts.php index 45680798d66..96c1e280564 100644 --- a/resources/lang/en/accounts.php +++ b/resources/lang/en/accounts.php @@ -54,9 +54,13 @@ 'github_user' => [ 'account' => 'account', - 'error_no_contribution' => 'Cannot link GitHub account without any contribution history in osu! repositories.', 'link' => 'Link GitHub account', 'title' => 'GitHub', + + 'error' => [ + 'already_linked' => 'This GitHub account is already linked to a different user.', + 'no_contribution' => 'Cannot link GitHub account without any contribution history in osu! repositories.', + ], ], 'notifications' => [ From 17fe25a1f72756cde5e49eb3c3c9597940f68244 Mon Sep 17 00:00:00 2001 From: clayton Date: Fri, 19 May 2023 19:32:27 -0700 Subject: [PATCH 029/470] Fix display of legacy changelog entries basically revert some of the code that assumed GitHub username would never be null. --- app/Models/GithubUser.php | 20 ++++++++++++++----- app/Transformers/GithubUserTransformer.php | 2 +- .../js/components/changelog-entry.coffee | 17 ++++++++++------ resources/js/interfaces/github-user-json.ts | 4 ++++ 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/app/Models/GithubUser.php b/app/Models/GithubUser.php index 687c4e2287a..336c4e57d5a 100644 --- a/app/Models/GithubUser.php +++ b/app/Models/GithubUser.php @@ -11,6 +11,9 @@ use Illuminate\Database\Eloquent\Relations\HasMany; /** + * Note: `$canonical_id`, `$id`, and `$username` are null when this model is + * created for use in legacy changelog entries. + * * @property int $canonical_id * @property-read \Illuminate\Database\Eloquent\Collection $changelogEntries * @property \Carbon\Carbon|null $created_at @@ -57,23 +60,30 @@ public function user(): BelongsTo return $this->belongsTo(User::class, 'user_id'); } - public function githubUrl(): string + public function displayUsername(): string { - return "https://github.com/{$this->username}"; + return $this->username ?? $this->osuUsername() ?? '[no name]'; } - public function osuUsername(): ?string + public function githubUrl(): ?string { - return $this->user?->username; + return $this->username !== null + ? "https://github.com/{$this->username}" + : null; } - public function userUrl(): ?string + public function osuUrl(): ?string { return $this->user_id !== null ? route('users.show', $this->user_id) : null; } + public function osuUsername(): ?string + { + return $this->user?->username; + } + public function getAttribute($key) { return match ($key) { diff --git a/app/Transformers/GithubUserTransformer.php b/app/Transformers/GithubUserTransformer.php index b491ed17e05..a1159d4ba09 100644 --- a/app/Transformers/GithubUserTransformer.php +++ b/app/Transformers/GithubUserTransformer.php @@ -14,7 +14,7 @@ class GithubUserTransformer extends TransformerAbstract public function transform(GithubUser $githubUser): array { return [ - 'display_name' => $githubUser->username, // TODO: can be removed + 'display_name' => $githubUser->displayUsername(), 'github_url' => $githubUser->githubUrl(), 'github_username' => $githubUser->username, 'id' => $githubUser->getKey(), diff --git a/resources/js/components/changelog-entry.coffee b/resources/js/components/changelog-entry.coffee index a5ebc75e265..4a2d18c9866 100644 --- a/resources/js/components/changelog-entry.coffee +++ b/resources/js/components/changelog-entry.coffee @@ -42,13 +42,18 @@ export ChangelogEntry = ({entry}) => "#{entry.repository.replace /^.*\//, ''}##{entry.github_pull_request_id}" ')' do => - user = _.escape(entry.github_user.github_username) + user = _.escape(entry.github_user.display_name) + url = entry.github_user.github_url ? entry.github_user.user_url + link = - "#{user}" + if url? + "#{user}" + else + user span className: 'changelog-entry__user' diff --git a/resources/js/interfaces/github-user-json.ts b/resources/js/interfaces/github-user-json.ts index 429de6635f7..87f381926d1 100644 --- a/resources/js/interfaces/github-user-json.ts +++ b/resources/js/interfaces/github-user-json.ts @@ -1,7 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. // See the LICENCE file in the repository root for full licence text. +// Note: `github_url`, `github_username`, and `id` are null when this model is +// created for use in legacy changelog entries. Typings don't reflect this +// because changelogs are only CoffeeScript for now. export default interface GithubUserJson { + display_name: string; github_url: string; github_username: string; id: number; From 708c5d9fdb7d33d6d7cf817216b32f6d084b0857 Mon Sep 17 00:00:00 2001 From: clayton Date: Fri, 19 May 2023 19:32:28 -0700 Subject: [PATCH 030/470] Redate migration --- ...=> 2023_05_20_023000_require_github_users_id_and_username.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename database/migrations/{2022_12_09_120041_require_github_users_id_and_username.php => 2023_05_20_023000_require_github_users_id_and_username.php} (100%) diff --git a/database/migrations/2022_12_09_120041_require_github_users_id_and_username.php b/database/migrations/2023_05_20_023000_require_github_users_id_and_username.php similarity index 100% rename from database/migrations/2022_12_09_120041_require_github_users_id_and_username.php rename to database/migrations/2023_05_20_023000_require_github_users_id_and_username.php From a06ed1ce66fdb10e7c79bf62fa548f1ee4fd0451 Mon Sep 17 00:00:00 2001 From: clayton Date: Fri, 19 May 2023 19:39:57 -0700 Subject: [PATCH 031/470] Fix url method name --- app/Models/GithubUser.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Models/GithubUser.php b/app/Models/GithubUser.php index 336c4e57d5a..d0823c616a9 100644 --- a/app/Models/GithubUser.php +++ b/app/Models/GithubUser.php @@ -72,16 +72,16 @@ public function githubUrl(): ?string : null; } - public function osuUrl(): ?string + public function osuUsername(): ?string { - return $this->user_id !== null - ? route('users.show', $this->user_id) - : null; + return $this->user?->username; } - public function osuUsername(): ?string + public function userUrl(): ?string { - return $this->user?->username; + return $this->user_id !== null + ? route('users.show', $this->user_id) + : null; } public function getAttribute($key) From 531dc6e68bdb9545c6bf1f0b2b17a17a8ead0966 Mon Sep 17 00:00:00 2001 From: RockRoller01 Date: Sat, 27 May 2023 00:07:53 +0200 Subject: [PATCH 032/470] add level 4 red base colour --- resources/css/colors.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/css/colors.less b/resources/css/colors.less index 32515921604..a854d754f29 100644 --- a/resources/css/colors.less +++ b/resources/css/colors.less @@ -145,14 +145,17 @@ --c-saturation-1: 100%; --c-saturation-2: 80%; --c-saturation-3: 60%; + --c-saturation-4: 40%; --c-lightness-1: 70%; --c-lightness-2: 60%; --c-lightness-3: 50%; + --c-lightness-4: 30%; each(@colours, { --colour-@{key}-hue: @value; --hsl-@{key}-1: var(~"--colour-@{key}-hue"), var(--c-saturation-1), var(--c-lightness-1); --hsl-@{key}-2: var(~"--colour-@{key}-hue"), var(--c-saturation-2), var(--c-lightness-2); --hsl-@{key}-3: var(~"--colour-@{key}-hue"), var(--c-saturation-3), var(--c-lightness-3); + --hsl-@{key}-4: var(~"--colour-@{key}-hue"), var(--c-saturation-4), var(--c-lightness-4); }); } // osu!Pink @@ -183,6 +186,7 @@ @osu-colour-red-1: hsl(var(--hsl-red-1)); @osu-colour-red-2: hsl(var(--hsl-red-2)); @osu-colour-red-3: hsl(var(--hsl-red-3)); +@osu-colour-red-4: hsl(var(--hsl-red-4)); // Darkorange @osu-colour-darkorange-1: hsl(var(--hsl-darkorange-1)); @osu-colour-darkorange-2: hsl(var(--hsl-darkorange-2)); From 6f1aa4b60730cff1711d3cd398be5e68c75c98fb Mon Sep 17 00:00:00 2001 From: RockRoller01 Date: Sat, 27 May 2023 00:08:32 +0200 Subject: [PATCH 033/470] use figma design colours --- resources/css/bem/forum-item-stripe.less | 6 +++--- resources/css/bem/forum-topic-entry.less | 15 +++++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/resources/css/bem/forum-item-stripe.less b/resources/css/bem/forum-item-stripe.less index 7a663db369e..9f8ece1a3cd 100644 --- a/resources/css/bem/forum-item-stripe.less +++ b/resources/css/bem/forum-item-stripe.less @@ -17,7 +17,7 @@ content: ''; .full-size(); border-radius: @border-radius-base 0 0 @border-radius-base; - background-color: @forum-item-background-color; + background-color: var(--forum-item-background-color); top: 0; will-change: transform; transition: @forum-item-animate; @@ -36,11 +36,11 @@ } .forum-item:hover &, .forum-topic-entry:hover & { - color: @forum-item-background-color-hover; + color: var(--forum-item-background-color-hover); width: 25px; &::after { - background-color: @forum-item-background-color-hover; + background-color: var(--forum-item-background-color-hover); @media @desktop { transform: translateX(15px); diff --git a/resources/css/bem/forum-topic-entry.less b/resources/css/bem/forum-topic-entry.less index d84fb5729a8..a168d69d09c 100644 --- a/resources/css/bem/forum-topic-entry.less +++ b/resources/css/bem/forum-topic-entry.less @@ -4,10 +4,13 @@ .forum-topic-entry { @_top: forum-topic-entry; + --forum-item-background-color: @osu-colour-b4; + --forum-item-background-color-hover: @osu-colour-b3; + font-size: @font-size--normal; display: flex; margin: 2px -@forum-item-overflow; - background-color: @forum-item-background-color; + background-color: var(--forum-item-background-color); .default-border-radius(); padding: 7px 0 7px @forum-item-overflow; color: @osu-colour-c2; @@ -15,7 +18,7 @@ position: relative; &:hover { - background-color: @forum-item-background-color-hover; + background-color: var(--forum-item-background-color-hover); } @media @desktop { @@ -25,7 +28,10 @@ } &--old { - color: darkgray; + --forum-item-background-color: @osu-colour-red-4; + --forum-item-background-color-hover: @osu-colour-red-3; + + opacity: 0.5; } &--deleted { @@ -180,9 +186,6 @@ color: @osu-colour-c1; word-wrap: break-word; - .@{_top}--old & { - color: grey; - } .link-hover({ color: @osu-colour-c1; }); From 3698002682b0ff3b36bf0327761ff3b5214bfaaa Mon Sep 17 00:00:00 2001 From: RockRoller01 Date: Sat, 27 May 2023 00:12:05 +0200 Subject: [PATCH 034/470] apply color to correct element --- resources/css/bem/forum-topic-entry.less | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/css/bem/forum-topic-entry.less b/resources/css/bem/forum-topic-entry.less index a168d69d09c..7ada6dacbc8 100644 --- a/resources/css/bem/forum-topic-entry.less +++ b/resources/css/bem/forum-topic-entry.less @@ -28,13 +28,13 @@ } &--old { - --forum-item-background-color: @osu-colour-red-4; - --forum-item-background-color-hover: @osu-colour-red-3; - opacity: 0.5; } &--deleted { + --forum-item-background-color: @osu-colour-red-4; + --forum-item-background-color-hover: @osu-colour-red-3; + opacity: 0.5; } From 9c5e16be63992f9f56c89750622950b8a297a095 Mon Sep 17 00:00:00 2001 From: RockRoller01 Date: Sat, 27 May 2023 06:35:08 +0200 Subject: [PATCH 035/470] Add tiered level badge colouring --- public/images/backgrounds/level-hexagon.svg | 1 - resources/css/bem/user-level.less | 66 +++++++++++++++++++-- resources/css/variables.less | 10 ++++ resources/js/components/user-level.tsx | 29 ++++++++- 4 files changed, 99 insertions(+), 7 deletions(-) delete mode 100644 public/images/backgrounds/level-hexagon.svg diff --git a/public/images/backgrounds/level-hexagon.svg b/public/images/backgrounds/level-hexagon.svg deleted file mode 100644 index 96610a3a14d..00000000000 --- a/public/images/backgrounds/level-hexagon.svg +++ /dev/null @@ -1 +0,0 @@ -Artboard 1 \ No newline at end of file diff --git a/resources/css/bem/user-level.less b/resources/css/bem/user-level.less index 02969494af5..1edb5955dcd 100644 --- a/resources/css/bem/user-level.less +++ b/resources/css/bem/user-level.less @@ -6,7 +6,65 @@ width: 50px; height: $width; font-size: @font-size--large; - background-image: url('~@images/backgrounds/level-hexagon.svg'); - background-position: center; - background-repeat: no-repeat; -} + position: relative; + + &__icon { + position: absolute; + width: 100%; + height: 100%; + + .stop-1 { + stop-color: var(--level-tier-stop-1, black); + } + + .stop-2 { + stop-color: var(--level-tier-stop-2, black); + } + } + + &__level { + position: absolute; + } + + &--tier { + &-iron { + --level-tier-stop-1: extract(@level-tier-iron, 1); + --level-tier-stop-2: extract(@level-tier-iron, 2); + } + + &-bronze { + --level-tier-stop-1: extract(@level-tier-bronze, 1); + --level-tier-stop-2: extract(@level-tier-bronze, 2); + } + + &-silver { + --level-tier-stop-1: extract(@level-tier-silver, 1); + --level-tier-stop-2: extract(@level-tier-silver, 2); + } + + &-gold { + --level-tier-stop-1: extract(@level-tier-gold, 1); + --level-tier-stop-2: extract(@level-tier-gold, 2); + } + + &-platinum { + --level-tier-stop-1: extract(@level-tier-platinum, 1); + --level-tier-stop-2: extract(@level-tier-platinum, 2); + } + + &-rhodium { + --level-tier-stop-1: extract(@level-tier-rhodium, 1); + --level-tier-stop-2: extract(@level-tier-rhodium, 2); + } + + &-radiant { + --level-tier-stop-1: extract(@level-tier-radiant, 1); + --level-tier-stop-2: extract(@level-tier-radiant, 2); + } + + &-lustrous { + --level-tier-stop-1: extract(@level-tier-lustrous, 1); + --level-tier-stop-2: extract(@level-tier-lustrous, 2); + } + } +} \ No newline at end of file diff --git a/resources/css/variables.less b/resources/css/variables.less index 31d0f5ce3d1..6d25abc7f42 100644 --- a/resources/css/variables.less +++ b/resources/css/variables.less @@ -317,3 +317,13 @@ @user-card-height: 120px; @user-list-icon-size: 20px; + +// level tier gradients +@level-tier-iron: #BAB3AB, #BAB3AB; +@level-tier-bronze: #B88F7A, #855C47; +@level-tier-silver: #E0E0EB, #A3A3C2; +@level-tier-gold: #F0E4A8, #E0C952; +@level-tier-platinum: #A8F0EF, #52E0DF; +@level-tier-rhodium: #D9F8D3, #A0CF96; +@level-tier-radiant: #97DCFF, #ED82FF; +@level-tier-lustrous: #FFE600, #ED82FF; diff --git a/resources/js/components/user-level.tsx b/resources/js/components/user-level.tsx index 67b733f13cd..3b286d5aed9 100644 --- a/resources/js/components/user-level.tsx +++ b/resources/js/components/user-level.tsx @@ -2,15 +2,40 @@ // See the LICENCE file in the repository root for full licence text. import * as React from 'react'; +import { classWithModifiers } from 'utils/css'; import { trans } from 'utils/lang'; export default function UserLevel({ level }: { level: number }) { + let tier = 'iron'; + + if (level >= 110) { + tier = 'lustrous'; + } else if (level >= 105) { + tier = 'radiant'; + } else if (level >= 100) { + tier = 'rhodium'; + } else if (level >= 80) { + tier = 'platinum'; + } else if (level >= 60) { + tier = 'gold'; + } else if (level >= 40) { + tier = 'silver'; + } else if (level >= 20) { + tier = 'bronze'; + } + + const blockClass = classWithModifiers('user-level', `tier-${tier}`); + + // using tier as a modifier in the linear gradient is required to ensure that + // if multiple level components are on one page they will only use matching gradients + return (
- {level} + {level} +
); } From 45ec957eb8ab314b98ac9d68262984835cb18cb1 Mon Sep 17 00:00:00 2001 From: RockRoller01 Date: Mon, 24 Jul 2023 08:24:09 +0200 Subject: [PATCH 036/470] apply code review --- public/images/backgrounds/level-hexagon.svg | 7 ++++ resources/css/bem/user-level.less | 38 ++++++--------------- resources/js/components/user-level.tsx | 2 +- 3 files changed, 19 insertions(+), 28 deletions(-) create mode 100644 public/images/backgrounds/level-hexagon.svg diff --git a/public/images/backgrounds/level-hexagon.svg b/public/images/backgrounds/level-hexagon.svg new file mode 100644 index 00000000000..1be2e0ebfce --- /dev/null +++ b/public/images/backgrounds/level-hexagon.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/css/bem/user-level.less b/resources/css/bem/user-level.less index 1edb5955dcd..edd7d3ee471 100644 --- a/resources/css/bem/user-level.less +++ b/resources/css/bem/user-level.less @@ -9,17 +9,9 @@ position: relative; &__icon { - position: absolute; - width: 100%; - height: 100%; - - .stop-1 { - stop-color: var(--level-tier-stop-1, black); - } - - .stop-2 { - stop-color: var(--level-tier-stop-2, black); - } + .full-size(); + clip-path: url('~@images/backgrounds/level-hexagon.svg#clip'); + background: linear-gradient(var(--bg)); } &__level { @@ -28,43 +20,35 @@ &--tier { &-iron { - --level-tier-stop-1: extract(@level-tier-iron, 1); - --level-tier-stop-2: extract(@level-tier-iron, 2); + --bg: @level-tier-iron; } &-bronze { - --level-tier-stop-1: extract(@level-tier-bronze, 1); - --level-tier-stop-2: extract(@level-tier-bronze, 2); + --bg: @level-tier-bronze; } &-silver { - --level-tier-stop-1: extract(@level-tier-silver, 1); - --level-tier-stop-2: extract(@level-tier-silver, 2); + --bg: @level-tier-silver; } &-gold { - --level-tier-stop-1: extract(@level-tier-gold, 1); - --level-tier-stop-2: extract(@level-tier-gold, 2); + --bg: @level-tier-gold; } &-platinum { - --level-tier-stop-1: extract(@level-tier-platinum, 1); - --level-tier-stop-2: extract(@level-tier-platinum, 2); + --bg: @level-tier-platinum; } &-rhodium { - --level-tier-stop-1: extract(@level-tier-rhodium, 1); - --level-tier-stop-2: extract(@level-tier-rhodium, 2); + --bg: @level-tier-rhodium; } &-radiant { - --level-tier-stop-1: extract(@level-tier-radiant, 1); - --level-tier-stop-2: extract(@level-tier-radiant, 2); + --bg: @level-tier-radiant; } &-lustrous { - --level-tier-stop-1: extract(@level-tier-lustrous, 1); - --level-tier-stop-2: extract(@level-tier-lustrous, 2); + --bg: @level-tier-lustrous; } } } \ No newline at end of file diff --git a/resources/js/components/user-level.tsx b/resources/js/components/user-level.tsx index 3b286d5aed9..5d8e733e1da 100644 --- a/resources/js/components/user-level.tsx +++ b/resources/js/components/user-level.tsx @@ -35,7 +35,7 @@ export default function UserLevel({ level }: { level: number }) { title={trans('users.show.stats.level', { level })} > {level} - +
); } From a21c8c4a8eca75cfb441d1d10cd0bf4eeaf4194d Mon Sep 17 00:00:00 2001 From: RockRoller01 Date: Mon, 24 Jul 2023 08:26:36 +0200 Subject: [PATCH 037/470] dont skip tpying "--tier" --- resources/css/bem/user-level.less | 62 +++++++++++++++---------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/resources/css/bem/user-level.less b/resources/css/bem/user-level.less index edd7d3ee471..b4f7e490e02 100644 --- a/resources/css/bem/user-level.less +++ b/resources/css/bem/user-level.less @@ -18,37 +18,35 @@ position: absolute; } - &--tier { - &-iron { - --bg: @level-tier-iron; - } - - &-bronze { - --bg: @level-tier-bronze; - } - - &-silver { - --bg: @level-tier-silver; - } - - &-gold { - --bg: @level-tier-gold; - } - - &-platinum { - --bg: @level-tier-platinum; - } - - &-rhodium { - --bg: @level-tier-rhodium; - } - - &-radiant { - --bg: @level-tier-radiant; - } - - &-lustrous { - --bg: @level-tier-lustrous; - } + &--tier-iron { + --bg: @level-tier-iron; + } + + &--tier-bronze { + --bg: @level-tier-bronze; + } + + &--tier-silver { + --bg: @level-tier-silver; + } + + &--tier-gold { + --bg: @level-tier-gold; + } + + &--tier-platinum { + --bg: @level-tier-platinum; + } + + &--tier-rhodium { + --bg: @level-tier-rhodium; + } + + &--tier-radiant { + --bg: @level-tier-radiant; + } + + &--tier-lustrous { + --bg: @level-tier-lustrous; } } \ No newline at end of file From 2412e25e0b2478c59dc654c4f0f00eddd534a37a Mon Sep 17 00:00:00 2001 From: RockRoller01 Date: Mon, 24 Jul 2023 09:07:22 +0200 Subject: [PATCH 038/470] use class with modifiers --- resources/views/forum/forums/_topic.blade.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/resources/views/forum/forums/_topic.blade.php b/resources/views/forum/forums/_topic.blade.php index ea521ba73c2..0123dda4f7c 100644 --- a/resources/views/forum/forums/_topic.blade.php +++ b/resources/views/forum/forums/_topic.blade.php @@ -7,9 +7,7 @@ @endphp
  • Date: Mon, 24 Jul 2023 09:07:29 +0200 Subject: [PATCH 039/470] exclude pinned and announce posts --- app/Models/Forum/Topic.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Models/Forum/Topic.php b/app/Models/Forum/Topic.php index 6f3f41a7c2f..208f057663e 100644 --- a/app/Models/Forum/Topic.php +++ b/app/Models/Forum/Topic.php @@ -484,6 +484,10 @@ public function deletedPostsCount() public function isOld() { + // pinned and announce posts should never be considered old + if($this->topic_type != 0){ + return false; + } return $this->topic_last_post_time < Carbon::now()->subMonths(config('osu.forum.old_months')); } From 1464ef2e8809e273d0a7b149fdc8034949c9e058 Mon Sep 17 00:00:00 2001 From: RockRoller01 Date: Mon, 24 Jul 2023 09:13:01 +0200 Subject: [PATCH 040/470] increase opacity --- resources/css/bem/forum-topic-entry.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/css/bem/forum-topic-entry.less b/resources/css/bem/forum-topic-entry.less index 7ada6dacbc8..09e419093c8 100644 --- a/resources/css/bem/forum-topic-entry.less +++ b/resources/css/bem/forum-topic-entry.less @@ -28,7 +28,7 @@ } &--old { - opacity: 0.5; + opacity: 0.7; } &--deleted { From c6e0abfe80218203eb5b04f82c46841919c7451c Mon Sep 17 00:00:00 2001 From: RockRoller01 Date: Mon, 24 Jul 2023 09:13:37 +0200 Subject: [PATCH 041/470] use color directly --- resources/css/bem/forum-topic-entry.less | 2 +- resources/css/colors.less | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/resources/css/bem/forum-topic-entry.less b/resources/css/bem/forum-topic-entry.less index 09e419093c8..23f32e71559 100644 --- a/resources/css/bem/forum-topic-entry.less +++ b/resources/css/bem/forum-topic-entry.less @@ -32,7 +32,7 @@ } &--deleted { - --forum-item-background-color: @osu-colour-red-4; + --forum-item-background-color: hsl(var(--colour-red-hue), 40%, 30%); --forum-item-background-color-hover: @osu-colour-red-3; opacity: 0.5; diff --git a/resources/css/colors.less b/resources/css/colors.less index a854d754f29..32515921604 100644 --- a/resources/css/colors.less +++ b/resources/css/colors.less @@ -145,17 +145,14 @@ --c-saturation-1: 100%; --c-saturation-2: 80%; --c-saturation-3: 60%; - --c-saturation-4: 40%; --c-lightness-1: 70%; --c-lightness-2: 60%; --c-lightness-3: 50%; - --c-lightness-4: 30%; each(@colours, { --colour-@{key}-hue: @value; --hsl-@{key}-1: var(~"--colour-@{key}-hue"), var(--c-saturation-1), var(--c-lightness-1); --hsl-@{key}-2: var(~"--colour-@{key}-hue"), var(--c-saturation-2), var(--c-lightness-2); --hsl-@{key}-3: var(~"--colour-@{key}-hue"), var(--c-saturation-3), var(--c-lightness-3); - --hsl-@{key}-4: var(~"--colour-@{key}-hue"), var(--c-saturation-4), var(--c-lightness-4); }); } // osu!Pink @@ -186,7 +183,6 @@ @osu-colour-red-1: hsl(var(--hsl-red-1)); @osu-colour-red-2: hsl(var(--hsl-red-2)); @osu-colour-red-3: hsl(var(--hsl-red-3)); -@osu-colour-red-4: hsl(var(--hsl-red-4)); // Darkorange @osu-colour-darkorange-1: hsl(var(--hsl-darkorange-1)); @osu-colour-darkorange-2: hsl(var(--hsl-darkorange-2)); From 50a3df7b81d48ebe041efdc0abc62519eb275acf Mon Sep 17 00:00:00 2001 From: RockRoller01 Date: Mon, 24 Jul 2023 09:17:16 +0200 Subject: [PATCH 042/470] formatting --- app/Models/Forum/Topic.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/Forum/Topic.php b/app/Models/Forum/Topic.php index 208f057663e..4e0489860fe 100644 --- a/app/Models/Forum/Topic.php +++ b/app/Models/Forum/Topic.php @@ -485,7 +485,7 @@ public function deletedPostsCount() public function isOld() { // pinned and announce posts should never be considered old - if($this->topic_type != 0){ + if ($this->topic_type != 0){ return false; } return $this->topic_last_post_time < Carbon::now()->subMonths(config('osu.forum.old_months')); From c58ed0f2a0624394eaecc0270fdf8f15826d8f6c Mon Sep 17 00:00:00 2001 From: RockRoller01 Date: Mon, 24 Jul 2023 09:40:38 +0200 Subject: [PATCH 043/470] formatting --- app/Models/Forum/Topic.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/Forum/Topic.php b/app/Models/Forum/Topic.php index 4e0489860fe..e71b60f15c6 100644 --- a/app/Models/Forum/Topic.php +++ b/app/Models/Forum/Topic.php @@ -485,7 +485,7 @@ public function deletedPostsCount() public function isOld() { // pinned and announce posts should never be considered old - if ($this->topic_type != 0){ + if ($this->topic_type !== 0){ return false; } return $this->topic_last_post_time < Carbon::now()->subMonths(config('osu.forum.old_months')); From 1ae633041c35c82ae1478f46852f14e705ed5348 Mon Sep 17 00:00:00 2001 From: RockRoller01 Date: Mon, 24 Jul 2023 09:49:32 +0200 Subject: [PATCH 044/470] formatting --- app/Models/Forum/Topic.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/Forum/Topic.php b/app/Models/Forum/Topic.php index e71b60f15c6..ef506e0c5da 100644 --- a/app/Models/Forum/Topic.php +++ b/app/Models/Forum/Topic.php @@ -485,7 +485,7 @@ public function deletedPostsCount() public function isOld() { // pinned and announce posts should never be considered old - if ($this->topic_type !== 0){ + if ($this->topic_type !== 0) { return false; } return $this->topic_last_post_time < Carbon::now()->subMonths(config('osu.forum.old_months')); From f7a561a39128fb961e2095afe7ff600765e65a91 Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 26 Jul 2023 02:03:56 +0900 Subject: [PATCH 045/470] Store multiplayer score in solo score table --- .../Rooms/Playlist/ScoresController.php | 117 ++++++------ .../Controllers/Solo/ScoresController.php | 22 +-- app/Libraries/MorphMap.php | 2 + app/Models/Contest.php | 4 +- app/Models/Multiplayer/PlaylistItem.php | 10 +- .../Multiplayer/PlaylistItemUserHighScore.php | 30 +-- app/Models/Multiplayer/Room.php | 43 +++-- app/Models/Multiplayer/Score.php | 157 ---------------- app/Models/Multiplayer/ScoreLink.php | 174 ++++++++++++++++++ app/Models/Multiplayer/UserScoreAggregate.php | 46 ++--- app/Models/Solo/Score.php | 26 ++- app/Models/Solo/ScoreData.php | 8 +- app/Models/Traits/SoloScoreInterface.php | 16 ++ .../Multiplayer/ScoreTransformer.php | 94 ---------- .../CurrentUserAttributesTransformer.php | 3 +- app/Transformers/ScoreTransformer.php | 84 ++++++++- .../factories/Multiplayer/ScoreFactory.php | 53 ------ .../Multiplayer/ScoreLinkFactory.php | 52 ++++++ ..._103744_create_multiplayer_score_links.php | 45 +++++ ...ore_link_id_to_multiplayer_scores_high.php | 35 ++++ ...core_link_id_to_multiplayer_rooms_high.php | 33 ++++ .../ModelSeeders/MultiplayerSeeder.php | 20 +- .../Chat/ChannelsControllerTest.php | 12 +- .../Rooms/Playlist/ScoresControllerTest.php | 22 +-- tests/Models/ContestTest.php | 29 ++- tests/Models/Multiplayer/RoomTest.php | 32 ++-- .../Multiplayer/UserScoreAggregateTest.php | 153 +++++++-------- 27 files changed, 737 insertions(+), 585 deletions(-) delete mode 100644 app/Models/Multiplayer/Score.php create mode 100644 app/Models/Multiplayer/ScoreLink.php create mode 100644 app/Models/Traits/SoloScoreInterface.php delete mode 100644 app/Transformers/Multiplayer/ScoreTransformer.php delete mode 100644 database/factories/Multiplayer/ScoreFactory.php create mode 100644 database/factories/Multiplayer/ScoreLinkFactory.php create mode 100644 database/migrations/2023_07_26_103744_create_multiplayer_score_links.php create mode 100644 database/migrations/2023_08_01_064505_add_score_link_id_to_multiplayer_scores_high.php create mode 100644 database/migrations/2023_08_01_101614_add_last_score_link_id_to_multiplayer_rooms_high.php diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php index f2bec1bfd6a..3e2ed735a5c 100644 --- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php +++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php @@ -5,14 +5,13 @@ namespace App\Http\Controllers\Multiplayer\Rooms\Playlist; -use App\Exceptions\InvariantException; use App\Http\Controllers\Controller as BaseController; use App\Libraries\ClientCheck; use App\Models\Multiplayer\PlaylistItem; use App\Models\Multiplayer\PlaylistItemUserHighScore; use App\Models\Multiplayer\Room; -use App\Transformers\Multiplayer\ScoreTransformer; -use Carbon\Carbon; +use App\Models\Solo\Score; +use App\Transformers\ScoreTransformer; /** * @group Multiplayer @@ -53,14 +52,15 @@ public function index($roomId, $playlistId) [$highScores, $hasMore] = $playlist ->highScores() ->cursorSort($cursorHelper, cursor_from_params($params)) - ->with(ScoreTransformer::BASE_PRELOAD) + ->with(ScoreTransformer::MULTIPLAYER_BASE_PRELOAD) ->limit($limit) ->getWithHasMore(); + $transformer = ScoreTransformer::newSolo(); $scoresJson = json_collection( $highScores->pluck('score'), - 'Multiplayer\Score', - ScoreTransformer::BASE_INCLUDES + $transformer, + ScoreTransformer::MULTIPLAYER_BASE_INCLUDES ); $total = $playlist->highScores()->count(); @@ -70,7 +70,7 @@ public function index($roomId, $playlistId) $userHighScore = $playlist->highScores()->where('user_id', $user->getKey())->first(); if ($userHighScore !== null) { - $userScoreJson = json_item($userHighScore->score, 'Multiplayer\Score', ScoreTransformer::BASE_INCLUDES); + $userScoreJson = json_item($userHighScore->score, $transformer, ScoreTransformer::BASE_INCLUDES); } } @@ -102,12 +102,16 @@ public function show($roomId, $playlistId, $id) { $room = Room::find($roomId) ?? abort(404, 'Invalid room id'); $playlistItem = $room->playlist()->find($playlistId) ?? abort(404, 'Invalid playlist id'); - $score = $playlistItem->scores()->findOrFail($id); + $scoreLinks = $playlistItem->scoreLinks()->findOrFail($id); return json_item( - $score, - 'Multiplayer\Score', - array_merge(['position', 'scores_around'], ScoreTransformer::BASE_INCLUDES) + $scoreLinks, + ScoreTransformer::newSolo(), + [ + ...ScoreTransformer::MULTIPLAYER_BASE_INCLUDES, + 'position', + 'scores_around', + ], ); } @@ -134,8 +138,12 @@ public function showUser($roomId, $playlistId, $userId) return json_item( $score, - 'Multiplayer\Score', - array_merge(['position', 'scores_around'], ScoreTransformer::BASE_INCLUDES) + ScoreTransformer::newSolo(), + [ + ...ScoreTransformer::MULTIPLAYER_BASE_INCLUDES, + 'position', + 'scores_around', + ], ); } @@ -149,14 +157,12 @@ public function store($roomId, $playlistId) $user = auth()->user(); $params = request()->all(); - ClientCheck::findBuild($user, $params); + $buildId = ClientCheck::findBuild($user, $params)?->getKey() + ?? config('osu.client.default_build_id'); - $score = $room->startPlay($user, $playlistItem); + $score = $room->startPlay($user, $playlistItem, $buildId); - return json_item( - $score, - 'Multiplayer\Score' - ); + return json_item($score, ScoreTransformer::newSolo()); } /** @@ -164,49 +170,40 @@ public function store($roomId, $playlistId) */ public function update($roomId, $playlistId, $scoreId) { - $room = Room::findOrFail($roomId); - - $playlistItem = $room->playlist() - ->where('id', $playlistId) - ->firstOrFail(); - - $roomScore = $playlistItem->scores() - ->where('user_id', auth()->user()->getKey()) - ->where('id', $scoreId) - ->firstOrFail(); - - try { - $score = $room->completePlay( - $roomScore, - $this->extractScoreParams(request()->all(), $playlistItem) - ); - - return json_item( - $score, - 'Multiplayer\Score', - array_merge(['position', 'scores_around'], ScoreTransformer::BASE_INCLUDES) - ); - } catch (InvariantException $e) { - return error_popup($e->getMessage(), $e->getStatusCode()); + $scoreLink = \DB::transaction(function () use ($roomId, $playlistId, $scoreId) { + $room = Room::findOrFail($roomId); + + $scoreLink = $room + ->scoreLinks() + ->where([ + 'user_id' => \Auth::id(), + 'playlist_item_id' => $playlistId, + ])->with('playlistItem') + ->lockForUpdate() + ->findOrFail($scoreId); + + $params = Score::extractParams(\Request::all(), $scoreLink); + + $room->completePlay($scoreLink, $params); + + return $scoreLink; + }); + + $score = $scoreLink->score; + $transformer = ScoreTransformer::newSolo(); + if ($score->wasRecentlyCreated) { + $scoreJson = json_item($score, $transformer); + $score::queueForProcessing($scoreJson); } - } - private function extractScoreParams(array $params, PlaylistItem $playlistItem) - { - $mods = app('mods')->parseInputArray( - $playlistItem->ruleset_id, - $params['mods'] ?? [], + return json_item( + $scoreLink, + $transformer, + [ + ...ScoreTransformer::MULTIPLAYER_BASE_INCLUDES, + 'position', + 'scores_around', + ], ); - - return [ - 'rank' => $params['rank'] ?? null, - 'total_score' => get_int($params['total_score'] ?? null), - 'accuracy' => get_float($params['accuracy'] ?? null), - 'max_combo' => get_int($params['max_combo'] ?? null), - 'ended_at' => Carbon::now(), - 'passed' => get_bool($params['passed'] ?? null), - 'mods' => $mods, - 'statistics' => $params['statistics'] ?? null, - ]; } } diff --git a/app/Http/Controllers/Solo/ScoresController.php b/app/Http/Controllers/Solo/ScoresController.php index 3bef06a3722..8de9548e9f0 100644 --- a/app/Http/Controllers/Solo/ScoresController.php +++ b/app/Http/Controllers/Solo/ScoresController.php @@ -29,27 +29,7 @@ public function store($beatmapId, $tokenId) // return existing score otherwise (assuming duplicated submission) if ($scoreToken->score_id === null) { - $params = get_params(request()->all(), null, [ - 'accuracy:float', - 'max_combo:int', - 'maximum_statistics:array', - 'mods:array', - 'passed:bool', - 'rank:string', - 'statistics:array', - 'total_score:int', - ]); - - $params = array_merge($params, [ - 'beatmap_id' => $scoreToken->beatmap_id, - 'build_id' => $scoreToken->build_id, - 'ended_at' => json_time(now()), - 'mods' => app('mods')->parseInputArray($scoreToken->ruleset_id, $params['mods'] ?? []), - 'ruleset_id' => $scoreToken->ruleset_id, - 'started_at' => $scoreToken->created_at_json, - 'user_id' => $scoreToken->user_id, - ]); - + $params = Score::extractParams(\Request::all(), $scoreToken); $score = Score::createFromJsonOrExplode($params); $score->createLegacyEntryOrExplode(); $scoreToken->fill(['score_id' => $score->getKey()])->saveOrExplode(); diff --git a/app/Libraries/MorphMap.php b/app/Libraries/MorphMap.php index 91f41953dae..d79ad41cb88 100644 --- a/app/Libraries/MorphMap.php +++ b/app/Libraries/MorphMap.php @@ -14,6 +14,7 @@ use App\Models\Comment; use App\Models\Forum; use App\Models\LegacyMatch; +use App\Models\Multiplayer\ScoreLink as MultiplayerScoreLink; use App\Models\NewsPost; use App\Models\Score; use App\Models\Solo; @@ -32,6 +33,7 @@ class MorphMap Forum\Topic::class => 'forum_topic', LegacyMatch\Score::class => 'legacy_match_score', Message::class => 'message', + MultiplayerScoreLink::class => 'multiplayer_score_link', NewsPost::class => 'news_post', Score\Best\Fruits::class => 'score_best_fruits', Score\Best\Mania::class => 'score_best_mania', diff --git a/app/Models/Contest.php b/app/Models/Contest.php index aa6c8797c2a..951177f6a04 100644 --- a/app/Models/Contest.php +++ b/app/Models/Contest.php @@ -85,13 +85,13 @@ public function assertVoteRequirement(?User $user): void $mustPass = $requirement['must_pass'] ?? true; $beatmapIdsQuery = Multiplayer\PlaylistItem::whereIn('room_id', $roomIds)->select('beatmap_id'); $requiredBeatmapsetCount = Beatmap::whereIn('beatmap_id', $beatmapIdsQuery)->distinct('beatmapset_id')->count(); - $playedBeatmapIdsQuery = Multiplayer\Score + $playedBeatmapIdsQuery = Multiplayer\ScoreLink ::whereIn('room_id', $roomIds) ->where(['user_id' => $user->getKey()]) ->completed() ->select('beatmap_id'); if ($mustPass) { - $playedBeatmapIdsQuery->where('passed', true); + $playedBeatmapIdsQuery->whereHas('playlistItemUserHighScore'); } $playedBeatmapsetCount = Beatmap::whereIn('beatmap_id', $playedBeatmapIdsQuery)->distinct('beatmapset_id')->count(); diff --git a/app/Models/Multiplayer/PlaylistItem.php b/app/Models/Multiplayer/PlaylistItem.php index cdeceffcdab..12a60be8cad 100644 --- a/app/Models/Multiplayer/PlaylistItem.php +++ b/app/Models/Multiplayer/PlaylistItem.php @@ -23,7 +23,7 @@ * @property Room $room * @property int $room_id * @property int|null $ruleset_id - * @property \Illuminate\Database\Eloquent\Collection $scores Score + * @property \Illuminate\Database\Eloquent\Collection $scoreLinks ScoreLink * @property \Carbon\Carbon|null $updated_at * @property bool expired * @property \Carbon\Carbon|null $played_at @@ -96,17 +96,17 @@ public function highScores() return $this->hasMany(PlaylistItemUserHighScore::class); } - public function scores() + public function scoreLinks() { - return $this->hasMany(Score::class); + return $this->hasMany(ScoreLink::class); } public function topScores() { return $this->highScores() - ->with('score') + ->with('scoreLink.score') ->orderBy('total_score', 'desc') - ->orderBy('score_id', 'asc'); + ->orderBy('score_link_id', 'asc'); } private function assertValidMaxAttempts() diff --git a/app/Models/Multiplayer/PlaylistItemUserHighScore.php b/app/Models/Multiplayer/PlaylistItemUserHighScore.php index 1368b455dd0..1330ab58dbc 100644 --- a/app/Models/Multiplayer/PlaylistItemUserHighScore.php +++ b/app/Models/Multiplayer/PlaylistItemUserHighScore.php @@ -17,9 +17,8 @@ * @property int $id * @property int $playlist_item_id * @property float|null $pp - * @property int $score_id - * @property Score $score - * @property int $total_score + * @property int $score_link_id + * @property ScoreLink $scoreLink * @property \Carbon\Carbon $updated_at * @property int $user_id */ @@ -30,11 +29,11 @@ class PlaylistItemUserHighScore extends Model const SORTS = [ 'score_desc' => [ ['column' => 'total_score', 'order' => 'DESC'], - ['column' => 'score_id', 'order' => 'ASC'], + ['column' => 'score_link_id', 'order' => 'ASC'], ], 'score_asc' => [ ['column' => 'total_score', 'order' => 'ASC'], - ['column' => 'score_id', 'order' => 'DESC'], + ['column' => 'score_link_id', 'order' => 'DESC'], ], ]; @@ -42,11 +41,11 @@ class PlaylistItemUserHighScore extends Model protected $table = 'multiplayer_scores_high'; - public static function lookupOrDefault(Score $score): static + public static function lookupOrDefault(ScoreLink $scoreLink): static { return static::firstOrNew([ - 'playlist_item_id' => $score->playlist_item_id, - 'user_id' => $score->user_id, + 'playlist_item_id' => $scoreLink->playlist_item_id, + 'user_id' => $scoreLink->user_id, ], [ 'accuracy' => 0, 'pp' => 0, @@ -54,18 +53,21 @@ public static function lookupOrDefault(Score $score): static ]); } - public function score() + public function scoreLink() { - return $this->belongsTo(Score::class); + return $this->belongsTo(ScoreLink::class); } - public function updateWithScore(Score $score): void + public function updateWithScoreLink(ScoreLink $scoreLink): void { + $score = $scoreLink->score; + $this->fill([ - 'accuracy' => $score->accuracy, + 'accuracy' => $score->data->accuracy, 'pp' => $score->pp, - 'score_id' => $score->getKey(), - 'total_score' => $score->total_score, + 'score_id' => 0, // TODO: remove after migrated + 'score_link_id' => $scoreLink->getKey(), + 'total_score' => $score->data->totalScore, ])->save(); } } diff --git a/app/Models/Multiplayer/Room.php b/app/Models/Multiplayer/Room.php index c310f408a5f..13bc6d99ad0 100644 --- a/app/Models/Multiplayer/Room.php +++ b/app/Models/Multiplayer/Room.php @@ -35,7 +35,7 @@ * @property string $name * @property int $participant_count * @property \Illuminate\Database\Eloquent\Collection $playlist PlaylistItem - * @property \Illuminate\Database\Eloquent\Collection $scores Score + * @property \Illuminate\Database\Eloquent\Collection $scoreLinks ScoreLink * @property-read Collection<\App\Models\Season> $seasons * @property \Carbon\Carbon $starts_at * @property \Carbon\Carbon|null $updated_at @@ -227,9 +227,9 @@ public function playlist() return $this->hasMany(PlaylistItem::class); } - public function scores() + public function scoreLinks() { - return $this->hasMany(Score::class); + return $this->hasMany(ScoreLink::class); } public function seasons() @@ -258,11 +258,9 @@ public function scopeEnded($query) public function scopeHasParticipated($query, User $user) { - return $query->whereIn( - 'id', - // SoftDelete scope is ignored, fixed in 5.8: - // https://github.com/laravel/framework/pull/26198 - Score::withoutTrashed()->where('user_id', $user->getKey())->select('room_id') + return $query->whereHas( + 'scoreLinks', + fn ($q) => $q->where('user_id', $user->getKey()), ); } @@ -386,23 +384,23 @@ public function getRecentParticipantIdsAttribute() public function calculateMissingTopScores() { // just run through all the users, UserScoreAggregate::new will calculate and persist if necessary. - $users = User::whereIn('user_id', Score::where('room_id', $this->getKey())->select('user_id')); + $users = User::whereIn('user_id', ScoreLink::where('room_id', $this->getKey())->select('user_id')); $users->each(function ($user) { UserScoreAggregate::new($user, $this); }); } - public function completePlay(Score $score, array $params) + public function completePlay(ScoreLink $scoreLink, array $params) { - priv_check_user($score->user, 'MultiplayerScoreSubmit')->ensureCan(); + priv_check_user($scoreLink->user, 'MultiplayerScoreSubmit')->ensureCan(); $this->assertValidCompletePlay(); - return $score->getConnection()->transaction(function () use ($params, $score) { - $score->complete($params); - UserScoreAggregate::new($score->user, $this)->addScore($score); + return $scoreLink->getConnection()->transaction(function () use ($params, $scoreLink) { + $scoreLink->complete($params); + UserScoreAggregate::new($scoreLink->user, $this)->addScoreLink($scoreLink); - return $score; + return $scoreLink; }); } @@ -589,13 +587,13 @@ public function startGame(User $host, array $rawParams) return $this->fresh(); } - public function startPlay(User $user, PlaylistItem $playlistItem) + public function startPlay(User $user, PlaylistItem $playlistItem, int $buildId) { priv_check_user($user, 'MultiplayerScoreSubmit')->ensureCan(); $this->assertValidStartPlay($user, $playlistItem); - return $this->getConnection()->transaction(function () use ($user, $playlistItem) { + return $this->getConnection()->transaction(function () use ($buildId, $user, $playlistItem) { $agg = UserScoreAggregate::new($user, $this); if ($agg->wasRecentlyCreated) { $this->incrementInstance('participant_count'); @@ -603,11 +601,12 @@ public function startPlay(User $user, PlaylistItem $playlistItem) $agg->updateUserAttempts(); - return Score::start([ - 'user_id' => $user->getKey(), - 'room_id' => $this->getKey(), - 'playlist_item_id' => $playlistItem->getKey(), + return ScoreLink::create([ 'beatmap_id' => $playlistItem->beatmap_id, + 'build_id' => $buildId, + 'playlist_item_id' => $playlistItem->getKey(), + 'room_id' => $this->getKey(), + 'user_id' => $user->getKey(), ]); }); } @@ -683,7 +682,7 @@ private function assertValidStartPlay(User $user, PlaylistItem $playlistItem) } if ($playlistItem->max_attempts !== null) { - $playlistAttempts = $playlistItem->scores()->where('user_id', $user->getKey())->count(); + $playlistAttempts = $playlistItem->scoreLinks()->where('user_id', $user->getKey())->count(); if ($playlistAttempts >= $playlistItem->max_attempts) { throw new InvariantException('You have reached the maximum number of tries allowed.'); } diff --git a/app/Models/Multiplayer/Score.php b/app/Models/Multiplayer/Score.php deleted file mode 100644 index c321a966689..00000000000 --- a/app/Models/Multiplayer/Score.php +++ /dev/null @@ -1,157 +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\Multiplayer; - -use App\Exceptions\GameCompletedException; -use App\Exceptions\InvariantException; -use App\Models\Model; -use App\Models\Solo\ScoreData; -use App\Models\User; -use Carbon\Carbon; -use Illuminate\Database\Eloquent\SoftDeletes; - -/** - * @property float|null $accuracy - * @property int $beatmap_id - * @property \Carbon\Carbon|null $created_at - * @property \Carbon\Carbon|null $deleted_at - * @property \Carbon\Carbon|null $ended_at - * @property int $id - * @property int|null $max_combo - * @property array|null $mods - * @property bool|null $passed - * @property PlaylistItem $playlistItem - * @property int $playlist_item_id - * @property float|null $pp - * @property mixed|null $rank - * @property Room $room - * @property int $room_id - * @property \Carbon\Carbon $started_at - * @property \stdClass|null $statistics - * @property int|null $total_score - * @property \Carbon\Carbon|null $updated_at - * @property User $user - * @property int $user_id - */ -class Score extends Model -{ - use SoftDeletes; - - protected $casts = [ - 'ended_at' => 'datetime', - 'mods' => 'object', - 'passed' => 'boolean', - 'started_at' => 'datetime', - 'statistics' => 'object', - ]; - protected $table = 'multiplayer_scores'; - - public static function start(array $params) - { - // TODO: move existence checks here? - $score = new static($params); - $score->started_at = Carbon::now(); - - $score->save(); - - return $score; - } - - public function playlistItem() - { - return $this->belongsTo(PlaylistItem::class, 'playlist_item_id'); - } - - public function room() - { - return $this->belongsTo(Room::class, 'room_id'); - } - - public function user() - { - return $this->belongsTo(User::class, 'user_id'); - } - - public function getDataAttribute() - { - // FIXME: convert this class to the new score table layout - $params = $this->getAttributes(); - $params['mods'] = json_decode($params['mods'], true); - $params['passed'] = get_bool($params['passed']); - $params['ruleset_id'] = $this->playlistItem->ruleset_id; - $params['statistics'] = json_decode($params['statistics'], true); - $params['ruleset_id'] = $this->playlistItem->ruleset_id; - - return new ScoreData($params); - } - - public function scopeCompleted($query) - { - return $query->whereNotNull('ended_at'); - } - - public function scopeForPlaylistItem($query, $playlistItemId) - { - return $query->where('playlist_item_id', $playlistItemId); - } - - public function isCompleted() - { - return present($this->ended_at); - } - - public function complete(array $params) - { - if ($this->isCompleted()) { - throw new GameCompletedException('cannot modify score after submission'); - } - - $this->fill($params); - - if (!empty($this->playlistItem->required_mods)) { - $missingMods = array_diff( - array_column($this->playlistItem->required_mods, 'acronym'), - array_column($this->mods, 'acronym') - ); - - if (!empty($missingMods)) { - throw new InvariantException('This play does not include the mods required.'); - } - } - - if (!empty($this->playlistItem->allowed_mods)) { - $missingMods = array_diff( - array_column($this->mods, 'acronym'), - array_column($this->playlistItem->required_mods, 'acronym'), - array_column($this->playlistItem->allowed_mods, 'acronym') - ); - - if (!empty($missingMods)) { - throw new InvariantException('This play includes mods that are not allowed.'); - } - } - - $this->data->assertCompleted(); - - $this->save(); - } - - public function userRank() - { - if ($this->total_score === null || $this->getKey() === null) { - return; - } - - $query = PlaylistItemUserHighScore - ::where('playlist_item_id', $this->playlist_item_id) - ->cursorSort('score_asc', [ - 'total_score' => $this->total_score, - 'score_id' => $this->getKey(), - ]); - - return 1 + $query->count(); - } -} diff --git a/app/Models/Multiplayer/ScoreLink.php b/app/Models/Multiplayer/ScoreLink.php new file mode 100644 index 00000000000..2a686535683 --- /dev/null +++ b/app/Models/Multiplayer/ScoreLink.php @@ -0,0 +1,174 @@ +. 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\Multiplayer; + +use App\Exceptions\GameCompletedException; +use App\Exceptions\InvariantException; +use App\Models\Model; +use App\Models\Solo\Score; +use App\Models\Traits\SoloScoreInterface; +use App\Models\User; +use Carbon\Carbon; +use Illuminate\Database\Eloquent\Relations\BelongsTo; + +/** + * @property int $build_id + * @property \Carbon\Carbon|null $created_at + * @property int $id + * @property PlaylistItem $playlistItem + * @property int $playlist_item_id + * @property Room $room + * @property int $room_id + * @property Score $score + * @property int|null $score_id + * @property \Carbon\Carbon|null $updated_at + * @property User $user + * @property int $user_id + */ +class ScoreLink extends Model implements SoloScoreInterface +{ + protected $table = 'multiplayer_score_links'; + + private Score $defaultScore; + + public function playlistItem() + { + return $this->belongsTo(PlaylistItem::class, 'playlist_item_id'); + } + + public function playlistItemUserHighScore() + { + return $this->hasOne(PlaylistItemUserHighScore::class); + } + + public function room() + { + return $this->belongsTo(Room::class, 'room_id'); + } + + public function score(): BelongsTo + { + return $this->belongsTo(Score::class, 'score_id'); + } + + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function getAttribute($key) + { + return match ($key) { + 'build_id', + 'id', + 'playlist_item_id', + 'room_id', + 'score_id', + 'user_id' => $this->getRawAttribute($key), + + 'data' => $this->getScoreOrDefault()->data, + 'has_replay' => $this->getScoreOrDefault()->has_replay ?? false, + 'pp' => $this->getScoreOrDefault()->pp ?? 0.0, + + 'beatmap_id' => $this->playlistItem?->beatmap_id, + 'ruleset_id' => $this->playlistItem?->ruleset_id, + + 'created_at', + 'updated_at' => $this->getTimeFast($key), + + 'created_at_json', + 'updated_at_json' => $this->getJsonTimeFast($key), + + 'playlistItem', + 'playlistItemUserHighScore', + 'room', + 'score', + 'user' => $this->getRelationValue($key), + }; + } + + public function scopeCompleted($query) + { + return $query->whereNotNull('score_id'); + } + + public function scopeForPlaylistItem($query, $playlistItemId) + { + return $query->where('playlist_item_id', $playlistItemId); + } + + public function isCompleted(): bool + { + return $this->score_id !== null; + } + + public function complete(array $params) + { + $this->getConnection()->transaction(function () use ($params) { + if ($this->isCompleted()) { + throw new GameCompletedException('cannot modify score after submission'); + } + + $score = Score::createFromJsonOrExplode($params); + $mods = $score->data->mods; + + if (!empty($this->playlistItem->required_mods)) { + $missingMods = array_diff( + array_column($this->playlistItem->required_mods, 'acronym'), + array_column($mods, 'acronym') + ); + + if (!empty($missingMods)) { + throw new InvariantException('This play does not include the mods required.'); + } + } + + if (!empty($this->playlistItem->allowed_mods)) { + $missingMods = array_diff( + array_column($mods, 'acronym'), + array_column($this->playlistItem->required_mods, 'acronym'), + array_column($this->playlistItem->allowed_mods, 'acronym') + ); + + if (!empty($missingMods)) { + throw new InvariantException('This play includes mods that are not allowed.'); + } + } + + $this->score()->associate($score); + $this->save(); + }); + } + + public function position(): ?int + { + $score = $this->score; + + if ($score === null) { + return null; + } + + $query = PlaylistItemUserHighScore + ::where('playlist_item_id', $this->playlist_item_id) + ->cursorSort('score_asc', [ + 'total_score' => $score->data->totalScore, + 'score_link_id' => $this->getKey(), + ]); + + return 1 + $query->count(); + } + + private function getScoreOrDefault(): Score + { + return $this->score ?? ($this->defaultScore ??= new Score([ + 'beatmap_id' => $this->beatmap_id, + 'ruleset_id' => $this->ruleset_id, + 'user_id' => $this->user_id, + 'created_at' => Carbon::now(), + 'data' => [], + ])); + } +} diff --git a/app/Models/Multiplayer/UserScoreAggregate.php b/app/Models/Multiplayer/UserScoreAggregate.php index f0d68ecd52a..472efb6a5a7 100644 --- a/app/Models/Multiplayer/UserScoreAggregate.php +++ b/app/Models/Multiplayer/UserScoreAggregate.php @@ -8,6 +8,7 @@ use App\Models\Model; use App\Models\Traits\WithDbCursorHelper; use App\Models\User; +use Illuminate\Database\Eloquent\Builder; /** * Aggregate root for user multiplayer high scores. @@ -32,7 +33,7 @@ class UserScoreAggregate extends Model const SORTS = [ 'score_asc' => [ ['column' => 'total_score', 'order' => 'ASC'], - ['column' => 'last_score_id', 'order' => 'DESC'], + ['column' => 'last_score_link_id', 'order' => 'DESC'], ], ]; @@ -69,18 +70,19 @@ public static function new(User $user, Room $room): self return $obj; } - public function addScore(Score $score) + public function addScoreLink(ScoreLink $scoreLink) { - return $this->getConnection()->transaction(function () use ($score) { - if (!$score->isCompleted()) { + return $this->getConnection()->transaction(function () use ($scoreLink) { + $score = $scoreLink->score; + if ($score === null) { return false; } - $highestScore = PlaylistItemUserHighScore::lookupOrDefault($score); + $highestScore = PlaylistItemUserHighScore::lookupOrDefault($scoreLink); - if ($score->passed && $score->total_score > $highestScore->total_score) { - $this->updateUserTotal($score, $highestScore); - $highestScore->updateWithScore($score); + if ($score->data->passed && $score->data->totalScore > $highestScore->total_score) { + $this->updateUserTotal($scoreLink, $highestScore); + $highestScore->updateWithScoreLink($scoreLink); } return true; @@ -97,23 +99,21 @@ public function averagePp() return $this->completed > 0 ? $this->pp / $this->completed : 0; } - public function getScores() + public function scoreLinks(): Builder { - return Score + return ScoreLink ::where('room_id', $this->room_id) - ->where('user_id', $this->user_id) - ->get(); + ->where('user_id', $this->user_id); } public function recalculate() { $this->getConnection()->transaction(function () { $this->removeRunningTotals(); - $this->getScores()->each(function ($score) { + foreach ($this->scoreLinks()->with('score.performance')->get() as $scoreLink) { $this->attempts++; - $this->addScore($score); - }); - + $this->addScoreLink($scoreLink); + } $this->save(); }); } @@ -144,7 +144,7 @@ public function scopeForRanking($query) $userQuery->default(); }) ->orderBy('total_score', 'DESC') - ->orderBy('last_score_id', 'ASC'); + ->orderBy('last_score_link_id', 'ASC'); } public function updateUserAttempts() @@ -159,7 +159,7 @@ public function user() public function userRank() { - if ($this->total_score === null || $this->last_score_id === null) { + if ($this->total_score === null || $this->last_score_link_id === null) { return; } @@ -169,7 +169,7 @@ public function userRank() return 1 + $query->count(); } - private function updateUserTotal(Score $current, PlaylistItemUserHighScore $prev) + private function updateUserTotal(ScoreLink $currentScoreLink, PlaylistItemUserHighScore $prev) { if ($prev->exists) { $this->total_score -= $prev->total_score; @@ -178,11 +178,13 @@ private function updateUserTotal(Score $current, PlaylistItemUserHighScore $prev $this->completed--; } - $this->total_score += $current->total_score; - $this->accuracy += $current->accuracy; + $current = $currentScoreLink->score; + + $this->total_score += $current->data->totalScore; + $this->accuracy += $current->data->accuracy; $this->pp += $current->pp; $this->completed++; - $this->last_score_id = $current->getKey(); + $this->last_score_link_id = $currentScoreLink->getKey(); $this->save(); } diff --git a/app/Models/Solo/Score.php b/app/Models/Solo/Score.php index 6bca92c036d..5765d243f4f 100644 --- a/app/Models/Solo/Score.php +++ b/app/Models/Solo/Score.php @@ -11,9 +11,11 @@ use App\Libraries\Search\ScoreSearchParams; use App\Models\Beatmap; use App\Models\Model; +use App\Models\Multiplayer\ScoreLink as MultiplayerScoreLink; use App\Models\Score as LegacyScore; use App\Models\Traits; use App\Models\User; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use LaravelRedis; use Storage; @@ -30,7 +32,7 @@ * @property User $user * @property int $user_id */ -class Score extends Model implements Traits\ReportableInterface +class Score extends Model implements Traits\ReportableInterface, Traits\SoloScoreInterface { use Traits\Reportable, Traits\WithWeightedPp; @@ -66,6 +68,28 @@ public static function createFromJsonOrExplode(array $params) return $score; } + public static function extractParams(array $params, 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, + ]; + } + /** * Queue the item for score processing * diff --git a/app/Models/Solo/ScoreData.php b/app/Models/Solo/ScoreData.php index 2338fa1d010..5fd564293c4 100644 --- a/app/Models/Solo/ScoreData.php +++ b/app/Models/Solo/ScoreData.php @@ -87,7 +87,13 @@ public function get($model, $key, $value, $attributes) public function set($model, $key, $value, $attributes) { if (!($value instanceof ScoreData)) { - $value = new ScoreData($value); + $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, + ]); } return ['data' => json_encode($value)]; diff --git a/app/Models/Traits/SoloScoreInterface.php b/app/Models/Traits/SoloScoreInterface.php new file mode 100644 index 00000000000..3541b877c57 --- /dev/null +++ b/app/Models/Traits/SoloScoreInterface.php @@ -0,0 +1,16 @@ +. 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); + +namespace App\Models\Traits; + +interface SoloScoreInterface +{ + // Eloquent attributes + // public \App\Models\Solo\ScoreData $data; + // public bool $has_replay + // public float $pp; +} diff --git a/app/Transformers/Multiplayer/ScoreTransformer.php b/app/Transformers/Multiplayer/ScoreTransformer.php deleted file mode 100644 index d5b2e46974a..00000000000 --- a/app/Transformers/Multiplayer/ScoreTransformer.php +++ /dev/null @@ -1,94 +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\Transformers\Multiplayer; - -use App\Models\Multiplayer\PlaylistItemUserHighScore; -use App\Models\Multiplayer\Score; -use App\Transformers\TransformerAbstract; -use App\Transformers\UserCompactTransformer; - -class ScoreTransformer extends TransformerAbstract -{ - // warning: this is actually for PlaylistItemUserHighScore, not for Score - const BASE_PRELOAD = ['score.user.userProfileCustomization', 'score.user.country']; - const BASE_INCLUDES = ['user.country', 'user.cover']; - - protected array $availableIncludes = [ - 'position', - 'scores_around', - 'user', - ]; - - public function transform(Score $score) - { - return [ - 'id' => $score->id, - 'user_id' => $score->user_id, - 'room_id' => $score->room_id, - 'playlist_item_id' => $score->playlist_item_id, - 'beatmap_id' => $score->beatmap_id, - 'rank' => $score->rank, - 'total_score' => $score->total_score, - 'accuracy' => $score->accuracy, - 'max_combo' => $score->max_combo, - 'mods' => $score->mods, - 'statistics' => $score->statistics, - 'passed' => $score->passed, - - 'started_at' => json_time($score->started_at), - 'ended_at' => json_time($score->ended_at), - ]; - } - - public function includePosition(Score $score) - { - return $this->primitive($score->userRank()); - } - - public function includeScoresAround(Score $score) - { - $limit = 10; - - $highScorePlaceholder = new PlaylistItemUserHighScore([ - 'score_id' => $score->getKey(), - 'total_score' => $score->total_score, - ]); - - $typeOptions = [ - 'higher' => 'score_asc', - 'lower' => 'score_desc', - ]; - - $ret = []; - - foreach ($typeOptions as $type => $sortName) { - $cursorHelper = PlaylistItemUserHighScore::makeDbCursorHelper($sortName); - [$highScores, $hasMore] = PlaylistItemUserHighScore - ::cursorSort($cursorHelper, $highScorePlaceholder) - ->with(static::BASE_PRELOAD) - ->where('playlist_item_id', $score->playlist_item_id) - ->where('user_id', '<>', $score->user_id) - ->limit($limit) - ->getWithHasMore(); - - $ret[$type] = [ - 'scores' => json_collection($highScores->pluck('score'), new static(), static::BASE_INCLUDES), - 'params' => ['limit' => $limit, 'sort' => $cursorHelper->getSortName()], - ...cursor_for_response($cursorHelper->next($highScores, $hasMore)), - ]; - } - - return $this->primitive($ret); - } - - public function includeUser(Score $score) - { - return $this->item( - $score->user, - new UserCompactTransformer() - ); - } -} diff --git a/app/Transformers/Score/CurrentUserAttributesTransformer.php b/app/Transformers/Score/CurrentUserAttributesTransformer.php index 5ed7b0fa8b6..ced66eb3dbe 100644 --- a/app/Transformers/Score/CurrentUserAttributesTransformer.php +++ b/app/Transformers/Score/CurrentUserAttributesTransformer.php @@ -10,11 +10,12 @@ use App\Models\LegacyMatch; use App\Models\Score\Model as ScoreModel; use App\Models\Solo\Score as SoloScore; +use App\Models\Traits\SoloScoreInterface; use App\Transformers\TransformerAbstract; class CurrentUserAttributesTransformer extends TransformerAbstract { - public function transform(LegacyMatch\Score|ScoreModel|SoloScore $score): array + public function transform(LegacyMatch\Score|ScoreModel|SoloScoreInterface $score): array { $pinnable = $score instanceof ScoreModel ? $score->best diff --git a/app/Transformers/ScoreTransformer.php b/app/Transformers/ScoreTransformer.php index d0d63f225ed..fcd373fe902 100644 --- a/app/Transformers/ScoreTransformer.php +++ b/app/Transformers/ScoreTransformer.php @@ -10,13 +10,24 @@ use App\Models\Beatmap; use App\Models\DeletedUser; use App\Models\LegacyMatch; +use App\Models\Multiplayer\PlaylistItemUserHighScore; +use App\Models\Multiplayer\ScoreLink as MultiplayerScoreLink; use App\Models\Score\Best\Model as ScoreBest; use App\Models\Score\Model as ScoreModel; use App\Models\Solo\Score as SoloScore; +use App\Models\Traits\SoloScoreInterface; use League\Fractal\Resource\Item; 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.user.country', + 'scoreLink.user.userProfileCustomization', + ]; + const TYPE_LEGACY = 'legacy'; const TYPE_SOLO = 'solo'; @@ -38,6 +49,10 @@ class ScoreTransformer extends TransformerAbstract 'rank_global', 'user', 'weight', + + // Only for MultiplayerScoreLink + 'position', + 'scores_around', ]; protected array $defaultIncludes = [ @@ -46,6 +61,11 @@ class ScoreTransformer extends TransformerAbstract private string $transformFunction; + public static function newSolo(): static + { + return new static(static::TYPE_SOLO); + } + public function __construct(?string $type = null) { $type ??= is_api_request() && api_version() < 20220705 @@ -62,14 +82,14 @@ public function __construct(?string $type = null) } } - public function transform(LegacyMatch\Score|ScoreModel|SoloScore $score) + public function transform(LegacyMatch\Score|ScoreModel|SoloScoreInterface $score) { $fn = $this->transformFunction; return $this->$fn($score); } - public function transformSolo(ScoreModel|SoloScore $score) + public function transformSolo(ScoreModel|SoloScoreInterface $score) { if ($score instanceof ScoreModel) { $legacyPerfect = $score->perfect; @@ -80,19 +100,28 @@ public function transformSolo(ScoreModel|SoloScore $score) $pp = $best->pp; $replay = $best->replay; } - } elseif ($score instanceof SoloScore) { + } elseif ($score instanceof SoloScoreInterface) { $pp = $score->pp; $replay = $score->has_replay; + + if ($score instanceof MultiplayerScoreLink) { + $multiplayerAttributes = [ + 'room_id' => $score->room_id, + 'playlist_item_id' => $score->playlist_item_id, + ]; + } } - return array_merge($score->data->jsonSerialize(), [ + return [ + ...$score->data->jsonSerialize(), + ...($multiplayerAttributes ?? []), 'best_id' => $bestId ?? null, 'id' => $score->getKey(), 'legacy_perfect' => $legacyPerfect ?? null, 'pp' => $pp ?? null, 'replay' => $replay ?? false, 'type' => $score->getMorphClass(), - ]); + ]; } public function transformLegacy(LegacyMatch\Score|ScoreModel|SoloScore $score) @@ -170,7 +199,7 @@ public function includeBeatmapset(LegacyMatch\Score|ScoreModel|SoloScore $score) return $this->item($score->beatmap->beatmapset, new BeatmapsetCompactTransformer()); } - public function includeCurrentUserAttributes(LegacyMatch\Score|ScoreModel|SoloScore $score): Item + public function includeCurrentUserAttributes(LegacyMatch\Score|ScoreModel|SoloScoreInterface $score): Item { return $this->item($score, new Score\CurrentUserAttributesTransformer()); } @@ -184,6 +213,47 @@ public function includeMatch(LegacyMatch\Score $score) ]); } + public function includePosition(MultiplayerScoreLink $scoreLink) + { + return $this->primitive($scoreLink->position()); + } + + public function includeScoresAround(MultiplayerScoreLink $scoreLink) + { + $limit = 10; + + $highScorePlaceholder = new PlaylistItemUserHighScore([ + 'score_link_id' => $scoreLink->getKey(), + 'total_score' => $scoreLink->data->totalScore, + ]); + + $typeOptions = [ + 'higher' => 'score_asc', + 'lower' => 'score_desc', + ]; + + $ret = []; + + foreach ($typeOptions as $type => $sortName) { + $cursorHelper = PlaylistItemUserHighScore::makeDbCursorHelper($sortName); + [$highScores, $hasMore] = PlaylistItemUserHighScore + ::cursorSort($cursorHelper, $highScorePlaceholder) + ->with(static::MULTIPLAYER_BASE_PRELOAD) + ->where('playlist_item_id', $scoreLink->playlist_item_id) + ->where('user_id', '<>', $scoreLink->user_id) + ->limit($limit) + ->getWithHasMore(); + + $ret[$type] = [ + 'scores' => json_collection($highScores->pluck('scoreLink.score'), new static(), static::MULTIPLAYER_BASE_INCLUDES), + 'params' => ['limit' => $limit, 'sort' => $cursorHelper->getSortName()], + ...cursor_for_response($cursorHelper->next($highScores, $hasMore)), + ]; + } + + return $this->primitive($ret); + } + public function includeRankCountry(ScoreBest|SoloScore $score) { return $this->primitive($score->userRank(['type' => 'country'])); @@ -194,7 +264,7 @@ public function includeRankGlobal(ScoreBest|SoloScore $score) return $this->primitive($score->userRank([])); } - public function includeUser(LegacyMatch\Score|ScoreModel|SoloScore $score) + public function includeUser(LegacyMatch\Score|ScoreModel|SoloScoreInterface $score) { return $this->item( $score->user ?? new DeletedUser(['user_id' => $score->user_id]), diff --git a/database/factories/Multiplayer/ScoreFactory.php b/database/factories/Multiplayer/ScoreFactory.php deleted file mode 100644 index e72320d3713..00000000000 --- a/database/factories/Multiplayer/ScoreFactory.php +++ /dev/null @@ -1,53 +0,0 @@ -. 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); - -namespace Database\Factories\Multiplayer; - -use App\Models\Multiplayer\PlaylistItem; -use App\Models\Multiplayer\Score; -use App\Models\User; -use Carbon\Carbon; -use Database\Factories\Factory; - -class ScoreFactory extends Factory -{ - protected $model = Score::class; - - public function completed(): static - { - return $this->state(['ended_at' => Carbon::now()->subMinutes(1)]); - } - - public function definition(): array - { - return [ - 'playlist_item_id' => PlaylistItem::factory(), - 'beatmap_id' => fn(array $attributes) => PlaylistItem::find($attributes['playlist_item_id'])->beatmap_id, - 'room_id' => fn(array $attributes) => PlaylistItem::find($attributes['playlist_item_id'])->room_id, - 'user_id' => User::factory(), - 'total_score' => 1, - 'started_at' => fn() => Carbon::now()->subMinutes(5), - 'accuracy' => 0.5, - 'pp' => 1, - ]; - } - - public function failed(): static - { - return $this->completed()->state(['passed' => false]); - } - - public function passed(): static - { - return $this->completed()->state(['passed' => true]); - } - - public function scoreless(): static - { - return $this->state(['total_score' => 0]); - } -} diff --git a/database/factories/Multiplayer/ScoreLinkFactory.php b/database/factories/Multiplayer/ScoreLinkFactory.php new file mode 100644 index 00000000000..947d5679632 --- /dev/null +++ b/database/factories/Multiplayer/ScoreLinkFactory.php @@ -0,0 +1,52 @@ +. 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); + +namespace Database\Factories\Multiplayer; + +use App\Models\Multiplayer\PlaylistItem; +use App\Models\Multiplayer\ScoreLink; +use App\Models\Solo\Score; +use App\Models\User; +use Database\Factories\Factory; + +class ScoreLinkFactory extends Factory +{ + protected $model = ScoreLink::class; + + public function completed(?array $scoreAttr = [], ?array $scoreDataAttr = []): static + { + return $this->state([ + 'score_id' => fn (array $attr) => Score::factory([ + 'beatmap_id' => $attr['beatmap_id'], + 'user_id' => $attr['user_id'], + ...$scoreAttr, + ])->withData($scoreDataAttr), + ]); + } + + public function definition(): array + { + return [ + 'playlist_item_id' => PlaylistItem::factory(), + 'user_id' => User::factory(), + + // depends on PlaylistItem + 'beatmap_id' => fn (array $attr) => PlaylistItem::find($attr['playlist_item_id'])->beatmap_id, + 'room_id' => fn (array $attr) => PlaylistItem::find($attr['playlist_item_id'])->room_id, + ]; + } + + public function failed(): static + { + return $this->completed([], ['passed' => false]); + } + + public function passed(): static + { + return $this->completed([], ['passed' => true]); + } +} diff --git a/database/migrations/2023_07_26_103744_create_multiplayer_score_links.php b/database/migrations/2023_07_26_103744_create_multiplayer_score_links.php new file mode 100644 index 00000000000..56ef5d490cb --- /dev/null +++ b/database/migrations/2023_07_26_103744_create_multiplayer_score_links.php @@ -0,0 +1,45 @@ +. 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\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('multiplayer_score_links', function (Blueprint $table) { + $table->id(); + + $table->unsignedInteger('user_id'); + $table->unsignedBigInteger('room_id'); + $table->unsignedBigInteger('playlist_item_id'); + $table->unsignedMediumInteger('beatmap_id'); + $table->unsignedMediumInteger('build_id')->default(0); + $table->unsignedBigInteger('score_id')->nullable(); + + $table->timestampsTz(); + + $table->index('score_id'); + $table->index(['room_id', 'user_id']); + $table->index('playlist_item_id'); + $table->index('user_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('multiplayer_score_links'); + } +}; diff --git a/database/migrations/2023_08_01_064505_add_score_link_id_to_multiplayer_scores_high.php b/database/migrations/2023_08_01_064505_add_score_link_id_to_multiplayer_scores_high.php new file mode 100644 index 00000000000..7718661bd61 --- /dev/null +++ b/database/migrations/2023_08_01_064505_add_score_link_id_to_multiplayer_scores_high.php @@ -0,0 +1,35 @@ +. 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\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('multiplayer_scores_high', function (Blueprint $table) { + $table->unsignedBigInteger('score_link_id')->nullable()->after('score_id'); + $table->index(['playlist_item_id', DB::raw('total_score DESC'), 'score_link_id'], 'top_scores_linked'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('multiplayer_scores_high', function (Blueprint $table) { + $table->dropIndex('top_scores_linked'); + $table->dropColumn('score_link_id'); + }); + } +}; diff --git a/database/migrations/2023_08_01_101614_add_last_score_link_id_to_multiplayer_rooms_high.php b/database/migrations/2023_08_01_101614_add_last_score_link_id_to_multiplayer_rooms_high.php new file mode 100644 index 00000000000..55819991c91 --- /dev/null +++ b/database/migrations/2023_08_01_101614_add_last_score_link_id_to_multiplayer_rooms_high.php @@ -0,0 +1,33 @@ +. 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\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('multiplayer_rooms_high', function (Blueprint $table) { + $table->unsignedBigInteger('last_score_link_id')->nullable()->after('last_score_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('multiplayer_rooms_high', function (Blueprint $table) { + $table->dropColumn('last_score_link_id'); + }); + } +}; diff --git a/database/seeders/ModelSeeders/MultiplayerSeeder.php b/database/seeders/ModelSeeders/MultiplayerSeeder.php index 89562aea728..43472f1f026 100644 --- a/database/seeders/ModelSeeders/MultiplayerSeeder.php +++ b/database/seeders/ModelSeeders/MultiplayerSeeder.php @@ -10,7 +10,7 @@ use App\Models\Beatmap; use App\Models\Multiplayer\PlaylistItem; use App\Models\Multiplayer\Room; -use App\Models\Multiplayer\Score; +use App\Models\Multiplayer\ScoreLink; use App\Models\User; use Carbon\Carbon; use Illuminate\Database\Seeder; @@ -39,18 +39,22 @@ public function run() $attempts = rand(1, 10); for ($i = 0; $i < $attempts; $i++) { $completed = rand(0, 100) > 20; - Score::factory()->create([ + $scoreLink = ScoreLink::factory()->make([ 'playlist_item_id' => $playlistItem->getKey(), 'user_id' => $user->getKey(), 'beatmap_id' => $beatmap->getKey(), 'room_id' => $room->getKey(), - 'total_score' => rand(10000, 100000), - 'started_at' => Carbon::now()->subMinutes(5), - 'ended_at' => $completed ? Carbon::now() : null, - 'passed' => $completed ? rand(0, 100) > 20 : null, - 'accuracy' => rand(50, 100) / 100, - 'pp' => rand(100, 200), ]); + if ($completed) { + $scoreLink = $scoreLink->completed([ + 'pp' => rand(100, 200), + ], [ + 'total_score' => rand(10000, 100000), + 'started_at' => Carbon::now()->subMinutes(5), + 'passed' => rand(0, 100) > 20, + 'accuracy' => rand(50, 100) / 100, + ]); + } } } } diff --git a/tests/Controllers/Chat/ChannelsControllerTest.php b/tests/Controllers/Chat/ChannelsControllerTest.php index 519f0fd57af..603879f66db 100644 --- a/tests/Controllers/Chat/ChannelsControllerTest.php +++ b/tests/Controllers/Chat/ChannelsControllerTest.php @@ -10,7 +10,7 @@ use App\Libraries\UserChannelList; use App\Models\Chat\Channel; use App\Models\Chat\Message; -use App\Models\Multiplayer\Score; +use App\Models\Multiplayer\ScoreLink; use App\Models\User; use Illuminate\Testing\AssertableJsonString; use Illuminate\Testing\Fluent\AssertableJson; @@ -181,11 +181,11 @@ public function testChannelJoinPM() // fail public function testChannelJoinMultiplayerWhenNotParticipated() { - $score = Score::factory()->create(); + $scoreLink = ScoreLink::factory()->create(); $this->actAsScopedUser($this->user, ['*']); $request = $this->json('PUT', route('api.chat.channels.join', [ - 'channel' => $score->room->channel_id, + 'channel' => $scoreLink->room->channel_id, 'user' => $this->user->getKey(), ])); @@ -194,15 +194,15 @@ public function testChannelJoinMultiplayerWhenNotParticipated() public function testChannelJoinMultiplayerWhenParticipated() { - $score = Score::factory()->create(['user_id' => $this->user]); + $scoreLink = ScoreLink::factory()->create(['user_id' => $this->user]); $this->actAsScopedUser($this->user, ['*']); $request = $this->json('PUT', route('api.chat.channels.join', [ - 'channel' => $score->room->channel_id, + 'channel' => $scoreLink->room->channel_id, 'user' => $this->user->getKey(), ])); - $request->assertStatus(200)->assertJsonFragment(['channel_id' => $score->room->channel_id, 'type' => Channel::TYPES['multiplayer']]); + $request->assertStatus(200)->assertJsonFragment(['channel_id' => $scoreLink->room->channel_id, 'type' => Channel::TYPES['multiplayer']]); } //endregion diff --git a/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php b/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php index dc4e6ceec0f..ce254f7eb6d 100644 --- a/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php +++ b/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php @@ -7,7 +7,7 @@ use App\Models\Build; use App\Models\Multiplayer\PlaylistItem; -use App\Models\Multiplayer\Score; +use App\Models\Multiplayer\ScoreLink; use App\Models\User; use Tests\TestCase; @@ -15,15 +15,15 @@ class ScoresControllerTest extends TestCase { public function testShow() { - $score = Score::factory()->create(); + $scoreLink = ScoreLink::factory()->create(); $user = User::factory()->create(); $this->actAsScopedUser($user, ['*']); $this->json('GET', route('api.rooms.playlist.scores.show', [ - 'room' => $score->room_id, - 'playlist' => $score->playlist_item_id, - 'score' => $score->getKey(), + 'room' => $scoreLink->room_id, + 'playlist' => $scoreLink->playlist_item_id, + 'score' => $scoreLink->getKey(), ]))->assertSuccessful(); } @@ -35,7 +35,6 @@ public function testStore($allowRanking, $hashParam, $status) $user = User::factory()->create(); $playlistItem = PlaylistItem::factory()->create(); $build = Build::factory()->create(['allow_ranking' => $allowRanking]); - $initialScoresCount = Score::count(); $this->actAsScopedUser($user, ['*']); @@ -44,14 +43,13 @@ public function testStore($allowRanking, $hashParam, $status) $params['version_hash'] = $hashParam ? bin2hex($build->hash) : md5('invalid_'); } + $countDiff = ((string) $status)[0] === '2' ? 1 : 0; + $this->expectCountChange(fn () => ScoreLink::count(), $countDiff); + $this->json('POST', route('api.rooms.playlist.scores.store', [ 'room' => $playlistItem->room_id, 'playlist' => $playlistItem->getKey(), ]), $params)->assertStatus($status); - - $countDiff = ((string) $status)[0] === '2' ? 1 : 0; - - $this->assertSame($initialScoresCount + $countDiff, Score::count()); } /** @@ -63,14 +61,14 @@ public function testUpdate($bodyParams, $status) $playlistItem = PlaylistItem::factory()->create(); $room = $playlistItem->room; $build = Build::factory()->create(['allow_ranking' => true]); - $score = $room->startPlay($user, $playlistItem); + $scoreLink = $room->startPlay($user, $playlistItem, 0); $this->actAsScopedUser($user, ['*']); $url = route('api.rooms.playlist.scores.update', [ 'room' => $room, 'playlist' => $playlistItem, - 'score' => $score, + 'score' => $scoreLink, ]); $this->json('PUT', $url, $bodyParams)->assertStatus($status); diff --git a/tests/Models/ContestTest.php b/tests/Models/ContestTest.php index 63626390278..7203182c993 100644 --- a/tests/Models/ContestTest.php +++ b/tests/Models/ContestTest.php @@ -14,8 +14,10 @@ use App\Models\ContestEntry; use App\Models\Multiplayer\PlaylistItem; use App\Models\Multiplayer\Room; -use App\Models\Multiplayer\Score as MultiplayerScore; +use App\Models\Multiplayer\ScoreLink as MultiplayerScoreLink; +use App\Models\Multiplayer\UserScoreAggregate; use App\Models\User; +use Carbon\Carbon; use Tests\TestCase; class ContestTest extends TestCase @@ -56,28 +58,37 @@ public function testAssertVoteRequirementPlaylistBeatmapsets(bool $loggedIn, boo ]); $entries = ContestEntry::factory()->count(2)->create(['contest_id' => $contest->getKey()]); - if (!$canVote) { - $this->expectException(InvariantException::class); - } - $user = $loggedIn ? User::factory()->create() : null; if ($loggedIn && $played) { $userId = $user->getKey(); - $endedAt = now(); + $endedAt = json_time(Carbon::now()); foreach ($beatmapsets as $beatmapset) { $room = array_rand_val($rooms); $playlistItem = $room ->playlist() ->whereIn('beatmap_id', array_column($beatmapset->beatmaps->all(), 'beatmap_id')) ->first(); - MultiplayerScore::factory()->create([ - 'ended_at' => $completed ? $endedAt : null, - 'passed' => $passed, + + $scoreLink = MultiplayerScoreLink::factory()->state([ 'playlist_item_id' => $playlistItem, 'user_id' => $userId, ]); + if ($completed) { + $scoreLink = $scoreLink->completed([], [ + 'ended_at' => $endedAt, + 'passed' => $passed, + ]); + } + $scoreLink->create(); } + foreach ($rooms as $room) { + UserScoreAggregate::lookupOrDefault($user, $room)->recalculate(); + } + } + + if (!$canVote) { + $this->expectException(InvariantException::class); } $contest->assertVoteRequirement($user); diff --git a/tests/Models/Multiplayer/RoomTest.php b/tests/Models/Multiplayer/RoomTest.php index e10a870fe00..29ea49cd44d 100644 --- a/tests/Models/Multiplayer/RoomTest.php +++ b/tests/Models/Multiplayer/RoomTest.php @@ -100,7 +100,7 @@ public function testRoomHasEnded() ]); $this->expectException(InvariantException::class); - $room->startPlay($user, $playlistItem); + $room->startPlay($user, $playlistItem, 0); } public function testStartPlay(): void @@ -111,12 +111,12 @@ public function testStartPlay(): void $this->expectCountChange(fn () => $room->participant_count, 1); $this->expectCountChange(fn () => $room->userHighScores()->count(), 1); - $this->expectCountChange(fn () => $room->scores()->count(), 1); + $this->expectCountChange(fn () => $room->scoreLinks()->count(), 1); - $room->startPlay($user, $playlistItem); + $room->startPlay($user, $playlistItem, 0); $room->refresh(); - $this->assertSame($user->getKey(), $room->scores()->last()->user_id); + $this->assertSame($user->getKey(), $room->scoreLinks()->last()->user_id); } public function testMaxAttemptsReached() @@ -126,14 +126,14 @@ public function testMaxAttemptsReached() $playlistItem1 = PlaylistItem::factory()->create(['room_id' => $room]); $playlistItem2 = PlaylistItem::factory()->create(['room_id' => $room]); - $room->startPlay($user, $playlistItem1); + $room->startPlay($user, $playlistItem1, 0); $this->assertTrue(true); - $room->startPlay($user, $playlistItem2); + $room->startPlay($user, $playlistItem2, 0); $this->assertTrue(true); $this->expectException(InvariantException::class); - $room->startPlay($user, $playlistItem1); + $room->startPlay($user, $playlistItem1, 0); } public function testMaxAttemptsForItemReached() @@ -149,21 +149,21 @@ public function testMaxAttemptsForItemReached() 'max_attempts' => 1, ]); - $initialCount = $room->scores()->count(); - $room->startPlay($user, $playlistItem1); - $this->assertSame($initialCount + 1, $room->scores()->count()); + $initialCount = $room->scoreLinks()->count(); + $room->startPlay($user, $playlistItem1, 0); + $this->assertSame($initialCount + 1, $room->scoreLinks()->count()); - $initialCount = $room->scores()->count(); + $initialCount = $room->scoreLinks()->count(); try { - $room->startPlay($user, $playlistItem1); + $room->startPlay($user, $playlistItem1, 0); } catch (Exception $ex) { $this->assertTrue($ex instanceof InvariantException); } - $this->assertSame($initialCount, $room->scores()->count()); + $this->assertSame($initialCount, $room->scoreLinks()->count()); - $initialCount = $room->scores()->count(); - $room->startPlay($user, $playlistItem2); - $this->assertSame($initialCount + 1, $room->scores()->count()); + $initialCount = $room->scoreLinks()->count(); + $room->startPlay($user, $playlistItem2, 0); + $this->assertSame($initialCount + 1, $room->scoreLinks()->count()); } public function testCannotStartPlayedItem() diff --git a/tests/Models/Multiplayer/UserScoreAggregateTest.php b/tests/Models/Multiplayer/UserScoreAggregateTest.php index 0992b270597..21a8e3ef04b 100644 --- a/tests/Models/Multiplayer/UserScoreAggregateTest.php +++ b/tests/Models/Multiplayer/UserScoreAggregateTest.php @@ -7,7 +7,7 @@ use App\Models\Multiplayer\PlaylistItem; use App\Models\Multiplayer\Room; -use App\Models\Multiplayer\Score; +use App\Models\Multiplayer\ScoreLink; use App\Models\Multiplayer\UserScoreAggregate; use App\Models\User; use Tests\TestCase; @@ -21,7 +21,7 @@ public function testStartingPlayIncreasesAttempts() $user = User::factory()->create(); $playlistItem = $this->playlistItem(); - $this->room->startPlay($user, $playlistItem); + $this->room->startPlay($user, $playlistItem, 0); $agg = UserScoreAggregate::new($user, $this->room); $this->assertSame(1, $agg->attempts); @@ -34,14 +34,14 @@ public function testInCompleteScoresAreNotCounted() $playlistItem = $this->playlistItem(); $agg = UserScoreAggregate::new($user, $this->room); - $score = Score::factory() - ->create([ + $scoreLink = ScoreLink::factory() + ->state([ 'room_id' => $this->room, 'playlist_item_id' => $playlistItem, 'user_id' => $user, - ]); + ])->create(); - $agg->addScore($score); + $agg->addScoreLink($scoreLink); $result = json_item($agg, 'Multiplayer\UserScoreAggregate'); $this->assertSame(0, $result['completed']); @@ -54,24 +54,25 @@ public function testFailedScoresAreAttemptsOnly() $playlistItem = $this->playlistItem(); $agg = UserScoreAggregate::new($user, $this->room); - $agg->addScore( - Score::factory() - ->failed() - ->create([ + $agg->addScoreLink( + ScoreLink + ::factory() + ->state([ 'room_id' => $this->room, 'playlist_item_id' => $playlistItem, 'user_id' => $user, - ]) + ])->failed() + ->create() ); - $agg->addScore( - Score::factory() - ->passed() - ->create([ + $agg->addScoreLink( + ScoreLink::factory() + ->state([ 'room_id' => $this->room, 'playlist_item_id' => $playlistItem, 'user_id' => $user, - ]) + ])->completed([], ['passed' => true, 'total_score' => 1]) + ->create() ); $result = json_item($agg, 'Multiplayer\UserScoreAggregate'); @@ -86,14 +87,14 @@ public function testPassedScoresIncrementsCompletedCount() $playlistItem = $this->playlistItem(); $agg = UserScoreAggregate::new($user, $this->room); - $agg->addScore( - Score::factory() - ->passed() - ->create([ - 'room_id' => $this->room, - 'playlist_item_id' => $playlistItem, - 'user_id' => $user, - ]) + $agg->addScoreLink( + ScoreLink::factory() + ->state([ + 'room_id' => $this->room, + 'playlist_item_id' => $playlistItem, + 'user_id' => $user, + ])->completed([], ['passed' => true, 'total_score' => 1]) + ->create() ); $result = json_item($agg, 'Multiplayer\UserScoreAggregate'); @@ -109,56 +110,60 @@ public function testPassedScoresAreAveraged() $playlistItem2 = $this->playlistItem(); $agg = UserScoreAggregate::new($user, $this->room); - $agg->addScore( - Score::factory() - ->create([ - 'room_id' => $this->room, - 'playlist_item_id' => $playlistItem, - 'user_id' => $user, - 'total_score' => 1, - 'pp' => 0.2, - 'pp' => 0.2, - ]) - ); - - $agg->addScore( - Score::factory() - ->failed() - ->create([ - 'room_id' => $this->room, - 'playlist_item_id' => $playlistItem, - 'user_id' => $user, - 'total_score' => 1, - 'accuracy' => 0.3, - 'pp' => 0.3, - ]) - ); - - $agg->addScore( - Score::factory() - ->passed() - ->create([ - 'room_id' => $this->room, - 'playlist_item_id' => $playlistItem, - 'user_id' => $user, - 'total_score' => 1, - 'accuracy' => 0.5, - 'pp' => 0.5, - ]) - ); - - $agg->addScore( - Score::factory() - ->passed() - ->create([ - 'room_id' => $this->room, - 'playlist_item_id' => $playlistItem2, - 'user_id' => $user, - 'total_score' => 1, - 'accuracy' => 0.8, - 'pp' => 0.8, - ]) - ); + $agg->addScoreLink(tap( + ScoreLink::factory() + ->state([ + 'room_id' => $this->room, + 'playlist_item_id' => $playlistItem, + 'user_id' => $user, + ])->completed([], [ + 'total_score' => 1, + 'passed' => false, + ])->create(), + fn ($l) => $l->score->performance()->create(['pp' => 0.2]), + )); + + $agg->addScoreLink(tap( + ScoreLink::factory() + ->state([ + 'room_id' => $this->room, + 'playlist_item_id' => $playlistItem, + 'user_id' => $user, + ])->completed([], [ + 'total_score' => 1, + 'accuracy' => 0.3, + 'passed' => false, + ])->create(), + fn ($l) => $l->score->performance()->create(['pp' => 0.3]), + )); + + $agg->addScoreLink(tap( + ScoreLink::factory() + ->state([ + 'room_id' => $this->room, + 'playlist_item_id' => $playlistItem, + 'user_id' => $user, + ])->completed([], [ + 'total_score' => 1, + 'accuracy' => 0.5, + 'passed' => true, + ])->create(), + fn ($l) => $l->score->performance()->create(['pp' => 0.5]), + )); + + $agg->addScoreLink(tap( + ScoreLink::factory() + ->state([ + 'room_id' => $this->room, + 'playlist_item_id' => $playlistItem2, + 'user_id' => $user, + ])->completed([], [ + 'total_score' => 1, + 'accuracy' => 0.8, + 'passed' => true, + ])->create(), + fn ($l) => $l->score->performance()->create(['pp' => 0.8]), + )); $result = json_item($agg, 'Multiplayer\UserScoreAggregate'); From 5785951927fcc8dda9982992e2b3f210b7dd2866 Mon Sep 17 00:00:00 2001 From: nanaya Date: Thu, 17 Aug 2023 23:49:54 +0900 Subject: [PATCH 046/470] Don't check for average pp The number won't be correct as a lot would be involved to properly fill the data. The column itself may be deleted sometime later. --- .../Multiplayer/UserScoreAggregateTest.php | 97 ++++++++----------- 1 file changed, 42 insertions(+), 55 deletions(-) diff --git a/tests/Models/Multiplayer/UserScoreAggregateTest.php b/tests/Models/Multiplayer/UserScoreAggregateTest.php index 21a8e3ef04b..69433aa6971 100644 --- a/tests/Models/Multiplayer/UserScoreAggregateTest.php +++ b/tests/Models/Multiplayer/UserScoreAggregateTest.php @@ -110,64 +110,51 @@ public function testPassedScoresAreAveraged() $playlistItem2 = $this->playlistItem(); $agg = UserScoreAggregate::new($user, $this->room); - $agg->addScoreLink(tap( - ScoreLink::factory() - ->state([ - 'room_id' => $this->room, - 'playlist_item_id' => $playlistItem, - 'user_id' => $user, - ])->completed([], [ - 'total_score' => 1, - 'passed' => false, - ])->create(), - fn ($l) => $l->score->performance()->create(['pp' => 0.2]), - )); - - $agg->addScoreLink(tap( - ScoreLink::factory() - ->state([ - 'room_id' => $this->room, - 'playlist_item_id' => $playlistItem, - 'user_id' => $user, - ])->completed([], [ - 'total_score' => 1, - 'accuracy' => 0.3, - 'passed' => false, - ])->create(), - fn ($l) => $l->score->performance()->create(['pp' => 0.3]), - )); - - $agg->addScoreLink(tap( - ScoreLink::factory() - ->state([ - 'room_id' => $this->room, - 'playlist_item_id' => $playlistItem, - 'user_id' => $user, - ])->completed([], [ - 'total_score' => 1, - 'accuracy' => 0.5, - 'passed' => true, - ])->create(), - fn ($l) => $l->score->performance()->create(['pp' => 0.5]), - )); - - $agg->addScoreLink(tap( - ScoreLink::factory() - ->state([ - 'room_id' => $this->room, - 'playlist_item_id' => $playlistItem2, - 'user_id' => $user, - ])->completed([], [ - 'total_score' => 1, - 'accuracy' => 0.8, - 'passed' => true, - ])->create(), - fn ($l) => $l->score->performance()->create(['pp' => 0.8]), - )); + $agg->addScoreLink(ScoreLink::factory() + ->state([ + 'room_id' => $this->room, + 'playlist_item_id' => $playlistItem, + 'user_id' => $user, + ])->completed([], [ + 'total_score' => 1, + 'passed' => false, + ])->create()); + + $agg->addScoreLink(ScoreLink::factory() + ->state([ + 'room_id' => $this->room, + 'playlist_item_id' => $playlistItem, + 'user_id' => $user, + ])->completed([], [ + 'total_score' => 1, + 'accuracy' => 0.3, + 'passed' => false, + ])->create()); + + $agg->addScoreLink(ScoreLink::factory() + ->state([ + 'room_id' => $this->room, + 'playlist_item_id' => $playlistItem, + 'user_id' => $user, + ])->completed([], [ + 'total_score' => 1, + 'accuracy' => 0.5, + 'passed' => true, + ])->create()); + + $agg->addScoreLink(ScoreLink::factory() + ->state([ + 'room_id' => $this->room, + 'playlist_item_id' => $playlistItem2, + 'user_id' => $user, + ])->completed([], [ + 'total_score' => 1, + 'accuracy' => 0.8, + 'passed' => true, + ])->create()); $result = json_item($agg, 'Multiplayer\UserScoreAggregate'); - $this->assertSame(0.65, $result['pp']); $this->assertSame(0.65, $result['accuracy']); } From 1cf5b92b94987a2e4e2f3fbf798f0ddbe964316d Mon Sep 17 00:00:00 2001 From: nanaya Date: Mon, 21 Aug 2023 19:39:40 +0900 Subject: [PATCH 047/470] Pluck correct thing --- .../Controllers/Multiplayer/Rooms/Playlist/ScoresController.php | 2 +- app/Transformers/ScoreTransformer.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php index 3e2ed735a5c..1fbbd2ad90d 100644 --- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php +++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php @@ -58,7 +58,7 @@ public function index($roomId, $playlistId) $transformer = ScoreTransformer::newSolo(); $scoresJson = json_collection( - $highScores->pluck('score'), + $highScores->pluck('scoreLink'), $transformer, ScoreTransformer::MULTIPLAYER_BASE_INCLUDES ); diff --git a/app/Transformers/ScoreTransformer.php b/app/Transformers/ScoreTransformer.php index fcd373fe902..506f4b42340 100644 --- a/app/Transformers/ScoreTransformer.php +++ b/app/Transformers/ScoreTransformer.php @@ -245,7 +245,7 @@ public function includeScoresAround(MultiplayerScoreLink $scoreLink) ->getWithHasMore(); $ret[$type] = [ - 'scores' => json_collection($highScores->pluck('scoreLink.score'), new static(), static::MULTIPLAYER_BASE_INCLUDES), + 'scores' => json_collection($highScores->pluck('scoreLink'), new static(), static::MULTIPLAYER_BASE_INCLUDES), 'params' => ['limit' => $limit, 'sort' => $cursorHelper->getSortName()], ...cursor_for_response($cursorHelper->next($highScores, $hasMore)), ]; From e0998314ffeee9f0a1b1e31a92f6eadd16795e1d Mon Sep 17 00:00:00 2001 From: nanaya Date: Mon, 21 Aug 2023 19:41:25 +0900 Subject: [PATCH 048/470] Only fetch new high scores (the ones with scoreLink) --- .../Controllers/Multiplayer/Rooms/Playlist/ScoresController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php index 1fbbd2ad90d..8a1736d8168 100644 --- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php +++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php @@ -51,6 +51,7 @@ public function index($roomId, $playlistId) [$highScores, $hasMore] = $playlist ->highScores() + ->whereHas('scoreLink') ->cursorSort($cursorHelper, cursor_from_params($params)) ->with(ScoreTransformer::MULTIPLAYER_BASE_PRELOAD) ->limit($limit) From 420a4fd0fb46795ac4a26a76d1d15412980e68a2 Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 23 Aug 2023 14:26:37 +0900 Subject: [PATCH 049/470] Use correct attribute and transformer --- .../Multiplayer/Rooms/Playlist/ScoresController.php | 4 ++-- app/Transformers/ScoreTransformer.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php index 8a1736d8168..5ce1c983ce6 100644 --- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php +++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php @@ -71,7 +71,7 @@ public function index($roomId, $playlistId) $userHighScore = $playlist->highScores()->where('user_id', $user->getKey())->first(); if ($userHighScore !== null) { - $userScoreJson = json_item($userHighScore->score, $transformer, ScoreTransformer::BASE_INCLUDES); + $userScoreJson = json_item($userHighScore->scoreLink, $transformer, ScoreTransformer::MULTIPLAYER_BASE_INCLUDES); } } @@ -135,7 +135,7 @@ public function showUser($roomId, $playlistId, $userId) { $room = Room::find($roomId) ?? abort(404, 'Invalid room id'); $playlistItem = $room->playlist()->find($playlistId) ?? abort(404, 'Invalid playlist id'); - $score = $playlistItem->highScores()->where('user_id', $userId)->firstOrFail()->score ?? abort(404); + $score = $playlistItem->highScores()->where('user_id', $userId)->firstOrFail()->scoreLink ?? abort(404); return json_item( $score, diff --git a/app/Transformers/ScoreTransformer.php b/app/Transformers/ScoreTransformer.php index 506f4b42340..8d0dabb0b05 100644 --- a/app/Transformers/ScoreTransformer.php +++ b/app/Transformers/ScoreTransformer.php @@ -245,7 +245,7 @@ public function includeScoresAround(MultiplayerScoreLink $scoreLink) ->getWithHasMore(); $ret[$type] = [ - 'scores' => json_collection($highScores->pluck('scoreLink'), new static(), static::MULTIPLAYER_BASE_INCLUDES), + 'scores' => json_collection($highScores->pluck('scoreLink'), static::newSolo(), static::MULTIPLAYER_BASE_INCLUDES), 'params' => ['limit' => $limit, 'sort' => $cursorHelper->getSortName()], ...cursor_for_response($cursorHelper->next($highScores, $hasMore)), ]; From 10e1152f52445b4dca44d9f82cdb1fe55f497f7e Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 23 Aug 2023 15:25:31 +0900 Subject: [PATCH 050/470] Add test for scores index --- .../Rooms/Playlist/ScoresController.php | 15 +++++-- .../Rooms/Playlist/ScoresControllerTest.php | 43 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php index 5ce1c983ce6..570f42496b7 100644 --- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php +++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php @@ -10,6 +10,7 @@ use App\Models\Multiplayer\PlaylistItem; use App\Models\Multiplayer\PlaylistItemUserHighScore; use App\Models\Multiplayer\Room; +use App\Models\Multiplayer\ScoreLink; use App\Models\Solo\Score; use App\Transformers\ScoreTransformer; @@ -68,10 +69,16 @@ public function index($roomId, $playlistId) $user = auth()->user(); if ($user !== null) { - $userHighScore = $playlist->highScores()->where('user_id', $user->getKey())->first(); - - if ($userHighScore !== null) { - $userScoreJson = json_item($userHighScore->scoreLink, $transformer, ScoreTransformer::MULTIPLAYER_BASE_INCLUDES); + $userHighScoreLink = ScoreLink::whereIn( + 'id', + $playlist + ->highScores() + ->where('user_id', $user->getKey()) + ->select('score_link_id'), + )->first(); + + if ($userHighScoreLink !== null) { + $userScoreJson = json_item($userHighScoreLink, $transformer, ScoreTransformer::MULTIPLAYER_BASE_INCLUDES); } } diff --git a/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php b/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php index ce254f7eb6d..40283170f0b 100644 --- a/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php +++ b/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php @@ -8,11 +8,54 @@ use App\Models\Build; use App\Models\Multiplayer\PlaylistItem; use App\Models\Multiplayer\ScoreLink; +use App\Models\Multiplayer\UserScoreAggregate; use App\Models\User; use Tests\TestCase; class ScoresControllerTest extends TestCase { + public function testIndex() + { + $playlist = PlaylistItem::factory()->create(); + $user = User::factory()->create(); + $scores = []; + $scores[] = ScoreLink + ::factory() + ->state(['playlist_item_id' => $playlist]) + ->completed([], ['passed' => true, 'total_score' => 30]) + ->create(); + $scores[] = $userScore = ScoreLink + ::factory() + ->state([ + 'playlist_item_id' => $playlist, + 'user_id' => $user, + ])->completed([], ['passed' => true, 'total_score' => 20]) + ->create(); + $scores[] = ScoreLink + ::factory() + ->state(['playlist_item_id' => $playlist]) + ->completed([], ['passed' => true, 'total_score' => 10]) + ->create(); + + foreach ($scores as $score) { + UserScoreAggregate::lookupOrDefault($score->user, $score->room)->recalculate(); + } + + $this->actAsScopedUser($user, ['*']); + + $resp = $this->json('GET', route('api.rooms.playlist.scores.index', [ + 'room' => $playlist->room_id, + 'playlist' => $playlist->getKey(), + ]))->assertSuccessful(); + + $json = json_decode($resp->getContent(), true); + $this->assertSame(count($scores), count($json['scores'])); + foreach ($json['scores'] as $i => $jsonScore) { + $this->assertSame($scores[$i]->getKey(), $jsonScore['id']); + } + $this->assertSame($json['user_score']['id'], $userScore->getKey()); + } + public function testShow() { $scoreLink = ScoreLink::factory()->create(); From 0755a5908337018ab69a0de164600b2accb50a7a Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 23 Aug 2023 15:42:29 +0900 Subject: [PATCH 051/470] Move scores around generation to model and add tests --- .../Multiplayer/PlaylistItemUserHighScore.php | 29 +++++++++++ app/Transformers/ScoreTransformer.php | 51 +++++++------------ .../Rooms/Playlist/ScoresControllerTest.php | 37 ++++++++++++-- 3 files changed, 80 insertions(+), 37 deletions(-) diff --git a/app/Models/Multiplayer/PlaylistItemUserHighScore.php b/app/Models/Multiplayer/PlaylistItemUserHighScore.php index 1330ab58dbc..0b3f2a290bf 100644 --- a/app/Models/Multiplayer/PlaylistItemUserHighScore.php +++ b/app/Models/Multiplayer/PlaylistItemUserHighScore.php @@ -53,6 +53,35 @@ public static function lookupOrDefault(ScoreLink $scoreLink): static ]); } + public static function scoresAround(ScoreLink $scoreLink): array + { + $placeholder = new static([ + 'score_link_id' => $scoreLink->getKey(), + 'total_score' => $scoreLink->data->totalScore, + ]); + + static $typeOptions = [ + 'higher' => 'score_asc', + 'lower' => 'score_desc', + ]; + + $ret = []; + + foreach ($typeOptions as $type => $sortName) { + $cursorHelper = PlaylistItemUserHighScore::makeDbCursorHelper($sortName); + + $ret[$type] = [ + 'query' => static + ::cursorSort($cursorHelper, $placeholder) + ->where('playlist_item_id', $scoreLink->playlist_item_id) + ->where('user_id', '<>', $scoreLink->user_id), + 'cursorHelper' => $cursorHelper, + ]; + } + + return $ret; + } + public function scoreLink() { return $this->belongsTo(ScoreLink::class); diff --git a/app/Transformers/ScoreTransformer.php b/app/Transformers/ScoreTransformer.php index 8d0dabb0b05..be96db4acce 100644 --- a/app/Transformers/ScoreTransformer.php +++ b/app/Transformers/ScoreTransformer.php @@ -220,38 +220,25 @@ public function includePosition(MultiplayerScoreLink $scoreLink) public function includeScoresAround(MultiplayerScoreLink $scoreLink) { - $limit = 10; - - $highScorePlaceholder = new PlaylistItemUserHighScore([ - 'score_link_id' => $scoreLink->getKey(), - 'total_score' => $scoreLink->data->totalScore, - ]); - - $typeOptions = [ - 'higher' => 'score_asc', - 'lower' => 'score_desc', - ]; - - $ret = []; - - foreach ($typeOptions as $type => $sortName) { - $cursorHelper = PlaylistItemUserHighScore::makeDbCursorHelper($sortName); - [$highScores, $hasMore] = PlaylistItemUserHighScore - ::cursorSort($cursorHelper, $highScorePlaceholder) - ->with(static::MULTIPLAYER_BASE_PRELOAD) - ->where('playlist_item_id', $scoreLink->playlist_item_id) - ->where('user_id', '<>', $scoreLink->user_id) - ->limit($limit) - ->getWithHasMore(); - - $ret[$type] = [ - 'scores' => json_collection($highScores->pluck('scoreLink'), static::newSolo(), static::MULTIPLAYER_BASE_INCLUDES), - 'params' => ['limit' => $limit, 'sort' => $cursorHelper->getSortName()], - ...cursor_for_response($cursorHelper->next($highScores, $hasMore)), - ]; - } - - return $this->primitive($ret); + static $limit = 10; + static $transformer; + $transformer ??= static::newSolo(); + + return $this->primitive(array_map( + function ($item) use ($limit, $transformer) { + [$highScores, $hasMore] = $item['query'] + ->with(static::MULTIPLAYER_BASE_PRELOAD) + ->limit($limit) + ->getWithHasMore(); + + return [ + 'scores' => json_collection($highScores->pluck('scoreLink'), $transformer, static::MULTIPLAYER_BASE_INCLUDES), + 'params' => ['limit' => $limit, 'sort' => $item['cursorHelper']->getSortName()], + ...cursor_for_response($item['cursorHelper']->next($highScores, $hasMore)), + ]; + }, + PlaylistItemUserHighScore::scoresAround($scoreLink), + )); } public function includeRankCountry(ScoreBest|SoloScore $score) diff --git a/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php b/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php index 40283170f0b..8f8f37888f9 100644 --- a/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php +++ b/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php @@ -58,16 +58,43 @@ public function testIndex() public function testShow() { - $scoreLink = ScoreLink::factory()->create(); + $playlist = PlaylistItem::factory()->create(); $user = User::factory()->create(); + $scores = []; + $scores[] = ScoreLink + ::factory() + ->state(['playlist_item_id' => $playlist]) + ->completed([], ['passed' => true, 'total_score' => 30]) + ->create(); + $scores[] = $userScore = ScoreLink + ::factory() + ->state([ + 'playlist_item_id' => $playlist, + 'user_id' => $user, + ])->completed([], ['passed' => true, 'total_score' => 20]) + ->create(); + $scores[] = ScoreLink + ::factory() + ->state(['playlist_item_id' => $playlist]) + ->completed([], ['passed' => true, 'total_score' => 10]) + ->create(); + + foreach ($scores as $score) { + UserScoreAggregate::lookupOrDefault($score->user, $score->room)->recalculate(); + } $this->actAsScopedUser($user, ['*']); - $this->json('GET', route('api.rooms.playlist.scores.show', [ - 'room' => $scoreLink->room_id, - 'playlist' => $scoreLink->playlist_item_id, - 'score' => $scoreLink->getKey(), + $resp = $this->json('GET', route('api.rooms.playlist.scores.show', [ + 'room' => $userScore->room_id, + 'playlist' => $userScore->playlist_item_id, + 'score' => $userScore->getKey(), ]))->assertSuccessful(); + + $json = json_decode($resp->getContent(), true); + $this->assertSame($json['id'], $userScore->getKey()); + $this->assertSame($json['scores_around']['higher']['scores'][0]['id'], $scores[0]->getKey()); + $this->assertSame($json['scores_around']['lower']['scores'][0]['id'], $scores[2]->getKey()); } /** From 4e744e1d0522ce184c87faf4bb57a04372b337ab Mon Sep 17 00:00:00 2001 From: nanaya Date: Thu, 24 Aug 2023 21:58:01 +0900 Subject: [PATCH 052/470] Use static when inside the class itself --- app/Models/Multiplayer/PlaylistItemUserHighScore.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/Multiplayer/PlaylistItemUserHighScore.php b/app/Models/Multiplayer/PlaylistItemUserHighScore.php index 0b3f2a290bf..7e4e3bee354 100644 --- a/app/Models/Multiplayer/PlaylistItemUserHighScore.php +++ b/app/Models/Multiplayer/PlaylistItemUserHighScore.php @@ -68,7 +68,7 @@ public static function scoresAround(ScoreLink $scoreLink): array $ret = []; foreach ($typeOptions as $type => $sortName) { - $cursorHelper = PlaylistItemUserHighScore::makeDbCursorHelper($sortName); + $cursorHelper = static::makeDbCursorHelper($sortName); $ret[$type] = [ 'query' => static From 614b63e508f2aea109fdb09dae828e01217983a6 Mon Sep 17 00:00:00 2001 From: nanaya Date: Thu, 24 Aug 2023 22:03:19 +0900 Subject: [PATCH 053/470] Directly use findOrFail --- .../Controllers/Multiplayer/Rooms/Playlist/ScoresController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php index 570f42496b7..dbc21f4faf3 100644 --- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php +++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php @@ -45,7 +45,7 @@ public function __construct() */ public function index($roomId, $playlistId) { - $playlist = PlaylistItem::where('room_id', $roomId)->where('id', $playlistId)->firstOrFail(); + $playlist = PlaylistItem::where('room_id', $roomId)->findOrFail($playlistId); $params = request()->all(); $limit = clamp(get_int($params['limit'] ?? null) ?? 50, 1, 50); $cursorHelper = PlaylistItemUserHighScore::makeDbCursorHelper($params['sort'] ?? null); From 2f4f88dc40c2c7e9e0b37379360324f466a872a9 Mon Sep 17 00:00:00 2001 From: nanaya Date: Thu, 24 Aug 2023 22:04:47 +0900 Subject: [PATCH 054/470] Use same base query between total and items --- .../Multiplayer/Rooms/Playlist/ScoresController.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php index dbc21f4faf3..f35717b44d4 100644 --- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php +++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php @@ -50,9 +50,10 @@ public function index($roomId, $playlistId) $limit = clamp(get_int($params['limit'] ?? null) ?? 50, 1, 50); $cursorHelper = PlaylistItemUserHighScore::makeDbCursorHelper($params['sort'] ?? null); - [$highScores, $hasMore] = $playlist - ->highScores() - ->whereHas('scoreLink') + $highScoresQuery = $playlist->highScores()->whereHas('scoreLink'); + + [$highScores, $hasMore] = $highScoresQuery + ->clone() ->cursorSort($cursorHelper, cursor_from_params($params)) ->with(ScoreTransformer::MULTIPLAYER_BASE_PRELOAD) ->limit($limit) @@ -64,7 +65,7 @@ public function index($roomId, $playlistId) $transformer, ScoreTransformer::MULTIPLAYER_BASE_INCLUDES ); - $total = $playlist->highScores()->count(); + $total = $highScoresQuery->count(); $user = auth()->user(); From 49295e4249d18eb38cd4b2c3de151980909168d3 Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 30 Aug 2023 21:36:37 +0900 Subject: [PATCH 055/470] Add pinning data for score link --- .../Score/CurrentUserAttributesTransformer.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/Transformers/Score/CurrentUserAttributesTransformer.php b/app/Transformers/Score/CurrentUserAttributesTransformer.php index ced66eb3dbe..8c94a845dd3 100644 --- a/app/Transformers/Score/CurrentUserAttributesTransformer.php +++ b/app/Transformers/Score/CurrentUserAttributesTransformer.php @@ -17,9 +17,15 @@ class CurrentUserAttributesTransformer extends TransformerAbstract { public function transform(LegacyMatch\Score|ScoreModel|SoloScoreInterface $score): array { - $pinnable = $score instanceof ScoreModel - ? $score->best - : ($score instanceof SoloScore ? $score : null); + if ($score instanceof ScoreModel) { + $pinnable = $score->best; + } elseif ($score instanceof SoloScore) { + $pinnable = $score; + } elseif ($score instanceof MultiplayerScoreLink) { + $pinnable = $score->score; + } else { + $pinnable = null; + } return [ 'pin' => $pinnable !== null && $this->isOwnScore($pinnable) From f746a89aa94d33bb560dfc830dc6f3a5401b5cff Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 30 Aug 2023 21:45:31 +0900 Subject: [PATCH 056/470] Remove old migrations --- ..._103744_create_multiplayer_score_links.php | 45 ------------------- ...ore_link_id_to_multiplayer_scores_high.php | 35 --------------- ...core_link_id_to_multiplayer_rooms_high.php | 33 -------------- 3 files changed, 113 deletions(-) delete mode 100644 database/migrations/2023_07_26_103744_create_multiplayer_score_links.php delete mode 100644 database/migrations/2023_08_01_064505_add_score_link_id_to_multiplayer_scores_high.php delete mode 100644 database/migrations/2023_08_01_101614_add_last_score_link_id_to_multiplayer_rooms_high.php diff --git a/database/migrations/2023_07_26_103744_create_multiplayer_score_links.php b/database/migrations/2023_07_26_103744_create_multiplayer_score_links.php deleted file mode 100644 index 56ef5d490cb..00000000000 --- a/database/migrations/2023_07_26_103744_create_multiplayer_score_links.php +++ /dev/null @@ -1,45 +0,0 @@ -. 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\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; - -return new class extends Migration -{ - /** - * Run the migrations. - */ - public function up(): void - { - Schema::create('multiplayer_score_links', function (Blueprint $table) { - $table->id(); - - $table->unsignedInteger('user_id'); - $table->unsignedBigInteger('room_id'); - $table->unsignedBigInteger('playlist_item_id'); - $table->unsignedMediumInteger('beatmap_id'); - $table->unsignedMediumInteger('build_id')->default(0); - $table->unsignedBigInteger('score_id')->nullable(); - - $table->timestampsTz(); - - $table->index('score_id'); - $table->index(['room_id', 'user_id']); - $table->index('playlist_item_id'); - $table->index('user_id'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('multiplayer_score_links'); - } -}; diff --git a/database/migrations/2023_08_01_064505_add_score_link_id_to_multiplayer_scores_high.php b/database/migrations/2023_08_01_064505_add_score_link_id_to_multiplayer_scores_high.php deleted file mode 100644 index 7718661bd61..00000000000 --- a/database/migrations/2023_08_01_064505_add_score_link_id_to_multiplayer_scores_high.php +++ /dev/null @@ -1,35 +0,0 @@ -. 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\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; - -return new class extends Migration -{ - /** - * Run the migrations. - */ - public function up(): void - { - Schema::table('multiplayer_scores_high', function (Blueprint $table) { - $table->unsignedBigInteger('score_link_id')->nullable()->after('score_id'); - $table->index(['playlist_item_id', DB::raw('total_score DESC'), 'score_link_id'], 'top_scores_linked'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('multiplayer_scores_high', function (Blueprint $table) { - $table->dropIndex('top_scores_linked'); - $table->dropColumn('score_link_id'); - }); - } -}; diff --git a/database/migrations/2023_08_01_101614_add_last_score_link_id_to_multiplayer_rooms_high.php b/database/migrations/2023_08_01_101614_add_last_score_link_id_to_multiplayer_rooms_high.php deleted file mode 100644 index 55819991c91..00000000000 --- a/database/migrations/2023_08_01_101614_add_last_score_link_id_to_multiplayer_rooms_high.php +++ /dev/null @@ -1,33 +0,0 @@ -. 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\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; - -return new class extends Migration -{ - /** - * Run the migrations. - */ - public function up(): void - { - Schema::table('multiplayer_rooms_high', function (Blueprint $table) { - $table->unsignedBigInteger('last_score_link_id')->nullable()->after('last_score_id'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('multiplayer_rooms_high', function (Blueprint $table) { - $table->dropColumn('last_score_link_id'); - }); - } -}; From 49a3ba9b9f7eeeaf3d5b9ae04b51d3cce0d7036a Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 30 Aug 2023 22:30:44 +0900 Subject: [PATCH 057/470] It's not plural --- .../Multiplayer/Rooms/Playlist/ScoresController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php index f35717b44d4..974d725152f 100644 --- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php +++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php @@ -111,10 +111,10 @@ public function show($roomId, $playlistId, $id) { $room = Room::find($roomId) ?? abort(404, 'Invalid room id'); $playlistItem = $room->playlist()->find($playlistId) ?? abort(404, 'Invalid playlist id'); - $scoreLinks = $playlistItem->scoreLinks()->findOrFail($id); + $scoreLink = $playlistItem->scoreLinks()->findOrFail($id); return json_item( - $scoreLinks, + $scoreLink, ScoreTransformer::newSolo(), [ ...ScoreTransformer::MULTIPLAYER_BASE_INCLUDES, From 9e2ff95e8cccdd26b495d8fe9ab528d66b1bdc72 Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 30 Aug 2023 22:31:32 +0900 Subject: [PATCH 058/470] It's not (solo) score --- .../Multiplayer/Rooms/Playlist/ScoresController.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php index 974d725152f..7ece79641f2 100644 --- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php +++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php @@ -143,10 +143,10 @@ public function showUser($roomId, $playlistId, $userId) { $room = Room::find($roomId) ?? abort(404, 'Invalid room id'); $playlistItem = $room->playlist()->find($playlistId) ?? abort(404, 'Invalid playlist id'); - $score = $playlistItem->highScores()->where('user_id', $userId)->firstOrFail()->scoreLink ?? abort(404); + $scoreLink = $playlistItem->highScores()->where('user_id', $userId)->firstOrFail()->scoreLink ?? abort(404); return json_item( - $score, + $scoreLink, ScoreTransformer::newSolo(), [ ...ScoreTransformer::MULTIPLAYER_BASE_INCLUDES, @@ -169,9 +169,9 @@ public function store($roomId, $playlistId) $buildId = ClientCheck::findBuild($user, $params)?->getKey() ?? config('osu.client.default_build_id'); - $score = $room->startPlay($user, $playlistItem, $buildId); + $scoreLink = $room->startPlay($user, $playlistItem, $buildId); - return json_item($score, ScoreTransformer::newSolo()); + return json_item($scoreLink, ScoreTransformer::newSolo()); } /** From e4c9d38c868c24ad63f83efb8d70c02be91d364c Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 30 Aug 2023 22:40:11 +0900 Subject: [PATCH 059/470] Update to use ScoreLink --- .../Multiplayer/UserScoreAggregateTransformer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Transformers/Multiplayer/UserScoreAggregateTransformer.php b/app/Transformers/Multiplayer/UserScoreAggregateTransformer.php index 3793790ea93..4068555d27f 100644 --- a/app/Transformers/Multiplayer/UserScoreAggregateTransformer.php +++ b/app/Transformers/Multiplayer/UserScoreAggregateTransformer.php @@ -5,7 +5,7 @@ namespace App\Transformers\Multiplayer; -use App\Models\Multiplayer\Score; +use App\Models\Multiplayer\ScoreLink; use App\Models\Multiplayer\UserScoreAggregate; use App\Transformers\TransformerAbstract; use App\Transformers\UserCompactTransformer; @@ -33,7 +33,7 @@ public function transform(UserScoreAggregate $score) public function includePlaylistItemAttempts(UserScoreAggregate $score) { - $scoreAggs = Score::where([ + $scoreAggs = ScoreLink::where([ 'room_id' => $score->room_id, 'user_id' => $score->user_id, ])->groupBy('playlist_item_id') From d8f401d8c05d746705d0a22d821c4c2b010e204e Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 30 Aug 2023 22:55:01 +0900 Subject: [PATCH 060/470] Result from aggregate must be accessed using raw attribute --- app/Transformers/Multiplayer/UserScoreAggregateTransformer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Transformers/Multiplayer/UserScoreAggregateTransformer.php b/app/Transformers/Multiplayer/UserScoreAggregateTransformer.php index 4068555d27f..37b9acd1a6a 100644 --- a/app/Transformers/Multiplayer/UserScoreAggregateTransformer.php +++ b/app/Transformers/Multiplayer/UserScoreAggregateTransformer.php @@ -44,7 +44,7 @@ public function includePlaylistItemAttempts(UserScoreAggregate $score) foreach ($scoreAggs as $scoreAgg) { $attempts[] = [ - 'attempts' => $scoreAgg->attempts, + 'attempts' => $scoreAgg->getRawAttribute('attempts'), 'id' => $scoreAgg->playlist_item_id, ]; } From 69d00cd2cfcace2a70d150031f1c32fa96899550 Mon Sep 17 00:00:00 2001 From: nanaya Date: Thu, 31 Aug 2023 19:29:12 +0900 Subject: [PATCH 061/470] Add test for rooms.show --- .../Multiplayer/RoomsControllerTest.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Controllers/Multiplayer/RoomsControllerTest.php b/tests/Controllers/Multiplayer/RoomsControllerTest.php index 1bcfde01f86..9ea14171e18 100644 --- a/tests/Controllers/Multiplayer/RoomsControllerTest.php +++ b/tests/Controllers/Multiplayer/RoomsControllerTest.php @@ -10,6 +10,8 @@ use App\Models\Chat\UserChannel; use App\Models\Multiplayer\PlaylistItem; use App\Models\Multiplayer\Room; +use App\Models\Multiplayer\ScoreLink; +use App\Models\Multiplayer\UserScoreAggregate; use App\Models\OAuth\Token; use App\Models\User; use Tests\TestCase; @@ -26,6 +28,25 @@ public function testIndex() $this->json('GET', route('api.rooms.index'))->assertSuccessful(); } + public function testShow() + { + $room = Room::factory()->create(); + $user = User::factory()->create(); + $playlist = PlaylistItem::factory()->create(['room_id' => $room]); + $scoreLink = ScoreLink + ::factory() + ->state([ + 'playlist_item_id' => $playlist, + 'user_id' => $user, + ])->completed([], ['passed' => true, 'total_score' => 20]) + ->create(); + UserScoreAggregate::lookupOrDefault($scoreLink->user, $scoreLink->room)->recalculate(); + + $this->actAsScopedUser($user, ['*']); + + $this->json('GET', route('api.rooms.show', $room))->assertSuccessful(); + } + public function testStore() { $token = Token::factory()->create(['scopes' => ['*']]); From cc54ca360a35ac480bd68d417f858f307b2f8f22 Mon Sep 17 00:00:00 2001 From: nanaya Date: Thu, 31 Aug 2023 19:31:53 +0900 Subject: [PATCH 062/470] More score link variable name adjustment --- .../Rooms/Playlist/ScoresControllerTest.php | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php b/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php index 8f8f37888f9..8f7ea793b10 100644 --- a/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php +++ b/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php @@ -18,27 +18,27 @@ public function testIndex() { $playlist = PlaylistItem::factory()->create(); $user = User::factory()->create(); - $scores = []; - $scores[] = ScoreLink + $scoreLinks = []; + $scoreLinks[] = ScoreLink ::factory() ->state(['playlist_item_id' => $playlist]) ->completed([], ['passed' => true, 'total_score' => 30]) ->create(); - $scores[] = $userScore = ScoreLink + $scoreLinks[] = $userScoreLink = ScoreLink ::factory() ->state([ 'playlist_item_id' => $playlist, 'user_id' => $user, ])->completed([], ['passed' => true, 'total_score' => 20]) ->create(); - $scores[] = ScoreLink + $scoreLinks[] = ScoreLink ::factory() ->state(['playlist_item_id' => $playlist]) ->completed([], ['passed' => true, 'total_score' => 10]) ->create(); - foreach ($scores as $score) { - UserScoreAggregate::lookupOrDefault($score->user, $score->room)->recalculate(); + foreach ($scoreLinks as $scoreLink) { + UserScoreAggregate::lookupOrDefault($scoreLink->user, $scoreLink->room)->recalculate(); } $this->actAsScopedUser($user, ['*']); @@ -49,52 +49,52 @@ public function testIndex() ]))->assertSuccessful(); $json = json_decode($resp->getContent(), true); - $this->assertSame(count($scores), count($json['scores'])); + $this->assertSame(count($scoreLinks), count($json['scores'])); foreach ($json['scores'] as $i => $jsonScore) { - $this->assertSame($scores[$i]->getKey(), $jsonScore['id']); + $this->assertSame($scoreLinks[$i]->getKey(), $jsonScore['id']); } - $this->assertSame($json['user_score']['id'], $userScore->getKey()); + $this->assertSame($json['user_score']['id'], $userScoreLink->getKey()); } public function testShow() { $playlist = PlaylistItem::factory()->create(); $user = User::factory()->create(); - $scores = []; - $scores[] = ScoreLink + $scoreLinks = []; + $scoreLinks[] = ScoreLink ::factory() ->state(['playlist_item_id' => $playlist]) ->completed([], ['passed' => true, 'total_score' => 30]) ->create(); - $scores[] = $userScore = ScoreLink + $scoreLinks[] = $userScoreLink = ScoreLink ::factory() ->state([ 'playlist_item_id' => $playlist, 'user_id' => $user, ])->completed([], ['passed' => true, 'total_score' => 20]) ->create(); - $scores[] = ScoreLink + $scoreLinks[] = ScoreLink ::factory() ->state(['playlist_item_id' => $playlist]) ->completed([], ['passed' => true, 'total_score' => 10]) ->create(); - foreach ($scores as $score) { - UserScoreAggregate::lookupOrDefault($score->user, $score->room)->recalculate(); + foreach ($scoreLinks as $scoreLink) { + UserScoreAggregate::lookupOrDefault($scoreLink->user, $scoreLink->room)->recalculate(); } $this->actAsScopedUser($user, ['*']); $resp = $this->json('GET', route('api.rooms.playlist.scores.show', [ - 'room' => $userScore->room_id, - 'playlist' => $userScore->playlist_item_id, - 'score' => $userScore->getKey(), + 'room' => $userScoreLink->room_id, + 'playlist' => $userScoreLink->playlist_item_id, + 'score' => $userScoreLink->getKey(), ]))->assertSuccessful(); $json = json_decode($resp->getContent(), true); - $this->assertSame($json['id'], $userScore->getKey()); - $this->assertSame($json['scores_around']['higher']['scores'][0]['id'], $scores[0]->getKey()); - $this->assertSame($json['scores_around']['lower']['scores'][0]['id'], $scores[2]->getKey()); + $this->assertSame($json['id'], $userScoreLink->getKey()); + $this->assertSame($json['scores_around']['higher']['scores'][0]['id'], $scoreLinks[0]->getKey()); + $this->assertSame($json['scores_around']['lower']['scores'][0]['id'], $scoreLinks[2]->getKey()); } /** From 38fd39102e49a184fda493b0416bbb6f11bb61e9 Mon Sep 17 00:00:00 2001 From: nanaya Date: Tue, 5 Sep 2023 17:00:43 +0900 Subject: [PATCH 063/470] Sort --- app/Transformers/ScoreTransformer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Transformers/ScoreTransformer.php b/app/Transformers/ScoreTransformer.php index 3c214a36c27..b5ee622040c 100644 --- a/app/Transformers/ScoreTransformer.php +++ b/app/Transformers/ScoreTransformer.php @@ -106,8 +106,8 @@ public function transformSolo(ScoreModel|SoloScoreInterface $score) if ($score instanceof MultiplayerScoreLink) { $multiplayerAttributes = [ - 'room_id' => $score->room_id, 'playlist_item_id' => $score->playlist_item_id, + 'room_id' => $score->room_id, ]; } } From 829dc7383df7d3e0adc6dfeab97baad727e43870 Mon Sep 17 00:00:00 2001 From: nanaya Date: Tue, 5 Sep 2023 17:00:56 +0900 Subject: [PATCH 064/470] Add solo score id for multiplayer score json --- app/Transformers/ScoreTransformer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Transformers/ScoreTransformer.php b/app/Transformers/ScoreTransformer.php index b5ee622040c..ac2b85955e2 100644 --- a/app/Transformers/ScoreTransformer.php +++ b/app/Transformers/ScoreTransformer.php @@ -108,6 +108,7 @@ public function transformSolo(ScoreModel|SoloScoreInterface $score) $multiplayerAttributes = [ 'playlist_item_id' => $score->playlist_item_id, 'room_id' => $score->room_id, + 'solo_score_id' => $score->score_id, ]; } } From 03f19dc4bc08498a31829c2722e8c309d11620e8 Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 12 Sep 2023 15:46:49 -0700 Subject: [PATCH 065/470] Fix state issues on navigation --- resources/js/entrypoints/account-edit.tsx | 4 ++-- resources/js/github-user/index.tsx | 14 ++++++++++---- .../views/accounts/_edit_github_user.blade.php | 5 ++++- resources/views/accounts/edit.blade.php | 6 ------ 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/resources/js/entrypoints/account-edit.tsx b/resources/js/entrypoints/account-edit.tsx index ab9ded7e745..e134e8825d2 100644 --- a/resources/js/entrypoints/account-edit.tsx +++ b/resources/js/entrypoints/account-edit.tsx @@ -21,8 +21,8 @@ core.reactTurbolinks.register('authorized-clients', () => { return ; }); -core.reactTurbolinks.register('github-user', () => ( - +core.reactTurbolinks.register('github-user', (container: HTMLElement) => ( + )); core.reactTurbolinks.register('legacy-api-key', (container: HTMLElement) => ( diff --git a/resources/js/github-user/index.tsx b/resources/js/github-user/index.tsx index 773cdbc7636..ded9a65dded 100644 --- a/resources/js/github-user/index.tsx +++ b/resources/js/github-user/index.tsx @@ -4,31 +4,37 @@ import BigButton from 'components/big-button'; import GithubUserJson from 'interfaces/github-user-json'; import { route } from 'laroute'; -import { action, makeObservable, observable } from 'mobx'; +import { action, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { onErrorWithCallback } from 'utils/ajax'; import { trans } from 'utils/lang'; interface Props { - user: GithubUserJson | null | undefined; + container: HTMLElement; } @observer export default class GithubUser extends React.Component { @observable private deleting = false; - @observable private user: GithubUserJson | null | undefined; + @observable private user: GithubUserJson | null; + private userDatasetSyncDisposer; private xhr?: JQuery.jqXHR; constructor(props: Props) { super(props); - this.user = props.user; + this.user = JSON.parse(this.props.container.dataset.user ?? '') as GithubUserJson | null; + this.userDatasetSyncDisposer = reaction( + () => JSON.stringify(this.user), + (githubUserJson) => this.props.container.dataset.user = githubUserJson, + ); makeObservable(this); } componentWillUnmount() { + this.userDatasetSyncDisposer(); this.xhr?.abort(); } diff --git a/resources/views/accounts/_edit_github_user.blade.php b/resources/views/accounts/_edit_github_user.blade.php index cfddc4a1bd0..16ab5e4fcdc 100644 --- a/resources/views/accounts/_edit_github_user.blade.php +++ b/resources/views/accounts/_edit_github_user.blade.php @@ -16,7 +16,10 @@ {{ osu_trans('accounts.github_user.account')}} diff --git a/resources/views/accounts/edit.blade.php b/resources/views/accounts/edit.blade.php index 3ce0ec548c8..86b06a1a408 100644 --- a/resources/views/accounts/edit.blade.php +++ b/resources/views/accounts/edit.blade.php @@ -162,12 +162,6 @@ class="js-account-edit-avatar__button fileupload" {!! json_encode($authorizedClients) !!} - @if (\App\Models\GithubUser::canAuthenticate()) - - @endif - From a1464947ae683d29b3c3cc93628561e3737f890e Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 12 Sep 2023 15:46:49 -0700 Subject: [PATCH 066/470] Continue xhr instead of aborting, remove redundant `deleting` state --- resources/js/github-user/index.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/resources/js/github-user/index.tsx b/resources/js/github-user/index.tsx index ded9a65dded..2ebc7116167 100644 --- a/resources/js/github-user/index.tsx +++ b/resources/js/github-user/index.tsx @@ -16,10 +16,9 @@ interface Props { @observer export default class GithubUser extends React.Component { - @observable private deleting = false; @observable private user: GithubUserJson | null; private userDatasetSyncDisposer; - private xhr?: JQuery.jqXHR; + @observable private xhr: JQuery.jqXHR | null = null; constructor(props: Props) { super(props); @@ -51,7 +50,7 @@ export default class GithubUser extends React.Component { { @action private onDeleteButtonClick = () => { - this.xhr?.abort(); - this.deleting = true; + if (this.xhr != null) return; this.xhr = $.ajax( route('account.github-users.destroy', { github_user: this.user?.id }), @@ -79,6 +77,6 @@ export default class GithubUser extends React.Component { ) .done(() => this.user = null) .fail(onErrorWithCallback(this.onDeleteButtonClick)) - .always(action(() => this.deleting = false)); + .always(action(() => this.xhr = null)); }; } From 179ebed3d12ee14d3a8beac4c924bae202b3024f Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 12 Sep 2023 15:46:49 -0700 Subject: [PATCH 067/470] Missing action --- resources/js/github-user/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/github-user/index.tsx b/resources/js/github-user/index.tsx index 2ebc7116167..b5b30f52b43 100644 --- a/resources/js/github-user/index.tsx +++ b/resources/js/github-user/index.tsx @@ -75,7 +75,7 @@ export default class GithubUser extends React.Component { route('account.github-users.destroy', { github_user: this.user?.id }), { method: 'DELETE' }, ) - .done(() => this.user = null) + .done(action(() => this.user = null)) .fail(onErrorWithCallback(this.onDeleteButtonClick)) .always(action(() => this.xhr = null)); }; From eeaf389ba1a28a4a78eaccf475ec513d9cebc1da Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 12 Sep 2023 15:46:49 -0700 Subject: [PATCH 068/470] Fix div id removed by accident --- resources/views/accounts/_edit_github_user.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/accounts/_edit_github_user.blade.php b/resources/views/accounts/_edit_github_user.blade.php index 16ab5e4fcdc..a28b73a9543 100644 --- a/resources/views/accounts/_edit_github_user.blade.php +++ b/resources/views/accounts/_edit_github_user.blade.php @@ -2,7 +2,7 @@ Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. See the LICENCE file in the repository root for full licence text. --}} -