Skip to content

Commit

Permalink
Merge pull request #11377 from notbakaneko/feature/beatmaps-multiple-…
Browse files Browse the repository at this point in the history
…mappers

Support multiple mappers on beatmap difficulties
  • Loading branch information
nanaya authored Dec 5, 2024
2 parents ccf3fa8 + 454d9fb commit a2fe0c5
Show file tree
Hide file tree
Showing 59 changed files with 993 additions and 445 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ CLIENT_CHECK_VERSION=false
# SEARCH_MINIMUM_LENGTH=2

# BEATMAPS_DIFFICULTY_CACHE_SERVER_URL=http://localhost:5001
# BEATMAPS_OWNERS_MAX=10
# BEATMAPSET_DISCUSSION_KUDOSU_PER_USER=10
# BEATMAPSET_GUEST_ADVANCED_SEARCH=0
# BEATMAPSET_MAXIMUM_DISQUALIFIED_RANK_PENALTY_DAYS=7
Expand Down
31 changes: 31 additions & 0 deletions app/Console/Commands/BeatmapsMigrateOwners.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?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.

namespace App\Console\Commands;

use App\Models\Beatmap;
use Illuminate\Console\Command;

class BeatmapsMigrateOwners extends Command
{
protected $signature = 'beatmaps:migrate-owners';

protected $description = 'Migrates beatmap owners to new table.';

public function handle()
{
$progress = $this->output->createProgressBar();

Beatmap::chunkById(1000, function ($beatmaps) use ($progress) {
foreach ($beatmaps as $beatmap) {
$beatmap->beatmapOwners()->firstOrCreate(['user_id' => $beatmap->user_id]);
$progress->advance();
}
});

$progress->finish();
$this->line('');
}
}
26 changes: 4 additions & 22 deletions app/Http/Controllers/BeatmapsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@

use App\Enums\Ruleset;
use App\Exceptions\InvariantException;
use App\Jobs\Notifications\BeatmapOwnerChange;
use App\Libraries\BeatmapDifficultyAttributes;
use App\Libraries\Beatmapset\ChangeBeatmapOwners;
use App\Libraries\Score\BeatmapScores;
use App\Libraries\Score\UserRank;
use App\Libraries\Search\ScoreSearch;
use App\Libraries\Search\ScoreSearchParams;
use App\Models\Beatmap;
use App\Models\BeatmapsetEvent;
use App\Models\User;
use App\Transformers\BeatmapTransformer;
use App\Transformers\ScoreTransformer;
Expand Down Expand Up @@ -381,26 +380,9 @@ public function soloScores($id)
public function updateOwner($id)
{
$beatmap = Beatmap::findOrFail($id);
$currentUser = auth()->user();
$newUserIds = get_arr(request('user_ids'), 'get_int');

priv_check('BeatmapUpdateOwner', $beatmap->beatmapset)->ensureCan();

$newUserId = get_int(request('beatmap.user_id'));

$beatmap->getConnection()->transaction(function () use ($beatmap, $currentUser, $newUserId) {
$beatmap->setOwner($newUserId);

BeatmapsetEvent::log(BeatmapsetEvent::BEATMAP_OWNER_CHANGE, $currentUser, $beatmap->beatmapset, [
'beatmap_id' => $beatmap->getKey(),
'beatmap_version' => $beatmap->version,
'new_user_id' => $beatmap->user_id,
'new_user_username' => $beatmap->user->username,
])->saveOrExplode();
});

if ($beatmap->user_id !== $currentUser->getKey()) {
(new BeatmapOwnerChange($beatmap, $currentUser))->dispatch();
}
(new ChangeBeatmapOwners($beatmap, $newUserIds ?? [], \Auth::user()))->handle();

return $beatmap->beatmapset->defaultDiscussionJson();
}
Expand Down Expand Up @@ -459,7 +441,7 @@ public function userScore($beatmapId, $userId)
'score' => json_item(
$score,
new ScoreTransformer(),
['beatmap', ...static::DEFAULT_SCORE_INCLUDES]
['beatmap.owners', ...static::DEFAULT_SCORE_INCLUDES]
),
];
}
Expand Down
3 changes: 3 additions & 0 deletions app/Http/Controllers/BeatmapsetsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ private function showJson($beatmapset)
"{$beatmapRelation}.baseDifficultyRatings",
"{$beatmapRelation}.baseMaxCombo",
"{$beatmapRelation}.failtimes",
"{$beatmapRelation}.beatmapOwners.user",
'genre',
'language',
'user',
Expand All @@ -402,8 +403,10 @@ private function showJson($beatmapset)
'beatmaps',
'beatmaps.failtimes',
'beatmaps.max_combo',
'beatmaps.owners',
'converts',
'converts.failtimes',
'converts.owners',
'current_nominations',
'current_user_attributes',
'description',
Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/ScoresController.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public function show($rulesetOrSoloId, $legacyId = null)
$scoreJson = json_item($score, new ScoreTransformer(), array_merge([
'beatmap.max_combo',
'beatmap.user',
'beatmap.owners',
'beatmapset',
'rank_global',
], $userIncludes));
Expand Down
80 changes: 80 additions & 0 deletions app/Libraries/Beatmapset/ChangeBeatmapOwners.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?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\Libraries\Beatmapset;

use App\Exceptions\InvariantException;
use App\Jobs\Notifications\BeatmapOwnerChange;
use App\Models\Beatmap;
use App\Models\BeatmapOwner;
use App\Models\BeatmapsetEvent;
use App\Models\User;
use Ds\Set;

class ChangeBeatmapOwners
{
private Set $userIds;

public function __construct(private Beatmap $beatmap, array $newUserIds, private User $source)
{
priv_check_user($source, 'BeatmapUpdateOwner', $beatmap->beatmapset)->ensureCan();

$this->userIds = new Set($newUserIds);

if ($this->userIds->count() > $GLOBALS['cfg']['osu']['beatmaps']['owners_max']) {
throw new InvariantException(osu_trans('beatmaps.change_owner.too_many'));
}

if ($this->userIds->isEmpty()) {
throw new InvariantException('user_ids must be specified');
}
}

public function handle(): void
{
$currentOwners = new Set($this->beatmap->getOwners()->pluck('user_id'));
if ($currentOwners->xor($this->userIds)->isEmpty()) {
return;
}

$newUserIds = $this->userIds->diff($currentOwners);

if (User::whereIn('user_id', $newUserIds->toArray())->default()->count() !== $newUserIds->count()) {
throw new InvariantException('invalid user_id');
}

$this->beatmap->getConnection()->transaction(function () {
$params = array_map(
fn ($userId) => ['beatmap_id' => $this->beatmap->getKey(), 'user_id' => $userId],
$this->userIds->toArray()
);

$this->beatmap->fill(['user_id' => $this->userIds->first()])->saveOrExplode();
$this->beatmap->beatmapOwners()->delete();
BeatmapOwner::insert($params);

$this->beatmap->refresh();

$newUsers = $this->beatmap->getOwners()->select('id', 'username')->all();
$beatmapset = $this->beatmap->beatmapset;
$firstMapper = $newUsers[0];

BeatmapsetEvent::log(BeatmapsetEvent::BEATMAP_OWNER_CHANGE, $this->source, $beatmapset, [
'beatmap_id' => $this->beatmap->getKey(),
'beatmap_version' => $this->beatmap->version,
// TODO: mainly for compatibility during dev when switching branches, can be removed after deployed.
'new_user_id' => $firstMapper['id'],
'new_user_username' => $firstMapper['username'],
'new_users' => $newUsers,
])->saveOrExplode();

$beatmapset->update(['eligible_main_rulesets' => null]);
});

(new BeatmapOwnerChange($this->beatmap, $this->source))->dispatch();
}
}
64 changes: 42 additions & 22 deletions app/Models/Beatmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
use App\Libraries\Transactions\AfterCommit;
use DB;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
* @property int $approved
* @property \Illuminate\Database\Eloquent\Collection $beatmapDiscussions BeatmapDiscussion
* @property-read Collection<BeatmapDiscussion> $beatmapDiscussions
* @property-read Collection<BeatmapOwner> $beatmapOwners
* @property int $beatmap_id
* @property Beatmapset $beatmapset
* @property int|null $beatmapset_id
Expand All @@ -29,20 +31,22 @@
* @property float $diff_drain
* @property float $diff_overall
* @property float $diff_size
* @property \Illuminate\Database\Eloquent\Collection $difficulty BeatmapDifficulty
* @property \Illuminate\Database\Eloquent\Collection $difficultyAttribs BeatmapDifficultyAttrib
* @property-read Collection<BeatmapDifficulty> $difficulty
* @property-read Collection<BeatmapDifficultyAttrib> $difficultyAttribs
* @property float $difficultyrating
* @property \Illuminate\Database\Eloquent\Collection $failtimes BeatmapFailtimes
* @property-read Collection<BeatmapFailtimes> $failtimes
* @property string|null $filename
* @property int $hit_length
* @property \Carbon\Carbon $last_update
* @property int $max_combo
* @property mixed $mode
* @property-read Collection<User> $owners
* @property int $passcount
* @property int $playcount
* @property int $playmode
* @property int $score_version
* @property int $total_length
* @property User $user
* @property int $user_id
* @property string $version
* @property string|null $youtube_preview
Expand Down Expand Up @@ -107,6 +111,11 @@ public function baseMaxCombo()
return $this->difficultyAttribs()->noMods()->maxCombo();
}

public function beatmapOwners()
{
return $this->hasMany(BeatmapOwner::class);
}

public function beatmapset()
{
return $this->belongsTo(Beatmapset::class, 'beatmapset_id')->withTrashed();
Expand Down Expand Up @@ -267,6 +276,7 @@ public function getAttribute($key)
'baseDifficultyRatings',
'baseMaxCombo',
'beatmapDiscussions',
'beatmapOwners',
'beatmapset',
'difficulty',
'difficultyAttribs',
Expand All @@ -279,6 +289,34 @@ public function getAttribute($key)
};
}

/**
* @return Collection<User>
*/
public function getOwners(): Collection
{
$owners = $this->beatmapOwners->loadMissing('user')->map(
fn ($beatmapOwner) => $beatmapOwner->user ?? new DeletedUser(['user_id' => $beatmapOwner->user_id])
);

// TODO: remove when everything writes to beatmap_owners.
if (!$owners->contains(fn ($beatmapOwner) => $beatmapOwner->user_id === $this->user_id)) {
$owners->prepend($this->user ?? new DeletedUser(['user_id' => $this->user_id]));
}

return $owners;
}

public function isOwner(User $user): bool
{
if ($this->user_id === $user->getKey()) {
return true;
}

return $this->relationLoaded('beatmapOwners')
? $this->beatmapOwners->contains('user_id', $user->getKey())
: $this->beatmapOwners()->where('user_id', $user->getKey())->exists();
}

public function maxCombo()
{
if (!$this->convert) {
Expand All @@ -305,24 +343,6 @@ public function maxCombo()
return $maxCombo?->value;
}

public function setOwner($newUserId)
{
if ($newUserId === null) {
throw new InvariantException('user_id must be specified');
}

if (User::find($newUserId) === null) {
throw new InvariantException('invalid user_id');
}

if ($newUserId === $this->user_id) {
throw new InvariantException('the specified user_id is already the owner');
}

$this->fill(['user_id' => $newUserId])->saveOrExplode();
$this->beatmapset->update(['eligible_main_rulesets' => null]);
}

public function status()
{
return array_search($this->approved, Beatmapset::STATES, true);
Expand Down
6 changes: 2 additions & 4 deletions app/Models/BeatmapDiscussion.php
Original file line number Diff line number Diff line change
Expand Up @@ -632,10 +632,8 @@ public function denyKudosu($deniedBy)

public function managedBy(User $user): bool
{
$id = $user->getKey();

return $this->beatmapset->user_id === $id
|| ($this->beatmap !== null && $this->beatmap->user_id === $id);
return $this->beatmapset->user_id === $user->getKey()
|| ($this->beatmap !== null && $this->beatmap->isOwner($user));
}

public function userRecentVotesCount($user, $increment = false)
Expand Down
31 changes: 31 additions & 0 deletions app/Models/BeatmapOwner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?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.

namespace App\Models;

/**
* @property Beatmap $beatmap
* @property int $beatmap_id
* @property User $user
* @property int $user_id
*/
class BeatmapOwner extends Model
{
public $incrementing = false;
public $timestamps = false;

protected $primaryKey = ':composite';
protected $primaryKeys = ['beatmap_id', 'user_id'];

public function beatmap()
{
return $this->belongsTo(Beatmap::class, 'beatmap_id');
}

public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}
Loading

0 comments on commit a2fe0c5

Please sign in to comment.