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
6namespace App\Http\Controllers\Multiplayer;
7
8use App\Http\Controllers\Controller;
9use App\Http\Controllers\Ranking\DailyChallengeController;
10use App\Models\Model;
11use App\Models\Multiplayer\Room;
12use App\Transformers\Multiplayer\RoomTransformer;
13
14class RoomsController extends Controller
15{
16 public function __construct()
17 {
18 $this->middleware('auth', ['except' => 'show']);
19 $this->middleware('require-scopes:public', ['only' => ['index', 'leaderboard', 'show']]);
20 }
21
22 public function destroy($id)
23 {
24 Room::findOrFail($id)->endGame(\Auth::user());
25
26 return response(null, 204);
27 }
28
29 /**
30 * Get Multiplayer Rooms
31 *
32 * Returns a list of multiplayer rooms.
33 *
34 * @group Multiplayer
35 *
36 * @queryParam limit Maximum number of results. No-example
37 * @queryParam mode Filter mode; `active` (default), `all`, `ended`, `participated`, `owned`. No-example
38 * @queryParam season_id Season ID to return Rooms from. No-example
39 * @queryParam sort Sort order; `ended`, `created`. No-example
40 * @queryParam type_group `playlists` (default) or `realtime`. No-example
41 */
42 public function index()
43 {
44 $apiVersion = api_version();
45 $compactReturn = $apiVersion >= 20220217;
46 $objectReturn = $apiVersion >= 99999999;
47 $params = request()->all();
48 $params['user'] = auth()->user();
49
50 $includes = ['host.country', 'playlist.beatmap'];
51
52 if (!$compactReturn) {
53 $includes = [...$includes, 'playlist.beatmap.beatmapset', 'playlist.beatmap.baseMaxCombo'];
54 }
55
56 $search = Room::search($params);
57 $query = $search['query'];
58
59 // temporary workaround for lazer client failing to deserialise `daily_challenge` room category
60 // can be removed 20241129
61 if ($apiVersion < 20240529) {
62 $query->whereNot('category', 'daily_challenge');
63 }
64
65 $rooms = $query
66 ->with($includes)
67 ->withRecentParticipantIds()
68 ->get();
69 Room::preloadRecentParticipants($rooms);
70
71 if ($compactReturn) {
72 $rooms->each->findAndSetCurrentPlaylistItem();
73 $rooms->loadMissing('currentPlaylistItem.beatmap.beatmapset');
74
75 $roomsJson = json_collection($rooms, new RoomTransformer(), [
76 'current_playlist_item.beatmap.beatmapset',
77 'difficulty_range',
78 'host.country',
79 'playlist_item_stats',
80 'recent_participants',
81 ]);
82
83 if ($objectReturn) {
84 return array_merge([
85 'rooms' => $roomsJson,
86 ], cursor_for_response($search['cursorHelper']->next($rooms)));
87 } else {
88 return $roomsJson;
89 }
90 } else {
91 return json_collection($rooms, new RoomTransformer(), [
92 'host.country',
93 'playlist.beatmap.beatmapset',
94 'playlist.beatmap.checksum',
95 'playlist.beatmap.max_combo',
96 'recent_participants',
97 ]);
98 }
99 }
100
101 public function join($roomId, $userId)
102 {
103 $currentUser = \Auth::user();
104 // this allows admins/whatever to add users to games in the future
105 if (get_int($userId) !== $currentUser->getKey()) {
106 abort(403);
107 }
108
109 $room = Room::findOrFail($roomId);
110 $room->assertCorrectPassword(get_string(request('password')));
111
112 $room->join($currentUser);
113
114 return RoomTransformer::createShowResponse($room);
115 }
116
117 public function leaderboard($roomId)
118 {
119 $limit = \Number::clamp(get_int(request('limit')) ?? Model::PER_PAGE, 1, 50);
120 $room = Room::findOrFail($roomId);
121
122 // leaderboard currently requires auth so auth()->check() is not required.
123 $userScore = $room->topScores()->where('user_id', auth()->id())->first();
124
125 return [
126 'leaderboard' => json_collection(
127 $room->topScores()->paginate($limit),
128 'Multiplayer\UserScoreAggregate',
129 ['user.country']
130 ),
131 'user_score' => $userScore !== null ? json_item(
132 $userScore,
133 'Multiplayer\UserScoreAggregate',
134 ['position', 'user.country']
135 ) : null,
136 ];
137 }
138
139 public function part($roomId, $userId)
140 {
141 $currentUser = \Auth::user();
142 // this allows admins/host/whoever to remove users from games in the future
143 if (get_int($userId) !== $currentUser->getKey()) {
144 abort(403);
145 }
146
147 $room = Room::findOrFail($roomId);
148 $room->part($currentUser);
149
150 return response([], 204);
151 }
152
153 public function show($id)
154 {
155 if ($id === 'latest') {
156 $room = Room::featured()->last();
157
158 if ($room === null) {
159 abort(404);
160 }
161 } else {
162 $room = Room::findOrFail($id);
163 }
164
165 if (is_api_request()) {
166 return RoomTransformer::createShowResponse($room);
167 }
168
169 if ($room->category === 'daily_challenge') {
170 return ujs_redirect(route('daily-challenge.show', DailyChallengeController::roomId($room)));
171 }
172
173 $playlistItemsQuery = $room->playlist();
174 if ($room->isRealtime()) {
175 $playlistItemsQuery->whereHas('scoreLinks');
176 }
177 $beatmaps = $playlistItemsQuery->with('beatmap.beatmapset.beatmaps')->get()->pluck('beatmap');
178 $beatmapsets = $beatmaps->pluck('beatmapset');
179 $highScores = $room->topScores()->paginate();
180 $spotlightRooms = Room::featured()->orderBy('id', 'DESC')->get();
181
182 $userScore = ($currentUser = \Auth::user()) === null
183 ? null
184 : $room->topScores()->whereBelongsTo($currentUser)->first();
185
186 return ext_view('multiplayer.rooms.show', [
187 'beatmaps' => $beatmaps,
188 'beatmapsets' => $beatmapsets,
189 'room' => $room,
190 'rooms' => $spotlightRooms,
191 'scores' => $highScores,
192 'userScore' => $userScore,
193 ]);
194 }
195
196 public function store()
197 {
198 $room = (new Room())->startGame(\Auth::user(), \Request::all());
199
200 return RoomTransformer::createShowResponse($room);
201 }
202}