category !== CurrentGameVersion::get()?->gameVersion) abort(403, 'Too old mod version'); if ($request->checkOnly) return json_encode($this->checkQueueStatus($request)); return json_encode($this->addPlayerToQueue($request)); } public function cancelQueue(MatchmakingRequest $request) { $user = Auth::user(); if ($user->id !== $request->playerId || $request->endState !== 'Cancelled') return response('', 204); $lock = Cache::lock(static::QUEUE_LOCK, 10); try { $lock->block(20, function () use (&$user) { // Delete the player from the Queue QueuedPlayer::where('user_id', '=', $user->id)->delete(); // And also from any game they are matched for. DB::table('game_user')->where('user_id', '=', $user->id)->delete(); }); } catch (LockTimeoutException $e) { Log::channel('matchmaking')->emergency('Queue Cancel: Could not acquire Lock for canceling user ' . $user->id . '(' . $user->last_known_username . ')'); } finally { $lock->release(); } return response('', 204); } public function matchInfo(string $matchId) { $foundGame = Game::find($matchId); if ($foundGame === null) return ['status' => 'Error', 'message' => 'Match not found.']; $user = Auth::user(); // Update timestamp for player heartbeat check $foundGame->players()->updateExistingPivot($user->id, [ 'updated_at' => Carbon::now(), ]); $foundGame->updated_at = Carbon::now(); $foundGame->save(); $response = MatchData::fromGame($foundGame); return json_encode($response); } public function register(RegisterMatchRequest $request, string $matchId) { $foundGame = Game::find($matchId); $foundGame->session_settings = $request->sessionSettings; $foundGame->status = MatchStatus::Opened; $foundGame->save(); return json_encode(MatchData::fromGame($foundGame)); } public function close($matchId) { $foundGame = Game::find($matchId); $foundGame->status = MatchStatus::Closed; $foundGame->save(); return json_encode(MatchData::fromGame($foundGame)); } /* * Set a game to Quit state, Maybe exists but unsure since it never showed up in the request logs. */ public function quit($matchId) { $foundGame = Game::find($matchId); $user = Auth::user(); if ($foundGame->creator === $user) { $foundGame->status = MatchStatus::Destroyed; $foundGame->save(); } else { $foundGame->players()->detach(Auth::user()->id); } return response(null, 204); } public function kill($matchId) { $foundGame = Game::find($matchId); $response = json_encode(MatchData::fromGame($foundGame)); $foundGame->delete(); return $response; } public function seedFileGet(string $gameVersion, string $seed, string $mapName) { return response('', 200); } public function seedFilePost(string $gameVersion, string $seed, string $mapName) { return response('', 200); } public function deleteUserFromMatch(Game $match, User $user) { $requestUser = Auth::user(); // Block request if it doesn't come from the host if ($match->creator->id != $requestUser->id) return response('Action not allowed, you are not the creator of the match.', 403); $this->removeUserFromGame($user, $match); return json_encode(['success' => true]); } public function endOfMatch(EndOfMatchRequest $request) { if (ArchivedGame::archivedGameExists($request->matchId)) return response('Match Already Closed', 209); $game = Game::find($request->matchId); $user = Auth::user(); if ($game->creator != $user) return response('you are not the creator of the match.', 403); $game->status = MatchStatus::Killed; $game->save(); $game->archiveGame($request->dominantFaction); return json_encode(['success' => true]); } public function playerEndOfMatch(PlayerEndOfMatchRequest $request) { $user = Auth::user(); $game = Game::find($request->matchId); if ($game === null) return response('Match not found.', 404); if ($game->creator != $user) throw new AuthorizationException('User is not host of given match'); $user = User::find($request->playerId); if ($user === null) return response('User not found.', 404); $lock = Cache::lock('playerEndOfMatch' . $user->id); try { // Lock the saving of the playerdata and stuff because the game can send multiple calls sometimes $lock->block(10 ,function () use (&$user, &$request, &$game) { if (ArchivedPlayerProgression::archivedPlayerExists($game->id, $user->id)) return; $playerData = $user->playerData(); $characterData = $playerData->characterDataForCharacter($request->characterGroup->getCharacter()); if ($request->hasQuit) $playerData->quitterState->addQuitterPenalty(); else $playerData->quitterState->addStayedMatch($playerData); $experienceSum = 0; foreach ($request->experienceEvents as $experienceEvent) { // Multiply the amount by the Multiplier of the type $experienceSum += (int)($experienceEvent->amount * $experienceEvent->type->getMultiplier()); } $characterData->addExperience($experienceSum); ++$characterData->readout_version; $characterData->save(); $gainedCurrencyA = 0; $gainedCurrencyB = 0; $gainedCurrencyC = 0; $currencyMultipliers = CurrencyMultipliers::get(); foreach ($request->earnedCurrencies as $earnedCurrency) { switch ($earnedCurrency['currencyName']) { case 'CurrencyA': $gainedCurrencyA += (int)($earnedCurrency['amount'] * $currencyMultipliers->currencyA); break; case 'CurrencyB': $gainedCurrencyB += (int)($earnedCurrency['amount'] * $currencyMultipliers->currencyB); break; case 'CurrencyC': $gainedCurrencyC += (int)($earnedCurrency['amount'] * $currencyMultipliers->currencyC); } } $playerData->currency_a += $gainedCurrencyA; $playerData->currency_b += $gainedCurrencyB; $playerData->currency_c += $gainedCurrencyC; ++$playerData->readout_version; $playerData->push(); ArchivedPlayerProgression::archivePlayerProgression( $game, $user, $request->hasQuit, $request->characterGroup->getCharacter(), $request->characterState, $experienceSum, $request->experienceEvents, $gainedCurrencyA, $gainedCurrencyB, $gainedCurrencyC, ); }); } catch (LockTimeoutException $e) { return response('The Player end of match request for this user is currently being processed', 409); } // We dont really know what the game wants except for a json object called "player". return json_encode(['player' => []], JSON_FORCE_OBJECT); } protected function checkQueueStatus(QueueRequest $request): QueueResponse { $user = Auth::user(); $foundQueuedPlayer = QueuedPlayer::firstWhere('user_id', '=', $user->id); // If we found a queued Player, return his status if ($foundQueuedPlayer !== null) { // Set Last queue call time to remove players that maybe crashed or something // if they haven't sent a queue request in a long time. $foundQueuedPlayer->updated_at = Carbon::now(); $foundQueuedPlayer->save(); $response = new QueueResponse(); $response->status = QueueStatus::Queued; $response->queueData = new QueueData( 1, static::getETA(), ); return $response; } // If we didn't find the player in the queued list, search if he is already joined a match // Only search for Created or open matches $foundGame = $user->activeGames(); // If they also aren't in a game, add them to the queue again if ($foundGame->count() < 1) { $this->addPlayerToQueue($request); $response = new QueueResponse(); $response->status = QueueStatus::Queued; $response->queueData = new QueueData( 1, static::getETA(), ); return $response; } /** @var Game $foundGame */ $foundGame = $foundGame->first(); // Update pivot updated_at to detect client crashes or other errors of theyhavent sent a request in a period of time. $foundGame->players()->updateExistingPivot($user->id, ['updated_at' => Carbon::now()]); $response = new QueueResponse(); if ($foundGame->status === MatchStatus::Opened) { $response->queueData = new QueueData( 1, static::getETA(), ); } $response->status = QueueStatus::Matched; $response->matchData = MatchData::fromGame($foundGame); return $response; } protected function addPlayerToQueue(QueueRequest $request) { Cache::lock(static::QUEUE_LOCK, 10)->block(20, function () use ($request) { $user = Auth::user(); // Remove user from any game he has previously joined. if ($user->activeGames()->exists()) { $games = $user->activeGames()->get(); foreach ($games as $game) { $this->removeUserFromGame($user, $game); } } // Delete any active matches where the newly queued user is the creator. Game::where('creator_user_id', '=', $user->id) ->whereIn('status', [MatchStatus::Opened, MatchStatus::Created]) ->delete(); $queued = QueuedPlayer::firstOrCreate(['user_id' => $user->id]); $queued->leader()->disassociate(); $queued->side = $request->side; $queued->user()->associate($user->id); $queued->save(); foreach ($request->additionalUserIds as $additionalUserId) { $follower = QueuedPlayer::firstOrCreate(['user_id' => $additionalUserId]); $follower->side = $request->side; $follower->user()->associate($additionalUserId); $follower->save(); $queued->followingUsers()->save($follower); } }); return $this->checkQueueStatus($request); } protected function removeUserFromGame(User $user, Game $game) { $game->players()->detach($user); // Delete the game if the creator deletes themselfes sice we had one case where the matchmaking broke due to a game being stuck like this. if ($game->players->count() !== 0 && $game->creator_user_id !== $user->id) return; $game->delete(); } protected static function getETA(): int { if(Cache::has(ProcessMatchmaking::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY)) { $matchmakingSettings = MatchmakingSettings::get(); /** @var Carbon $firstAttempt */ $firstAttempt = Cache::get(ProcessMatchmaking::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY); $predictedMatchTime = $firstAttempt->copy(); while ($firstAttempt->diffInSeconds($predictedMatchTime) < $matchmakingSettings->matchWaitingTime) { $predictedMatchTime->addSeconds(ProcessMatchmaking::$repeatTimeSeconds); } return Carbon::now()->diffInSeconds($predictedMatchTime) * 1000; } return -1; } }