From bb569b4d3987b7f15e702620eedb22d2ed5cc269 Mon Sep 17 00:00:00 2001 From: Tecc Date: Mon, 8 Dec 2025 12:35:06 +0100 Subject: [PATCH] Fix: Arena - PersonalRating and MMR issue for bot teams (#1789) # Fix: Arena PersonalRating and MMR issue for bot teams ## Problem Bot arena teams are created with artificial random ratings (1000-2000 range), but when bots join these teams, their personal ratings and matchmaker ratings (MMR) use default config values instead of being adjusted to match the team's artificial rating. This causes matchmaking issues since the system uses personal ratings for queue calculations. ## Root Cause The issue occurred because `SetRatingForAll()` was called during team creation but only affected the captain. When additional bots were added later via `AddMember()`, they received default values from `CONFIG_ARENA_START_PERSONAL_RATING` and `CONFIG_ARENA_START_MATCHMAKER_RATING` instead of values appropriate for the team's artificial rating. ## Solution After bots are added to arena teams, the fix: 1. Uses `SetRatingForAll()` to align all personal ratings with team rating 2. Adjusts matchmaker ratings based on team context vs default configuration 3. Saves changes to both database tables with proper data types ## Impact - Personal ratings now match team ratings for artificial bot teams - MMR values are adjusted for artificial bot team ratings instead of using default config values - Arena matchmaking functions correctly for bot teams with random ratings - Only affects new arena team assignments after deployment - Existing player teams and normal config behavior are unaffected ## Manual Database Update For existing installations, the provided SQL script could be used to fix bot teams created before this patch. ### Update personal rating ```sql UPDATE arena_team_member atm JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId JOIN characters c ON atm.guid = c.guid JOIN auth.account a ON c.account = a.id SET atm.personalRating = at.rating WHERE a.username LIKE 'rndbot%' AND atm.personalRating != at.rating; ``` ### Update MMR for existing entries ```sql UPDATE character_arena_stats cas JOIN characters c ON cas.guid = c.guid JOIN auth.account a ON c.account = a.id JOIN arena_team_member atm ON cas.guid = atm.guid JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId SET cas.matchMakerRating = GREATEST(at.rating, 1500), -- Use team rating or 1500 minimum cas.maxMMR = GREATEST(cas.maxMMR, cas.matchMakerRating) -- Update maxMMR if needed WHERE a.username LIKE '%rndbot%' AND ( -- Update if MMR doesn't match team context (at.rating > 1500 AND cas.matchMakerRating < at.rating) OR (at.rating <= 1500 AND cas.matchMakerRating != 1500) OR cas.matchMakerRating IS NULL ) AND ( -- Map arena team type to character_arena_stats slot (at.type = 2 AND cas.slot = 0) OR -- 2v2 teams use slot 0 (at.type = 3 AND cas.slot = 1) OR -- 3v3 teams use slot 1 (at.type = 5 AND cas.slot = 2) -- 5v5 teams use slot 2 ); ``` ### Insert missing MMR records for bots without character_arena_stats entries ```sql INSERT INTO character_arena_stats (guid, slot, matchMakerRating, maxMMR) SELECT atm.guid, CASE WHEN at.type = 2 THEN 0 -- 2v2 -> slot 0 WHEN at.type = 3 THEN 1 -- 3v3 -> slot 1 WHEN at.type = 5 THEN 2 -- 5v5 -> slot 2 ELSE 0 END as slot, GREATEST(at.rating, 1500) as matchMakerRating, GREATEST(at.rating, 1500) as maxMMR FROM arena_team_member atm JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId JOIN characters c ON atm.guid = c.guid JOIN auth.account a ON c.account = a.id WHERE a.username LIKE '%rndbot%' AND NOT EXISTS ( SELECT 1 FROM character_arena_stats cas2 WHERE cas2.guid = atm.guid AND cas2.slot = CASE WHEN at.type = 2 THEN 0 WHEN at.type = 3 THEN 1 WHEN at.type = 5 THEN 2 ELSE 0 END ) AND at.rating > 0; ``` ## Related issues Fixes: #1787 Fixes: #1800 ## Verification Queries ### Query 1: Check personal rating alignment ```sql SELECT 'Personal Rating Check' as check_type, COUNT(*) as total_bot_members, SUM(CASE WHEN atm.personalRating = at.rating THEN 1 ELSE 0 END) as correct_ratings, SUM(CASE WHEN atm.personalRating != at.rating THEN 1 ELSE 0 END) as incorrect_ratings, ROUND(AVG(at.rating), 2) as avg_team_rating, ROUND(AVG(atm.personalRating), 2) as avg_personal_rating FROM arena_team_member atm JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId JOIN characters c ON atm.guid = c.guid JOIN auth.account a ON c.account = a.id WHERE a.username LIKE '%rndbot%'; ``` ### Query 2: Check MMR alignment ```sql SELECT 'MMR Alignment Check' as check_type, COUNT(*) as total_mmr_records, SUM(CASE WHEN at.rating > 1500 AND cas.matchMakerRating >= at.rating THEN 1 WHEN at.rating <= 1500 AND cas.matchMakerRating = 1500 THEN 1 ELSE 0 END) as correct_mmr, SUM(CASE WHEN at.rating > 1500 AND cas.matchMakerRating < at.rating THEN 1 WHEN at.rating <= 1500 AND cas.matchMakerRating != 1500 THEN 1 ELSE 0 END) as incorrect_mmr, ROUND(AVG(at.rating), 2) as avg_team_rating, ROUND(AVG(cas.matchMakerRating), 2) as avg_mmr, ROUND(AVG(cas.maxMMR), 2) as avg_max_mmr FROM arena_team_member atm JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId JOIN characters c ON atm.guid = c.guid JOIN auth.account a ON c.account = a.id JOIN character_arena_stats cas ON atm.guid = cas.guid WHERE a.username LIKE '%rndbot%' AND ( (at.type = 2 AND cas.slot = 0) OR (at.type = 3 AND cas.slot = 1) OR (at.type = 5 AND cas.slot = 2) ); ``` ### Query 3: Detailed team-by-team analysis ```sql SELECT at.arenaTeamId, at.name as team_name, at.type as team_type, at.rating as team_rating, COUNT(atm.guid) as member_count, GROUP_CONCAT(DISTINCT atm.personalRating) as personal_ratings, GROUP_CONCAT(DISTINCT cas.matchMakerRating) as mmr_values, CASE WHEN COUNT(DISTINCT atm.personalRating) = 1 AND MIN(atm.personalRating) = at.rating THEN 'OK' ELSE 'MISMATCH' END as personal_rating_status, CASE WHEN COUNT(DISTINCT cas.matchMakerRating) = 1 AND ( (at.rating > 1500 AND MIN(cas.matchMakerRating) >= at.rating) OR (at.rating <= 1500 AND MIN(cas.matchMakerRating) = 1500) ) THEN 'OK' ELSE 'MISMATCH' END as mmr_status FROM arena_team at JOIN arena_team_member atm ON at.arenaTeamId = atm.arenaTeamId JOIN characters c ON atm.guid = c.guid JOIN auth.account a ON c.account = a.id LEFT JOIN character_arena_stats cas ON atm.guid = cas.guid AND cas.slot = CASE WHEN at.type = 2 THEN 0 WHEN at.type = 3 THEN 1 WHEN at.type = 5 THEN 2 ELSE 0 END WHERE a.username LIKE '%rndbot%' GROUP BY at.arenaTeamId, at.name, at.type, at.rating ORDER BY at.rating DESC; ``` --- src/factory/PlayerbotFactory.cpp | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/factory/PlayerbotFactory.cpp b/src/factory/PlayerbotFactory.cpp index 65ffc444..50a216c9 100644 --- a/src/factory/PlayerbotFactory.cpp +++ b/src/factory/PlayerbotFactory.cpp @@ -4099,6 +4099,7 @@ void PlayerbotFactory::InitImmersive() void PlayerbotFactory::InitArenaTeam() { + if (!sPlayerbotAIConfig->IsInRandomAccountList(bot->GetSession()->GetAccountId())) return; @@ -4185,10 +4186,34 @@ void PlayerbotFactory::InitArenaTeam() if (botcaptain && botcaptain->GetTeamId() == bot->GetTeamId()) // need? { + // Add bot to arena team arenateam->AddMember(bot->GetGUID()); - arenateam->SaveToDB(); + + // Only synchronize ratings once the team is full (avoid redundant work) + // The captain was added with incorrect ratings when the team was created, + // so we fix everyone's ratings once the roster is complete + if (arenateam->GetMembersSize() >= (uint32)arenateam->GetType()) + { + uint32 teamRating = arenateam->GetRating(); + + // Use SetRatingForAll to align all members with team rating + arenateam->SetRatingForAll(teamRating); + + // For bot-only teams, keep MMR synchronized with team rating + // This ensures matchmaking reflects the artificial team strength (1000-2000 range) + // instead of being influenced by the global CONFIG_ARENA_START_MATCHMAKER_RATING + for (auto& member : arenateam->GetMembers()) + { + // Set MMR to match personal rating (which already matches team rating) + member.MatchMakerRating = member.PersonalRating; + member.MaxMMR = std::max(member.MaxMMR, member.PersonalRating); + } + // Force save all member data to database + arenateam->SaveToDB(true); + } } } + arenateams.erase(arenateams.begin() + index); }