Skip to content

Commit 0fa80d7

Browse files
authored
Merge branch 'master' into mp-history-ts/match-event
2 parents a3a8acb + 16a0028 commit 0fa80d7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+983
-951
lines changed

app/Http/Controllers/BeatmapsController.php

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ private static function beatmapScores(string $id, ?string $scoreTransformerType,
7979
$scoreTransformer = new ScoreTransformer($scoreTransformerType);
8080

8181
$results = [
82+
'score_count' => UserRank::getCount($esFetch->baseParams),
8283
'scores' => json_collection(
8384
$scores,
8485
$scoreTransformer,

app/Http/Controllers/BeatmapsetsController.php

+2
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ public function show($id)
112112
}
113113

114114
/**
115+
* Search Beatmapset
116+
*
115117
* TODO: documentation
116118
*
117119
* @usesCursor

app/Jobs/EsDocument.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class EsDocument implements ShouldQueue
1717
{
1818
use Dispatchable, InteractsWithQueue, Queueable;
1919

20-
private array $modelMeta;
20+
protected array $modelMeta;
2121

2222
/**
2323
* Create a new job instance.

app/Jobs/EsDocumentUnique.php

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4+
// See the LICENCE file in the repository root for full licence text.
5+
6+
declare(strict_types=1);
7+
8+
namespace App\Jobs;
9+
10+
use Illuminate\Contracts\Queue\ShouldBeUnique;
11+
12+
class EsDocumentUnique extends EsDocument implements ShouldBeUnique
13+
{
14+
public int $uniqueFor = 600;
15+
16+
public function uniqueId(): string
17+
{
18+
return "{$this->modelMeta['class']}-{$this->modelMeta['id']}";
19+
}
20+
}

app/Libraries/Score/BeatmapScores.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414

1515
class BeatmapScores
1616
{
17+
public ScoreSearchParams $baseParams;
1718
public array $result;
18-
private ScoreSearchParams $baseParams;
1919

2020
public function __construct(private array $rawParams)
2121
{
@@ -48,7 +48,6 @@ public function rank(SoloScore $score): int
4848

4949
$params = clone $this->baseParams;
5050
$params->beforeScore = $score;
51-
$params->setSort(null);
5251

5352
return UserRank::getRank($params);
5453
}

app/Libraries/Score/UserRank.php

+12-6
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,9 @@
1111

1212
class UserRank
1313
{
14-
public static function getRank(ScoreSearchParams $params): int
14+
public static function getCount(ScoreSearchParams $params): int
1515
{
16-
if ($params->beforeTotalScore === null && $params->beforeScore === null) {
17-
throw new InvariantException('beforeScore or beforeTotalScore must be specified');
18-
}
19-
16+
$params->setSort(null);
2017
$search = new ScoreSearch($params);
2118

2219
$search->size(0);
@@ -27,6 +24,15 @@ public static function getRank(ScoreSearchParams $params): int
2724
$response = $search->response();
2825
$search->assertNoError();
2926

30-
return 1 + $response->aggregations($aggName)['value'];
27+
return $response->aggregations($aggName)['value'];
28+
}
29+
30+
public static function getRank(ScoreSearchParams $params): int
31+
{
32+
if ($params->beforeTotalScore === null && $params->beforeScore === null) {
33+
throw new InvariantException('beforeScore or beforeTotalScore must be specified');
34+
}
35+
36+
return 1 + static::getCount($params);
3137
}
3238
}

app/Libraries/Search/BeatmapsetQueryParser.php

+6-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public static function parse(?string $query): array
1515
$options = [];
1616

1717
// reference: https://github.com/ppy/osu/blob/f6baf49ad6b42c662a729ad05e18bd99bc48b4c7/osu.Game/Screens/Select/FilterQueryParser.cs
18-
$keywords = preg_replace_callback('#\b(?<key>\w+)(?<op>(:|=|(>|<)(:|=)?))(?<value>(".*")|(\S*))#i', function ($m) use (&$options) {
18+
// adjusted for multiple quoted options (with side effect of inner quotes must be escaped)
19+
$keywords = preg_replace_callback('#\b(?<key>\w+)(?<op>(:|=|(>|<)(:|=)?))(?<value>("{1,2})(?:\\\"|.)*?\7|\S*)#i', function ($m) use (&$options) {
1920
$key = strtolower($m['key']);
2021
$op = str_replace(':', '=', $m['op']);
2122
switch ($key) {
@@ -82,6 +83,9 @@ public static function parse(?string $query): array
8283
case 'source':
8384
$option = static::makeTextOption($op, $m['value']);
8485
break;
86+
case 'tag':
87+
$option = [static::makeTextOption($op, $m['value'])];
88+
break;
8589
case 'title':
8690
$option = static::makeTextOption($op, $m['value']);
8791
break;
@@ -237,7 +241,7 @@ private static function makeIntRangeOption($operator, $value)
237241
private static function makeTextOption(string $operator, string $value): ?string
238242
{
239243
return $operator === '='
240-
? presence(preg_replace('/^"(.*)"$/', '$1', $value))
244+
? presence(strtr(preg_replace('/^"(.*)"$/', '$1', $value), ['\\"' => '"']))
241245
: null;
242246
}
243247

app/Libraries/Search/BeatmapsetSearch.php

+27
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use App\Models\Beatmapset;
1515
use App\Models\Follow;
1616
use App\Models\Solo;
17+
use App\Models\Tag;
1718
use App\Models\User;
1819
use Ds\Set;
1920

@@ -60,6 +61,12 @@ public function getQuery()
6061
->should(['term' => ['_id' => ['value' => $this->params->queryString, 'boost' => 100]]])
6162
->should(QueryHelper::queryString($this->params->queryString, $partialMatchFields, 'or', 1 / count($terms)))
6263
->should(QueryHelper::queryString($this->params->queryString, [], 'and'))
64+
->should([
65+
'nested' => [
66+
'path' => 'beatmaps',
67+
'query' => QueryHelper::queryString($this->params->queryString, ['beatmaps.top_tags'], 'or', 0.5 / count($terms)),
68+
],
69+
])
6370
);
6471
}
6572

@@ -82,6 +89,7 @@ public function getQuery()
8289
$this->addPlayedFilter($query, $nested);
8390
$this->addRankFilter($nested);
8491
$this->addRecommendedFilter($nested);
92+
$this->addTagsFilter($nested);
8593

8694
$this->addSimpleFilters($query, $nested);
8795
$this->addCreatorFilter($query, $nested);
@@ -398,6 +406,25 @@ private function addTextFilter(BoolQuery $query, string $paramField, array $fiel
398406
$query->must($subQuery);
399407
}
400408

409+
private function addTagsFilter(BoolQuery $query): void
410+
{
411+
if ($this->params->tags === null) {
412+
return;
413+
}
414+
415+
$tagSet = new Set(array_map('mb_strtolower', $this->params->tags));
416+
$tags = Tag::whereIn('name', $this->params->tags)->limit(10)->pluck('name');
417+
$tagSet->remove(...$tags->map(fn ($name) => mb_strtolower($name))->toArray());
418+
419+
foreach ($tagSet as $tag) {
420+
$query->filter(QueryHelper::queryString($tag, ['beatmaps.top_tags'], 'and'));
421+
}
422+
423+
foreach ($tags as $tag) {
424+
$query->filter(['term' => ['beatmaps.top_tags.raw' => $tag]]);
425+
}
426+
}
427+
401428
private function getPlayedBeatmapIds(?array $rank = null)
402429
{
403430
$query = Solo\Score

app/Libraries/Search/BeatmapsetSearchParams.php

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class BeatmapsetSearchParams extends SearchParams
4545
public bool $showSpotlights = false;
4646
public ?string $source = null;
4747
public ?string $status = null;
48+
public ?array $tags = null;
4849
public ?string $title = null;
4950
public ?array $statusRange = null;
5051
public ?array $totalLength = null;

app/Libraries/Search/BeatmapsetSearchRequestParams.php

+1
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ private function parseQuery(): void
226226
'source' => 'source',
227227
'stars' => 'difficultyRating',
228228
'status' => 'statusRange',
229+
'tag' => 'tags',
229230
'title' => 'title',
230231
'updated' => 'updated',
231232
];

app/Models/Beatmap.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
namespace App\Models;
77

88
use App\Exceptions\InvariantException;
9-
use App\Jobs\EsDocument;
9+
use App\Jobs\EsDocumentUnique;
1010
use App\Libraries\Transactions\AfterCommit;
1111
use App\Traits\Memoizes;
1212
use Illuminate\Database\Eloquent\Builder;
@@ -247,7 +247,7 @@ public function afterCommit()
247247
$beatmapset = $this->beatmapset;
248248

249249
if ($beatmapset !== null) {
250-
dispatch(new EsDocument($beatmapset));
250+
dispatch(new EsDocumentUnique($beatmapset));
251251
}
252252
}
253253

app/Models/Beatmapset.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use App\Exceptions\ImageProcessorServiceException;
1111
use App\Exceptions\InvariantException;
1212
use App\Jobs\CheckBeatmapsetCovers;
13-
use App\Jobs\EsDocument;
13+
use App\Jobs\EsDocumentUnique;
1414
use App\Jobs\Notifications\BeatmapsetDiscussionLock;
1515
use App\Jobs\Notifications\BeatmapsetDiscussionUnlock;
1616
use App\Jobs\Notifications\BeatmapsetDisqualify;
@@ -1508,7 +1508,7 @@ public function refreshCache(bool $resetEligibleMainRulesets = false): void
15081508

15091509
public function afterCommit()
15101510
{
1511-
dispatch(new EsDocument($this));
1511+
dispatch(new EsDocumentUnique($this));
15121512
}
15131513

15141514
public function notificationCover()

app/Models/Multiplayer/PlaylistItemUserHighScore.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@ public function updateWithScoreLink(ScoreLink $scoreLink): bool
127127
{
128128
$score = $scoreLink->score;
129129

130-
if ($score === null || !$score->passed || $score->total_score <= $this->total_score) {
130+
if ($score === null || $score->total_score <= $this->total_score) {
131+
return false;
132+
}
133+
134+
if (!$score->passed && !$scoreLink->playlistItem->room->isRealtime()) {
131135
return false;
132136
}
133137

app/Models/Multiplayer/Room.php

+4
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,10 @@ public function completePlay(ScoreToken $scoreToken, array $params): ScoreLink
460460
$this->assertValidCompletePlay();
461461

462462
return $this->getConnection()->transaction(function () use ($params, $scoreToken) {
463+
$playlistItem = $scoreToken->playlistItem()->firstOrFail();
464+
$playlistItem->setRelation('room', $this);
465+
$scoreToken->setRelation('playlistItem', $playlistItem);
466+
463467
$scoreLink = ScoreLink::complete($scoreToken, $params);
464468
$user = $scoreLink->user;
465469
$agg = UserScoreAggregate::new($user, $this);

app/Models/Multiplayer/ScoreLink.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public static function complete(ScoreToken $token, array $params): static
3131
// multiplayer scores are always preserved.
3232
$score = Score::createFromJsonOrExplode([...$params, 'preserve' => true]);
3333

34-
$playlistItem = $token->playlistItem()->firstOrFail();
34+
$playlistItem = $token->playlistItem;
3535
$requiredMods = array_column($playlistItem->required_mods, 'acronym');
3636
$mods = array_column($score->data->mods, 'acronym');
3737
$mods = app('mods')->excludeModsAlwaysValidForSubmission($score->ruleset_id, $mods);

app/Models/Multiplayer/UserScoreAggregate.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public function recalculate()
121121
$scoreLinks = ScoreLink
122122
::whereHas('playlistItem', fn ($q) => $q->where('room_id', $this->room_id))
123123
->where('user_id', $this->user_id)
124-
->with('score')
124+
->with(['score', 'playlistItem.room'])
125125
->get();
126126
foreach ($scoreLinks as $scoreLink) {
127127
($playlistItemAggs[$scoreLink->playlist_item_id]

app/Models/ScoreToken.php

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* @property int|null $build_id
1818
* @property \Carbon\Carbon|null $created_at
1919
* @property int $id
20+
* @property \App\Models\Multiplayer\PlaylistItem $playlistItem
2021
* @property int $ruleset_id
2122
* @property \App\Models\Solo\Score $score
2223
* @property int $score_id

app/Models/Traits/Es/BaseDbIndexable.php

+6-4
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,13 @@ public function esRouting()
8282

8383
public function esDeleteDocument(array $options = [])
8484
{
85-
$document = array_merge([
85+
$document = [
8686
'index' => static::esIndexName(),
8787
'routing' => $this->esRouting(),
8888
'id' => $this->getEsId(),
8989
'client' => ['ignore' => 404],
90-
], $options);
90+
...$options,
91+
];
9192

9293
return Es::getClient()->delete($document);
9394
}
@@ -98,12 +99,13 @@ public function esIndexDocument(array $options = [])
9899
return $this->esDeleteDocument($options);
99100
}
100101

101-
$document = array_merge([
102+
$document = [
102103
'index' => static::esIndexName(),
103104
'routing' => $this->esRouting(),
104105
'id' => $this->getEsId(),
105106
'body' => $this->toEsJson(),
106-
], $options);
107+
...$options,
108+
];
107109

108110
return Es::getClient()->index($document);
109111
}

app/Models/Traits/Es/BeatmapsetSearch.php

+34-12
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,30 @@ public static function esSchemaFile()
3131
return config_path('schemas/beatmapsets.json');
3232
}
3333

34+
private static function esBeatmapTags(Beatmap $beatmap): array
35+
{
36+
$tags = app('tags');
37+
38+
return array_reject_null(
39+
array_map(
40+
fn ($tagId) => $tags->get($tagId['tag_id'])?->name,
41+
$beatmap->topTagIds()
42+
)
43+
);
44+
}
45+
3446
public function esShouldIndex()
3547
{
3648
return !$this->trashed() && !present($this->download_disabled_url);
3749
}
3850

3951
public function toEsJson()
4052
{
41-
return array_merge(
42-
$this->esBeatmapsetValues(),
43-
['beatmaps' => $this->esBeatmapsValues()],
44-
['difficulties' => $this->esDifficultiesValues()]
45-
);
53+
return [
54+
...$this->esBeatmapsetValues(),
55+
'beatmaps' => $this->esBeatmapsValues(),
56+
'difficulties' => $this->esDifficultiesValues(),
57+
];
4658
}
4759

4860
private function esBeatmapsetValues()
@@ -78,12 +90,17 @@ private function esBeatmapsValues()
7890
foreach ($this->beatmaps as $beatmap) {
7991
$beatmapValues = [];
8092
foreach ($mappings as $field => $mapping) {
81-
$beatmapValues[$field] = $beatmap->$field;
93+
$value = match ($field) {
94+
'top_tags' => $this->esBeatmapTags($beatmap),
95+
// TODO: remove adding $beatmap->user_id once everything else also populated beatmap_owners by default.
96+
// Duplicate user_id in the array should be fine for now since the field isn't scored for querying.
97+
'user_id' => $beatmap->beatmapOwners->pluck('user_id')->add($beatmap->user_id),
98+
default => $beatmap->$field,
99+
};
100+
101+
$beatmapValues[$field] = $value;
82102
}
83103

84-
// TODO: remove adding $beatmap->user_id once everything else also populated beatmap_owners by default.
85-
// Duplicate user_id in the array should be fine for now since the field isn't scored for querying.
86-
$beatmapValues['user_id'] = $beatmap->beatmapOwners->pluck('user_id')->add($beatmap->user_id);
87104
$values[] = $beatmapValues;
88105

89106
if ($beatmap->playmode === Beatmap::MODES['osu']) {
@@ -96,11 +113,16 @@ private function esBeatmapsValues()
96113
$convert->playmode = $modeInt;
97114
$convert->convert = true;
98115
$convertValues = [];
99-
foreach ($mappings as $field => $mapping) {
100-
$convertValues[$field] = $convert->$field;
116+
foreach ($mappings as $field => $_mapping) {
117+
$convertValues[$field] = match ($field) {
118+
// just add a copy for converts too.
119+
'top_tags',
120+
'user_id' => $beatmapValues[$field],
121+
122+
default => $convert->$field,
123+
};
101124
}
102125

103-
$convertValues['user_id'] = $beatmapValues['user_id']; // just add a copy for converts too.
104126
$values[] = $convertValues;
105127
}
106128
}

0 commit comments

Comments
 (0)