Skip to content

Commit

Permalink
add seasonal leaderboards
Browse files Browse the repository at this point in the history
  • Loading branch information
venix12 committed Jan 20, 2025
1 parent a551a10 commit c6ce540
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 8 deletions.
12 changes: 11 additions & 1 deletion app/Http/Controllers/SeasonsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,17 @@ public function show($id)
$seasons = Season::orderByDesc('id')->get();
$seasonsJson = json_collection($seasons, new SelectOptionTransformer());

return ext_view('seasons.show', compact('roomsJson', 'season', 'seasonJson', 'seasonsJson'));
$divisions = $season->divisionsWithAbsoluteThresholds();
$scores = $season->topScores()->paginate();

return ext_view('seasons.show', compact(
'divisions',
'roomsJson',
'scores',
'season',
'seasonJson',
'seasonsJson',
));
}

private function paramsForResponse(int $seasonId, ?array $rawParams = null)
Expand Down
20 changes: 20 additions & 0 deletions app/Models/Division.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

// Copyright (c) ppy Pty Ltd <[email protected]>. 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;

/**
* @property int $id
* @property string $image_url
* @property string $name
* @property int $season_id
* @property int $threshold
*/
class Division extends Model
{
public $timestamps = false;
}
41 changes: 41 additions & 0 deletions app/Models/Season.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Carbon\Carbon;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;

/**
* @property bool $finalised
Expand Down Expand Up @@ -45,6 +46,36 @@ public static function latestOrId($id)
return $season;
}

public function divisions(): HasMany
{
return $this->hasMany(Division::class);
}

public function divisionsOrdered(): array
{
return cache_remember_mutexed(
"divisions:{$this->id}",
$GLOBALS['cfg']['osu']['seasons']['divisions_cache_duration'],
[],
fn () => $this->divisions()->orderBy('threshold')->get(),
);
}

public function divisionsWithAbsoluteThresholds(): array
{
$divisions = [];
$userCount = $this->userScores()->count();

foreach ($this->divisionsOrdered() as $division) {
$divisions[] = [
'division' => $division,
'absolute_threshold' => $division->threshold * $userCount,
];
}

return $divisions;
}

public function endDate(): ?Carbon
{
return $this->finalised
Expand All @@ -57,8 +88,18 @@ public function startDate(): ?Carbon
return $this->rooms->min('starts_at');
}

public function topScores(): HasMany
{
return $this->userScores()->forRanking()->with(['user.country', 'user.team']);
}

public function rooms(): BelongsToMany
{
return $this->belongsToMany(Multiplayer\Room::class, SeasonRoom::class);
}

public function userScores(): HasMany
{
return $this->hasMany(UserSeasonScoreAggregate::class);
}
}
16 changes: 16 additions & 0 deletions app/Models/UserSeasonScoreAggregate.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use App\Exceptions\InvariantException;
use App\Models\Multiplayer\UserScoreAggregate;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

/**
Expand Down Expand Up @@ -72,8 +73,23 @@ public function calculate(bool $muteExceptions = true): void
$this->total_score = $total;
}

public function scopeForRanking($query): Builder
{
return $query
->whereHas('user', function ($userQuery) {
$userQuery->default();
})
->orderByDesc('total_score');
}


public function season(): BelongsTo
{
return $this->belongsTo(Season::class);
}

public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
}
8 changes: 3 additions & 5 deletions config/osu.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
'achievement' => [
'icon_prefix' => env('USER_ACHIEVEMENT_ICON_PREFIX', 'https://assets.ppy.sh/user-achievements/'),
],

'api' => [
// changing the throttle rate doesn't reset any existing timers,
// changing the prefix key is the only way to invalidate them.
Expand All @@ -27,15 +26,13 @@
'scores_download' => env('API_THROTTLE_SCORES_DOWNLOAD', '10,1,api-scores-download'),
],
],

'avatar' => [
'cache_purge_prefix' => env('AVATAR_CACHE_PURGE_PREFIX'),
'cache_purge_method' => env('AVATAR_CACHE_PURGE_METHOD'),
'cache_purge_authorization_key' => env('AVATAR_CACHE_PURGE_AUTHORIZATION_KEY'),
'default' => env('DEFAULT_AVATAR', env('APP_URL', 'http://localhost').'/images/layout/[email protected]'),
'storage' => env('AVATAR_STORAGE', 'local-avatar'),
],

'bbcode' => [
// this should be random or a config variable.
// ...who am I kidding, this shouldn't even exist at all.
Expand Down Expand Up @@ -193,12 +190,13 @@
'processing_queue' => presence(env('SCORES_PROCESSING_QUEUE')) ?? 'osu-queue:score-statistics',
'submission_enabled' => get_bool(env('SCORES_SUBMISSION_ENABLED')) ?? true,
],

'seasonal' => [
'contest_id' => get_int(env('SEASONAL_CONTEST_ID')),
'ends_at' => env('SEASONAL_ENDS_AT'),
],

'seasons' => [
'divisions_cache_duration' => 60 * (get_float(env('DIVISIONS_CACHE_DURATION')) ?? 60), // in minutes, converted to seconds
],
'store' => [
'notice' => presence(str_replace('\n', "\n", env('STORE_NOTICE') ?? '')),
],
Expand Down
37 changes: 37 additions & 0 deletions database/migrations/2025_01_06_203319_create_divisions_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

// Copyright (c) ppy Pty Ltd <[email protected]>. 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('divisions', function (Blueprint $table) {
$table->id();
$table->integer('season_id')->unsigned();
$table->string('name');
$table->string('image_url');
$table->float('threshold');

$table->index(['season_id', 'threshold']);
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('divisions');
}
};
1 change: 1 addition & 0 deletions resources/lang/en/rankings.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
'accuracy' => 'Accuracy',
'active_users' => 'Active Users',
'country' => 'Country',
'division' => 'Division',
'play_count' => 'Play Count',
'performance' => 'Performance',
'total_score' => 'Total Score',
Expand Down
74 changes: 74 additions & 0 deletions resources/views/seasons/_rankings_table.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{{--
Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the GNU Affero General Public License v3.0.
See the LICENCE file in the repository root for full licence text.
--}}

@php
$currentDivision = $divisions[0];
@endphp

<table class="ranking-page-table">
<thead>
<tr>
<th class="ranking-page-table__heading"></th>
<th class="ranking-page-table__heading ranking-page-table__heading--main"></th>
<th class="ranking-page-table__heading">
{{ osu_trans('rankings.stat.total_score') }}
</th>
<th class="ranking-page-table__heading ranking-page-table__heading--focused">
{{ osu_trans('rankings.stat.division') }}
</th>
</tr>
</thead>

<tbody>
@foreach ($scores as $index => $score)
@php
$rank = $scores->firstItem() + $index;
if ($rank > $currentDivision['absolute_threshold']) {
foreach ($divisions as $division) {
if ($rank <= $division['absolute_threshold']) {
$currentDivision = $division;
break;
}
}
}
@endphp

<tr class="ranking-page-table__row{{$score->user->isActive() ? '' : ' ranking-page-table__row--inactive'}}">
<td class="ranking-page-table__column ranking-page-table__column--rank">
#{{ $rank }}
</td>
<td class="ranking-page-table__column">
<div class="ranking-page-table__user-link">
<span class="ranking-page-table__flags">
@include('objects._flag_country', [
'country' => $score->user->country,
])
@if (($team = $score->user->team) !== null)
<a class="u-contents" href="{{ route('teams.show', $team) }}">
@include('objects._flag_team', compact('team'))
</a>
@endif
</span>
<a
href="{{ route('users.show', ['user' => $score->user_id]) }}"
class="ranking-page-table__user-link-text js-usercard"
data-user-id="{{ $score->user_id }}"
data-tooltip-position="right center"
>
{{ $score->user->username }}
</a>
</div>
</td>
<td class="ranking-page-table__column ranking-page-table__column--dimmed">
{!! i18n_number_format($score->total_score) !!}
</td>
<td class="ranking-page-table__column ranking-page-table__column--division">
{{ $currentDivision['division']->name }}
</td>
</tr>
@endforeach
</tbody>
</table>
10 changes: 8 additions & 2 deletions resources/views/seasons/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
'country' => null,
'hasFilter' => false,
'hasMode' => false,
'hasPager' => false,
'hasScores' => false,
'hasPager' => true,
'hasScores' => true,
'spotlight' => null,
'titlePrepend' => osu_trans('rankings.type.seasons').': '.$season->name,
'type' => 'seasons',
Expand All @@ -34,6 +34,12 @@ class="btn-osu-big btn-osu-big--rounded-thin"
<div class="js-react--seasons-show"></div>
@endsection

@if (!empty($divisions))
@section('scores')
@include('seasons._rankings_table', [$divisions, $scores])
@endsection
@endif

@section ("script")
@parent

Expand Down

0 comments on commit c6ce540

Please sign in to comment.