Added 1v4, 1v5 matchmaking timeout as a setting in the match configuration admin panel.

This commit is contained in:
Vari 2025-07-10 01:04:46 +02:00
parent 0e4d0969d9
commit 62f0be4398
7 changed files with 126 additions and 41 deletions

View File

@ -5,6 +5,7 @@
use App\Classes\Matchmaking\MatchmakingPlayerCount; use App\Classes\Matchmaking\MatchmakingPlayerCount;
use App\Enums\Game\Matchmaking\MatchmakingSide; use App\Enums\Game\Matchmaking\MatchmakingSide;
use App\Enums\Game\Matchmaking\MatchStatus; use App\Enums\Game\Matchmaking\MatchStatus;
use App\Models\Admin\MatchmakingSettings;
use App\Models\Game\Matchmaking\Game; use App\Models\Game\Matchmaking\Game;
use App\Models\Game\Matchmaking\MatchConfiguration; use App\Models\Game\Matchmaking\MatchConfiguration;
use App\Models\Game\Matchmaking\QueuedPlayer; use App\Models\Game\Matchmaking\QueuedPlayer;
@ -19,11 +20,6 @@ class ProcessMatchmaking extends Command
{ {
public static int $repeatTimeSeconds = 20; public static int $repeatTimeSeconds = 20;
/**
* How long the matchmaking should wait when only one 1v4 or 1v5 could be made before actually making it.
*/
const ONE_VS_FOUR_AND_FIVE_WAIT_TIME = 10;
const ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY = 'matchmaking_attempt_1v4_1v5'; const ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY = 'matchmaking_attempt_1v4_1v5';
/** /**
@ -47,6 +43,7 @@ class ProcessMatchmaking extends Command
*/ */
public function handle(): void public function handle(): void
{ {
$matchmakingSettings = MatchmakingSettings::get();
// Select all queued Players/party leaders, descending by party size // Select all queued Players/party leaders, descending by party size
$players = QueuedPlayer::withCount('followingUsers') $players = QueuedPlayer::withCount('followingUsers')
->sharedLock() ->sharedLock()
@ -96,7 +93,7 @@ public function handle(): void
if (Cache::has(static::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY)) { if (Cache::has(static::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY)) {
/** @var Carbon $firstAttempt */ /** @var Carbon $firstAttempt */
$firstAttempt = Cache::get(static::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY); $firstAttempt = Cache::get(static::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY);
if ($firstAttempt->diffInSeconds(Carbon::now()) < static::ONE_VS_FOUR_AND_FIVE_WAIT_TIME){ if ($firstAttempt->diffInSeconds(Carbon::now()) < $matchmakingSettings->matchWaitingTime){
return; return;
} }
Cache::forget(static::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY); Cache::forget(static::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY);

View File

@ -17,6 +17,7 @@
use App\Models\Admin\Archive\ArchivedGame; use App\Models\Admin\Archive\ArchivedGame;
use App\Models\Admin\Archive\ArchivedPlayerProgression; use App\Models\Admin\Archive\ArchivedPlayerProgression;
use App\Models\Admin\CurrencyMultipliers; use App\Models\Admin\CurrencyMultipliers;
use App\Models\Admin\MatchmakingSettings;
use App\Models\Admin\Versioning\CurrentGameVersion; use App\Models\Admin\Versioning\CurrentGameVersion;
use App\Models\Game\Matchmaking\Game; use App\Models\Game\Matchmaking\Game;
use App\Models\Game\Matchmaking\QueuedPlayer; use App\Models\Game\Matchmaking\QueuedPlayer;
@ -40,10 +41,10 @@ public function getRegions()
public function queue(QueueRequest $request) public function queue(QueueRequest $request)
{ {
if($request->category !== CurrentGameVersion::get()?->gameVersion) if ($request->category !== CurrentGameVersion::get()?->gameVersion)
abort(403, 'Too old mod version'); abort(403, 'Too old mod version');
if($request->checkOnly) if ($request->checkOnly)
return json_encode($this->checkQueueStatus($request)); return json_encode($this->checkQueueStatus($request));
return json_encode($this->addPlayerToQueue($request)); return json_encode($this->addPlayerToQueue($request));
@ -53,20 +54,20 @@ public function cancelQueue(MatchmakingRequest $request)
{ {
$user = Auth::user(); $user = Auth::user();
if($user->id !== $request->playerId || $request->endState !== 'Cancelled') if ($user->id !== $request->playerId || $request->endState !== 'Cancelled')
return response('', 204); return response('', 204);
$lock = Cache::lock(static::QUEUE_LOCK, 10); $lock = Cache::lock(static::QUEUE_LOCK, 10);
try { try {
$lock->block(20 ,function () use (&$user) { $lock->block(20, function () use (&$user) {
// Delete the player from the Queue // Delete the player from the Queue
QueuedPlayer::where('user_id', '=', $user->id)->delete(); QueuedPlayer::where('user_id', '=', $user->id)->delete();
// And also from any game they are matched for. // And also from any game they are matched for.
DB::table('game_user')->where('user_id', '=', $user->id)->delete(); DB::table('game_user')->where('user_id', '=', $user->id)->delete();
}); });
} catch (LockTimeoutException $e) { } catch (LockTimeoutException $e) {
Log::channel('matchmaking')->emergency('Queue Cancel: Could not acquire Lock for canceling user '.$user->id.'('.$user->last_known_username.')'); Log::channel('matchmaking')->emergency('Queue Cancel: Could not acquire Lock for canceling user ' . $user->id . '(' . $user->last_known_username . ')');
} finally { } finally {
$lock->release(); $lock->release();
} }
@ -78,7 +79,7 @@ public function matchInfo(string $matchId)
{ {
$foundGame = Game::find($matchId); $foundGame = Game::find($matchId);
if($foundGame === null) if ($foundGame === null)
return ['status' => 'Error', 'message' => 'Match not found.']; return ['status' => 'Error', 'message' => 'Match not found.'];
$user = Auth::user(); $user = Auth::user();
@ -103,24 +104,24 @@ public function register(RegisterMatchRequest $request, string $matchId)
return json_encode(MatchData::fromGame($foundGame)); return json_encode(MatchData::fromGame($foundGame));
} }
public function close($matchId) public function close($matchId)
{ {
$foundGame = Game::find($matchId); $foundGame = Game::find($matchId);
$foundGame->status = MatchStatus::Closed; $foundGame->status = MatchStatus::Closed;
$foundGame->save(); $foundGame->save();
return json_encode(MatchData::fromGame($foundGame)); 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. * Set a game to Quit state, Maybe exists but unsure since it never showed up in the request logs.
*/ */
public function quit($matchId) public function quit($matchId)
{ {
$foundGame = Game::find($matchId); $foundGame = Game::find($matchId);
$user = Auth::user(); $user = Auth::user();
if($foundGame->creator === $user) { if ($foundGame->creator === $user) {
$foundGame->status = MatchStatus::Destroyed; $foundGame->status = MatchStatus::Destroyed;
$foundGame->save(); $foundGame->save();
} }
@ -129,17 +130,17 @@ public function quit($matchId)
} }
return response(null, 204); return response(null, 204);
} }
public function kill($matchId) public function kill($matchId)
{ {
$foundGame = Game::find($matchId); $foundGame = Game::find($matchId);
$response = json_encode(MatchData::fromGame($foundGame)); $response = json_encode(MatchData::fromGame($foundGame));
$foundGame->delete(); $foundGame->delete();
return $response; return $response;
} }
public function seedFileGet(string $gameVersion, string $seed, string $mapName) public function seedFileGet(string $gameVersion, string $seed, string $mapName)
{ {
@ -156,7 +157,7 @@ public function deleteUserFromMatch(Game $match, User $user)
$requestUser = Auth::user(); $requestUser = Auth::user();
// Block request if it doesn't come from the host // Block request if it doesn't come from the host
if($match->creator->id != $requestUser->id) if ($match->creator->id != $requestUser->id)
return response('Action not allowed, you are not the creator of the match.', 403); return response('Action not allowed, you are not the creator of the match.', 403);
$this->removeUserFromGame($user, $match); $this->removeUserFromGame($user, $match);
@ -166,13 +167,13 @@ public function deleteUserFromMatch(Game $match, User $user)
public function endOfMatch(EndOfMatchRequest $request) public function endOfMatch(EndOfMatchRequest $request)
{ {
if(ArchivedGame::archivedGameExists($request->matchId)) if (ArchivedGame::archivedGameExists($request->matchId))
return response('Match Already Closed', 209); return response('Match Already Closed', 209);
$game = Game::find($request->matchId); $game = Game::find($request->matchId);
$user = Auth::user(); $user = Auth::user();
if($game->creator != $user) if ($game->creator != $user)
return response('you are not the creator of the match.', 403); return response('you are not the creator of the match.', 403);
$game->status = MatchStatus::Killed; $game->status = MatchStatus::Killed;
@ -187,7 +188,7 @@ public function playerEndOfMatch(PlayerEndOfMatchRequest $request)
$user = Auth::user(); $user = Auth::user();
$game = Game::find($request->matchId); $game = Game::find($request->matchId);
if($game === null) if ($game === null)
return response('Match not found.', 404); return response('Match not found.', 404);
if ($game->creator != $user) if ($game->creator != $user)
@ -195,21 +196,21 @@ public function playerEndOfMatch(PlayerEndOfMatchRequest $request)
$user = User::find($request->playerId); $user = User::find($request->playerId);
if($user === null) if ($user === null)
return response('User not found.', 404); return response('User not found.', 404);
$lock = Cache::lock('playerEndOfMatch'.$user->id); $lock = Cache::lock('playerEndOfMatch' . $user->id);
try { try {
// Lock the saving of the playerdata and stuff because the game can send multiple calls sometimes // Lock the saving of the playerdata and stuff because the game can send multiple calls sometimes
$lock->block(10 ,function () use (&$user, &$request, &$game) { $lock->block(10 ,function () use (&$user, &$request, &$game) {
if(ArchivedPlayerProgression::archivedPlayerExists($game->id, $user->id)) if (ArchivedPlayerProgression::archivedPlayerExists($game->id, $user->id))
return; return;
$playerData = $user->playerData(); $playerData = $user->playerData();
$characterData = $playerData->characterDataForCharacter($request->characterGroup->getCharacter()); $characterData = $playerData->characterDataForCharacter($request->characterGroup->getCharacter());
if($request->hasQuit) if ($request->hasQuit)
$playerData->quitterState->addQuitterPenalty(); $playerData->quitterState->addQuitterPenalty();
else else
$playerData->quitterState->addStayedMatch($playerData); $playerData->quitterState->addStayedMatch($playerData);
@ -379,10 +380,11 @@ protected function removeUserFromGame(User $user, Game $game)
protected static function getETA(): int { protected static function getETA(): int {
if(Cache::has(ProcessMatchmaking::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY)) { if(Cache::has(ProcessMatchmaking::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY)) {
$matchmakingSettings = MatchmakingSettings::get();
/** @var Carbon $firstAttempt */ /** @var Carbon $firstAttempt */
$firstAttempt = Cache::get(ProcessMatchmaking::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY); $firstAttempt = Cache::get(ProcessMatchmaking::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY);
$predictedMatchTime = $firstAttempt->copy(); $predictedMatchTime = $firstAttempt->copy();
while ($firstAttempt->diffInSeconds($predictedMatchTime) < ProcessMatchmaking::ONE_VS_FOUR_AND_FIVE_WAIT_TIME) { while ($firstAttempt->diffInSeconds($predictedMatchTime) < $matchmakingSettings->matchWaitingTime) {
$predictedMatchTime->addSeconds(ProcessMatchmaking::$repeatTimeSeconds); $predictedMatchTime->addSeconds(ProcessMatchmaking::$repeatTimeSeconds);
} }

View File

@ -7,10 +7,12 @@
use App\Http\Requests\Api\Admin\Tools\SaveCurrencyConfiguration; use App\Http\Requests\Api\Admin\Tools\SaveCurrencyConfiguration;
use App\Http\Requests\Api\Admin\Tools\SaveLauncherMessageRequest; use App\Http\Requests\Api\Admin\Tools\SaveLauncherMessageRequest;
use App\Http\Requests\Api\Admin\Tools\SaveMatchConfigurationRequest; use App\Http\Requests\Api\Admin\Tools\SaveMatchConfigurationRequest;
use App\Http\Requests\Api\Admin\Tools\SaveMatchmakingConfigurationRequest;
use App\Http\Requests\Api\Admin\Tools\SaveVersioningRequest; use App\Http\Requests\Api\Admin\Tools\SaveVersioningRequest;
use App\Models\Admin\CurrencyMultipliers; use App\Models\Admin\CurrencyMultipliers;
use App\Models\Admin\ExperienceMultipliers; use App\Models\Admin\ExperienceMultipliers;
use App\Models\Admin\LauncherMessage; use App\Models\Admin\LauncherMessage;
use App\Models\Admin\MatchmakingSettings;
use App\Models\Admin\Versioning\CurrentCatalogVersion; use App\Models\Admin\Versioning\CurrentCatalogVersion;
use App\Models\Admin\Versioning\CurrentContentVersion; use App\Models\Admin\Versioning\CurrentContentVersion;
use App\Models\Admin\Versioning\CurrentGameVersion; use App\Models\Admin\Versioning\CurrentGameVersion;
@ -73,4 +75,20 @@ public function saveCurrency(SaveCurrencyConfiguration $request)
return back(); return back();
} }
public function saveMatchmaking(SaveMatchmakingConfigurationRequest $request) {
$matchmakingConfig = MatchmakingSettings::get();
try {
$matchmakingConfig->matchWaitingTime = $request->matchmakingWaitingTime;
$matchmakingConfig->save();
Session::flash('alert-success', 'Matchmaking configuration saved successfully.');
}
catch (\Exception $e) {
Session::flash('alert-error', 'Matchmaking configuration could not be saved, something went wrong: ' . $e->getMessage());
}
return back();
}
} }

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Api\Admin\Tools;
use Illuminate\Foundation\Http\FormRequest;
class SaveMatchmakingConfigurationRequest extends FormRequest
{
public int $matchmakingWaitingTime;
public function rules(): array {
return [
'matchmakingWaitingTime' => ['required', 'integer'],
];
}
protected function passedValidation()
{
$this->matchmakingWaitingTime = (int)$this->input('matchmakingWaitingTime');
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Models\Admin;
use App\Models\AbstractFileBasedModel;
class MatchmakingSettings extends AbstractFileBasedModel
{
const FILE_NAME = 'matchmakingSettings';
const CACHE_DURATION = 86400;
/**
* How long the matchmaking should wait when only one 1v4 or 1v5 could be made before actually making it.
*/
public int $matchWaitingTime = 10;
protected static function getDefault(): ?static
{
return new static();
}
}

View File

@ -3,14 +3,15 @@
$experienceMultipliers = \App\Models\Admin\ExperienceMultipliers::get(); $experienceMultipliers = \App\Models\Admin\ExperienceMultipliers::get();
$currencyMultipliers = \App\Models\Admin\CurrencyMultipliers::get(); $currencyMultipliers = \App\Models\Admin\CurrencyMultipliers::get();
$matchmakingSettings = \App\Models\Admin\MatchmakingSettings::get();
@endphp @endphp
<x-layouts.admin> <x-layouts.admin>
<div class="container mx-auto border bg-slate-800 border-slate-500 rounded-xl p-4 my-6"> <div class="container mx-auto border bg-slate-800 border-slate-500 rounded-xl p-4 my-6">
<div class="flex flex-col gap-4 px-4 lg:[&>form>div]:px-52 [&>form>div]:flex [&>form>div]:justify-between [&>form>div]:items-center [&>form_label]:text-xl"> <div class="flex flex-col gap-4 px-4 lg:[&>form>div]:px-52 [&>form>div]:flex [&>form>div]:justify-between [&>form>div]:items-center [&>form_label]:text-xl">
<form action="{{ route('match-configuration.save.experience') }}" method="post"> <form action="{{ route('match-configuration.save.experience') }}" method="post" class="[&>div]:m-2">
@csrf @csrf
<span class="headline"> <span class="headline mb-2">
Experience Multipliers Experience Multipliers
</span> </span>
@ -139,7 +140,7 @@
Save Save
</x-inputs.button> </x-inputs.button>
</form> </form>
<form action="{{ route('match-configuration.save.currency') }}" method="post" class="mt-8"> <form action="{{ route('match-configuration.save.currency') }}" method="post" class="mt-8 [&>div]:m-2">
@csrf @csrf
<span class="headline"> <span class="headline">
Currency Multipliers Currency Multipliers
@ -190,6 +191,29 @@
Save Save
</x-inputs.button> </x-inputs.button>
</form> </form>
<form action="{{ route('match-configuration.save.matchmaking') }}" method="post" class="mt-8 [&>div]:m-2">
@csrf
<span class="headline mb-2">
Matchmaking Settings
</span>
<div>
<label for="matchmakingWaitingTime">
1v4 and 1v5 matchmaking wait duration
</label>
<x-inputs.number
id="matchmakingWaitingTime"
name="matchmakingWaitingTime"
value="{{ $matchmakingSettings->matchWaitingTime }}"
step="1"
required
/>
</div>
<x-inputs.button class="save mt-2">
Save
</x-inputs.button>
</form>
</div> </div>
</div> </div>
</x-layouts.admin> </x-layouts.admin>

View File

@ -56,6 +56,7 @@
Route::get('match-configuration', [MatchConfigurationController::class, 'index'])->name(MatchConfigurationController::class); Route::get('match-configuration', [MatchConfigurationController::class, 'index'])->name(MatchConfigurationController::class);
Route::post('match-configuration/experience', [MatchConfigurationController::class, 'saveExperience'])->name('match-configuration.save.experience'); Route::post('match-configuration/experience', [MatchConfigurationController::class, 'saveExperience'])->name('match-configuration.save.experience');
Route::post('match-configuration/currency', [MatchConfigurationController::class, 'saveCurrency'])->name('match-configuration.save.currency'); Route::post('match-configuration/currency', [MatchConfigurationController::class, 'saveCurrency'])->name('match-configuration.save.currency');
Route::post('match-configuration/matchmaking', [MatchConfigurationController::class, 'saveMatchmaking'])->name('match-configuration.save.matchmaking');
Route::fallback(function () { Route::fallback(function () {
return redirect(route('admin.dashboard')); return redirect(route('admin.dashboard'));