Skip to content

Commit 2e25d35

Browse files
alexp8Sneer-ra2
andauthored
Develop to main (#391)
* Duplicate handling. * Added page of list with active duplicates. * Full IP-check for confirmed duplicates. * Simplification. * Removed comment. * Small update. * Added database migration. * Added unit test. * track observers * move comment * update image.php * fix null pointer (#379) * Develop (#380) (#381) * Duplicate handling. * Added page of list with active duplicates. * Full IP-check for confirmed duplicates. * Simplification. * Removed comment. * Small update. * Added database migration. * Added unit test. * track observers * move comment * update image.php * fix null pointer (#379) --------- Co-authored-by: Sneer <forum@snear.de> Co-authored-by: Sneer-ra2 <116219243+Sneer-ra2@users.noreply.github.com> * add more logs (#382) * add more logs * update log * Fix for createPlayer. * fix bug where disconnect gave everyone minus points (#385) * fix bug where disconnect gave everyone minus points * fix ladder info map issue * Improved naming on elo page. * Improved description for duplicate reason. * bug fix * Database migration for user ratings. Player details and tooltip adjustments. * Updated admin rating page. * Quick fix. * Simplified conditions. * Fixed user settings update. * new implementation (#390) * verify observer is streaming on twitch (#384) * Show bans from duplicates. (#389) Co-authored-by: Alex Peterson <amp1993@gmail.com> --------- Co-authored-by: Sneer <forum@snear.de> Co-authored-by: Sneer-ra2 <116219243+Sneer-ra2@users.noreply.github.com>
1 parent bdb6185 commit 2e25d35

25 files changed

+714
-220
lines changed

cncnet-api/app/Extensions/Qm/Matchup/TeamMatchupHandler.php

Lines changed: 207 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Models\Game;
66
use App\Models\QmQueueEntry;
7+
use App\Models\QmLadderRules;
78
use Illuminate\Support\Collection;
89
use Illuminate\Support\Facades\Log;
910

@@ -36,7 +37,10 @@ public function matchup(): void
3637

3738
// Find opponents that can be matched with current player.
3839
$matchableOpponents = $this->quickMatchService->getEntriesInPointRange($this->qmQueueEntry, $matchableOpponents);
39-
40+
41+
// TESTING new 2v2 matching logic
42+
$this->testNew2v2Logic($this->qmQueueEntry, $matchableOpponents);
43+
4044
$opponentCount = $matchableOpponents->count();
4145
Log::debug("FindOpponent ** inQueue={$playerInQueue}, amount of matchable opponent after point filter: {$opponentCount} of {$count}");
4246

@@ -108,4 +112,206 @@ private function createTeamMatch(Collection $maps, Collection $teamAPlayers, Col
108112
$stats
109113
);
110114
}
115+
116+
/**
117+
* Solely used for testing function `getEntriesInPointRange2v2()` in production
118+
* Result of function is logged out.
119+
* Errors are caught and then logged out.
120+
*/
121+
public function testNew2v2Logic(QmQueueEntry $currentQmQueueEntry, Collection $opponents)
122+
{
123+
try
124+
{
125+
$match2v2 = $this->getEntriesInPointRange2v2($currentQmQueueEntry, $opponents);
126+
127+
Log::debug(
128+
"2v2 matching result for {$currentQmQueueEntry->qmPlayer?->player?->username}: " .
129+
($match2v2->isNotEmpty()
130+
? 'MATCH FOUND: ' . $match2v2->pluck('qmPlayer.player.username')->implode(', ')
131+
: 'No match found')
132+
);
133+
}
134+
catch (\Throwable $e)
135+
{
136+
Log::error("Error during TEST 2v2 matchmaking for queueEntry {$currentQmQueueEntry->id}: {$e->getMessage()}", [
137+
'trace' => $e->getTraceAsString()
138+
]);
139+
}
140+
}
141+
142+
/**
143+
* Attempt to find a valid 2v2 match for the given player.
144+
* Ensures that:
145+
* - The current player is always included in the match.
146+
* - All players in the match are within each other's allowed point range.
147+
* - The result is sorted by closeness in point range to improve match fairness.
148+
*
149+
* @param QmQueueEntry $currentQmQueueEntry
150+
* @param Collection|QmQueueEntry[] $opponents
151+
* @return Collection|QmQueueEntry[] A collection containing the matched players (including current), or empty if no match found
152+
*/
153+
public function getEntriesInPointRange2v2(QmQueueEntry $currentQmQueueEntry, Collection $opponents): Collection
154+
{
155+
$rules = $currentQmQueueEntry->ladderHistory->ladder->qmLadderRules;
156+
$playerName = $currentQmQueueEntry->qmPlayer?->player?->username ?? 'Unknown';
157+
158+
Log::debug("2v2 Search start: queueEntry={$currentQmQueueEntry->id}, name={$playerName}, opponentsInQueue=" . count($opponents));
159+
160+
// Filter opponents to those that pass point range check with the current player
161+
$potentialOpponents = $this->filterOpponentsInRange($currentQmQueueEntry, $opponents, $rules);
162+
163+
Log::debug("Potential opponents for {$playerName}: " . $potentialOpponents->pluck('qmPlayer.player.username')->implode(', '));
164+
165+
if ($potentialOpponents->count() < 3)
166+
{
167+
Log::debug("Not enough valid opponents for {$playerName} to form a 2v2 match");
168+
return collect();
169+
}
170+
171+
// Sort opponents by closeness in points to the current player
172+
$sortedOpponents = $potentialOpponents->sortBy(function ($opponent) use ($currentQmQueueEntry)
173+
{
174+
return abs($currentQmQueueEntry->points - $opponent->points);
175+
})->values();
176+
177+
// Try all possible 3-player combinations (since the current player makes 4)
178+
foreach ($sortedOpponents->combinations(3) as $threeOthers)
179+
{
180+
$matchPlayers = collect([$currentQmQueueEntry])->merge($threeOthers);
181+
182+
if ($this->allPlayersInRange($matchPlayers, $rules))
183+
{
184+
Log::debug(
185+
"✅ Found valid 2v2 match for {$playerName}: " .
186+
$matchPlayers->pluck('qmPlayer.player.username')->implode(', ')
187+
);
188+
return $matchPlayers;
189+
}
190+
}
191+
192+
Log::debug("❌ No valid 2v2 match found for {$playerName}");
193+
return collect();
194+
}
195+
196+
/**
197+
* Filter opponents that are within point range of the current player.
198+
*
199+
* @param QmQueueEntry $current
200+
* @param Collection|QmQueueEntry[] $opponents
201+
* @param QmLadderRules $rules
202+
* @return Collection|QmQueueEntry[]
203+
*/
204+
private function filterOpponentsInRange(QmQueueEntry $current, Collection $opponents, QmLadderRules $rules): Collection
205+
{
206+
$pointsPerSecond = $rules->points_per_second;
207+
$maxPointsDifference = $rules->max_points_difference;
208+
$currentPointFilter = $current->qmPlayer->player->user->userSettings->disabledPointFilter;
209+
210+
$matchable = collect();
211+
212+
foreach ($opponents as $opponent)
213+
{
214+
if (!isset($opponent->qmPlayer) || $opponent->qmPlayer->isObserver())
215+
{
216+
continue;
217+
}
218+
219+
$diff = abs($current->points - $opponent->points);
220+
$waitTimeBonus = (strtotime($current->updated_at) - strtotime($current->created_at)) * $pointsPerSecond;
221+
222+
$inNormalRange = $waitTimeBonus + $maxPointsDifference > $diff;
223+
$inDisabledFilterRange = $currentPointFilter
224+
&& $opponent->qmPlayer->player->user->userSettings->disabledPointFilter
225+
&& $diff < 1000
226+
&& $current->points > 400
227+
&& $opponent->points > 400;
228+
229+
if ($inNormalRange || $inDisabledFilterRange)
230+
{
231+
$matchable->push($opponent);
232+
}
233+
}
234+
235+
return $matchable;
236+
}
237+
238+
/**
239+
* Check if all players in the given collection are within point range of each other.
240+
* Logs a pass/fail table for each comparison.
241+
*
242+
* @param Collection|QmQueueEntry[] $players
243+
* @param QmLadderRules $rules
244+
* @return bool
245+
*/
246+
private function allPlayersInRange(Collection $players, QmLadderRules $rules): bool
247+
{
248+
$pointsPerSecond = $rules->points_per_second;
249+
$maxPointsDifference = $rules->max_points_difference;
250+
$comparisonResults = [];
251+
252+
foreach ($players as $i => $p1)
253+
{
254+
foreach ($players as $j => $p2)
255+
{
256+
if ($i >= $j) continue; // Skip self and duplicate checks
257+
258+
$diff = abs($p1->points - $p2->points);
259+
$waitTimeBonusP1 = (strtotime($p1->updated_at) - strtotime($p1->created_at)) * $pointsPerSecond;
260+
$waitTimeBonusP2 = (strtotime($p2->updated_at) - strtotime($p2->created_at)) * $pointsPerSecond;
261+
262+
$passesNormalRange = ($waitTimeBonusP1 + $maxPointsDifference >= $diff)
263+
|| ($waitTimeBonusP2 + $maxPointsDifference >= $diff);
264+
265+
$passesDisabledFilter = $p1->qmPlayer->player->user->userSettings->disabledPointFilter
266+
&& $p2->qmPlayer->player->user->userSettings->disabledPointFilter
267+
&& $diff < 1000
268+
&& $p1->points > 400
269+
&& $p2->points > 400;
270+
271+
$pass = $passesNormalRange || $passesDisabledFilter;
272+
273+
$comparisonResults[] = [
274+
'p1' => $p1->qmPlayer?->player?->username ?? 'Unknown',
275+
'p2' => $p2->qmPlayer?->player?->username ?? 'Unknown',
276+
'points1' => $p1->points,
277+
'points2' => $p2->points,
278+
'diff' => $diff,
279+
'pass' => $pass
280+
];
281+
282+
if (!$pass)
283+
{
284+
$this->logComparisonTable($comparisonResults);
285+
return false;
286+
}
287+
}
288+
}
289+
290+
$this->logComparisonTable($comparisonResults);
291+
return true;
292+
}
293+
294+
/**
295+
* Logs the comparison results for all player pairs.
296+
*
297+
* @param array $comparisonResults
298+
*/
299+
private function logComparisonTable(array $comparisonResults): void
300+
{
301+
Log::debug("=== 2v2 Player Comparison Table ===");
302+
foreach ($comparisonResults as $result)
303+
{
304+
$status = $result['pass'] ? '✅ PASS' : '❌ FAIL';
305+
Log::debug(sprintf(
306+
"%-15s (%4d pts) ↔ %-15s (%4d pts) | Diff: %4d | %s",
307+
$result['p1'],
308+
$result['points1'],
309+
$result['p2'],
310+
$result['points2'],
311+
$result['diff'],
312+
$status
313+
));
314+
}
315+
Log::debug("===================================");
316+
}
111317
}

cncnet-api/app/Http/Controllers/AdminController.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,7 @@ public function updateUser(Request $request)
646646
$user->save();
647647
}
648648

649-
if ($request->exists('alias'))
649+
if ($request->exists('alias') && trim($request->alias) !== ($user->alias ?? ''))
650650
{
651651
if ($user->isDuplicate())
652652
{
@@ -1154,7 +1154,7 @@ public function getLadderPlayer(Request $request, $ladderId = null, $playerId =
11541154
$ladderService = new LadderService;
11551155
$history = $ladderService->getActiveLadderByDate(Carbon::now()->format('m-Y'), $player->ladder->abbreviation);
11561156

1157-
$bans = $user->bans()->orderBy('created_at', 'DESC')->get();
1157+
$bans = $user->collectBans()->sortByDesc('created_at');
11581158

11591159
return view(
11601160
"admin.moderate-player",
@@ -1470,15 +1470,25 @@ public function getPlayerRatings(Request $request, $ladderAbbreviation = null)
14701470

14711471
$users = User::join("user_ratings as ur", "ur.user_id", "=", "users.id")
14721472
->orderBy("ur.rating", "DESC")
1473+
->where("ur.ladder_id", $ladder->id)
1474+
->where(function ($query) {
1475+
$query->whereNull("users.primary_user_id")
1476+
->orWhereColumn("users.primary_user_id", "users.id");
1477+
})
14731478
->whereIn("users.id", $byPlayer->pluck("user_id"))
1474-
->select(["users.*", "ur.rating", "ur.rated_games", "ur.peak_rating"])
1479+
->select(["users.*", "ur.rating", "ur.rated_games", "ur.deviation"])
14751480
->paginate(50);
14761481
}
14771482
else
14781483
{
14791484
$users = User::join("user_ratings as ur", "ur.user_id", "=", "users.id")
14801485
->orderBy("ur.rating", "DESC")
1481-
->select(["users.*", "ur.rating", "ur.rated_games", "ur.peak_rating"])
1486+
->where("ur.ladder_id", $ladder->id)
1487+
->where(function ($query) {
1488+
$query->whereNull("users.primary_user_id")
1489+
->orWhereColumn("users.primary_user_id", "users.id");
1490+
})
1491+
->select(["users.*", "ur.rating", "ur.rated_games", "ur.deviation"])
14821492
->paginate(50);
14831493
}
14841494

@@ -1784,7 +1794,7 @@ public function getObservedGames(Request $request, $ladderAbbreviation = null)
17841794

17851795
// to populate a dropdown and user can pick which history to view observed games
17861796
$histories = LadderHistory::where('ladder_id', $ladder->id)
1787-
->where('ends', '<=', now())
1797+
->where('starts', '<=', now())
17881798
->orderBy('ends', 'DESC')
17891799
->select('short')
17901800
->get();

cncnet-api/app/Http/Controllers/Api/V2/Qm/MatchUpController.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ public function __invoke(Request $request, Ladder $ladder, string $playerName)
6060
}
6161

6262
// failsafe, is user allowed to match on 2v2 ladder
63-
if ($ladder->ladder_type == Ladder::TWO_VS_TWO && !$user->userSettings->allow_2v2_ladders) {
63+
if ($ladder->ladder_type == Ladder::TWO_VS_TWO && !$user->userSettings->allow_2v2_ladders)
64+
{
6465
return $this->quickMatchService->onFatalError(
6566
$playerName . ' is not allowed to play on 2v2 ladders, speak with admins for assistance ' . $ladder->abbreviation
6667
);
@@ -240,7 +241,20 @@ private function onMatchMeUp(Request $request, Ladder $ladder, Player $player, ?
240241
// If we're new to the queue, create required QmMatchPlayer model
241242
if (!isset($qmPlayer))
242243
{
243-
$qmPlayer = $this->quickMatchService->createQMPlayer($request, $player, $ladder->current_history);
244+
try
245+
{
246+
$qmPlayer = $this->quickMatchService->createQMPlayer($request, $player, $ladder->current_history);
247+
}
248+
catch (\RuntimeException $ex)
249+
{
250+
Log::error('Failed to create QM Player: ' . $ex->getMessage(), [
251+
'player_id' => $player->id,
252+
'username' => $player->username,
253+
]);
254+
255+
return $this->quickMatchService->onFatalError($ex->getMessage());
256+
}
257+
244258
$validSides = $this->quickMatchService->checkPlayerSidesAreValid($qmPlayer, $request->side, $ladder->qmLadderRules);
245259
$qmPlayer->save();
246260

cncnet-api/app/Http/Controllers/ApiLadderController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,7 @@ public function getLadderGame(Request $request, $game = null, $gameId = null)
831831

832832
public function getLadderPlayer(Request $request, $game = null, $player = null)
833833
{
834-
$date = Carbon::now()->format('m-Y');
834+
$date = $request->query('date') ?? Carbon::now()->format('m-Y');
835835
$ladderService = $this->ladderService;
836836
return Cache::remember("getLadderPlayer/$date/$game/$player", 5 * 60, function () use ($ladderService, $date, $game, $player)
837837
{
@@ -842,7 +842,7 @@ public function getLadderPlayer(Request $request, $game = null, $player = null)
842842

843843
public function getLadderPlayerFromPublicApi(Request $request, $game = null, $player = null)
844844
{
845-
$date = Carbon::now()->format('m-Y');
845+
$date = $request->query('date') ?? Carbon::now()->format('m-Y');
846846
$ladderService = $this->ladderService;
847847
return Cache::remember("getLadderPlayerFromPublicApi/$date/$game/$player", 5 * 60, function () use ($ladderService, $date, $game, $player)
848848
{

cncnet-api/app/Http/Controllers/ApiUserController.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,21 @@ public function getUserInfo(Request $request)
2626
try
2727
{
2828
$user = $request->user();
29-
return $user;
29+
30+
$elo = $user->userRatings()->get([
31+
'ladder_id',
32+
'rating',
33+
'deviation',
34+
'elo_rank',
35+
'alltime_rank',
36+
'rated_games',
37+
'active',
38+
]);
39+
40+
$userData = $user->toArray();
41+
$userData['elo'] = $elo;
42+
43+
return $userData;
3044
}
3145
catch (Exception $ex)
3246
{

cncnet-api/app/Http/Controllers/LadderController.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,6 @@ public function getLadderGame(Request $request, $date = null, $cncnetGame = null
409409
public function getLadderPlayer(Request $request, $date = null, $cncnetGame = null, $username = null)
410410
{
411411
$history = $this->ladderService->getActiveLadderByDate($date, $cncnetGame);
412-
$currentHistory = $history->ladder->currentHistory();
413412

414413
if ($history == null)
415414
{
@@ -476,10 +475,10 @@ public function getLadderPlayer(Request $request, $date = null, $cncnetGame = nu
476475
$recentAchievements = $this->achievementService->getRecentlyUnlockedAchievements($history, $user, 3);
477476
$achievementProgressCounts = $this->achievementService->getProgressCountsByUser($history, $user);
478477

479-
$isAnonymous = $player->user->userSettings->is_anonymous;
478+
$isAnonymous = $player->user->userSettings->getIsAnonymousForLadderHistory($history);
480479

481480
$ladderNicks = [];
482-
if (!$isAnonymous && $history->id != $currentHistory->id) // only hide if anonymous and is the current month
481+
if (!$isAnonymous) // only hide if anonymous and is the current month
483482
{
484483
$ladderNicks = $user->usernames
485484
->where('id', '!=', $player->id)

0 commit comments

Comments
 (0)