|
11 | 11 | #include "Chat.h" |
12 | 12 | #include "Config.h" |
13 | 13 | #include "Containers.h" |
| 14 | +#include "Group.h" |
14 | 15 | #include "Language.h" |
| 16 | +#include "Log.h" |
15 | 17 | #include "ObjectAccessor.h" |
16 | 18 | #include "Opcodes.h" |
17 | 19 | #include "ReputationMgr.h" |
@@ -136,6 +138,7 @@ void CFBG::LoadConfig() |
136 | 138 | _IsEnableBalanceClassLowLevel = sConfigMgr->GetOption<bool>("CFBG.BalancedTeams.Class.LowLevel", true); |
137 | 139 | _IsEnableResetCooldowns = sConfigMgr->GetOption<bool>("CFBG.ResetCooldowns", false); |
138 | 140 | _IsEnableBalanceTeamsOnEntry = sConfigMgr->GetOption<bool>("CFBG.BalanceTeamsOnEntry.Enabled", true); |
| 141 | + _IsEnableBalanceTeamsAtStart = sConfigMgr->GetOption<bool>("CFBG.BalanceTeamsAtStart.Enabled", true); |
139 | 142 | _showPlayerName = sConfigMgr->GetOption<bool>("CFBG.Show.PlayerName", false); |
140 | 143 | _EvenTeamsMaxPlayersThreshold = sConfigMgr->GetOption<uint32>("CFBG.EvenTeams.MaxPlayersThreshold", 0); |
141 | 144 | _MaxPlayersCountInGroup = sConfigMgr->GetOption<uint32>("CFBG.Players.Count.In.Group", 3); |
@@ -341,11 +344,6 @@ void CFBG::EnforceBGTeamConsistency(Player* player) |
341 | 344 | if (!bg || bg->isArena()) |
342 | 345 | return; |
343 | 346 |
|
344 | | - // EndBattleground already restored real identities; re-faking a ghost who |
345 | | - // reclaims during WAIT_LEAVE would double-morph him for the rest of the window. |
346 | | - if (bg->GetStatus() == STATUS_WAIT_LEAVE) |
347 | | - return; |
348 | | - |
349 | 347 | TeamId const assigned = player->GetBgTeamId(); |
350 | 348 |
|
351 | 349 | // Native: must not carry a fake. |
@@ -392,8 +390,11 @@ void CFBG::BalanceTeamsOnEntry(Battleground* bg, Player* player) |
392 | 390 | if (bg->isArena() || bg->isRated()) |
393 | 391 | return; |
394 | 392 |
|
395 | | - // Solo entrants only: never split a party across teams. |
396 | | - if (player->GetGroup()) |
| 393 | + // Never split a genuine BG premade materialising here, but a solo-queued |
| 394 | + // player who merely sits in a social/questing party (e.g. a duo auto-queued |
| 395 | + // as two separate solo entries) is still eligible. At this hook -- before BG |
| 396 | + // raid placement -- GetGroup() is the social party. |
| 397 | + if (IsPartyCommittedToBG(player, player->GetGroup(), bg)) |
397 | 398 | return; |
398 | 399 |
|
399 | 400 | // Genuine first entry only: skip relog re-adds (already in the BG), otherwise |
@@ -452,6 +453,150 @@ void CFBG::BalanceTeamsOnEntry(Battleground* bg, Player* player) |
452 | 453 | startPos->GetPositionZ(), startPos->GetOrientation()); |
453 | 454 | } |
454 | 455 |
|
| 456 | +bool CFBG::IsPartyCommittedToBG(Player* player, Group* group, Battleground* bg) |
| 457 | +{ |
| 458 | + if (!group) |
| 459 | + return false; |
| 460 | + |
| 461 | + for (auto const& slot : group->GetMemberSlots()) |
| 462 | + { |
| 463 | + if (slot.guid == player->GetGUID()) |
| 464 | + continue; |
| 465 | + |
| 466 | + // Already standing in this instance. |
| 467 | + if (bg->GetPlayers().find(slot.guid) != bg->GetPlayers().end()) |
| 468 | + return true; |
| 469 | + |
| 470 | + // Or still porting in on an invite to it. An offline member can't be on |
| 471 | + // his way, so he never blocks the flip. |
| 472 | + Player* member = ObjectAccessor::FindConnectedPlayer(slot.guid); |
| 473 | + if (member && member->IsInvitedForBattlegroundInstance(bg->GetInstanceID())) |
| 474 | + return true; |
| 475 | + } |
| 476 | + |
| 477 | + return false; |
| 478 | +} |
| 479 | + |
| 480 | +void CFBG::BalanceTeamsAtStart(Battleground* bg) |
| 481 | +{ |
| 482 | + // The gates just opened. Team selection was balanced when the invites went |
| 483 | + // out, but same-side no-shows with an empty backfill queue can leave the |
| 484 | + // physical teams grossly uneven (4v1). Nothing re-checks the split before the |
| 485 | + // doors open, so do it here: flip surplus entrants onto the smaller side |
| 486 | + // until the diff is at most 1. |
| 487 | + if (!IsEnableSystem() || !IsEnableBalanceTeamsAtStart()) |
| 488 | + return; |
| 489 | + |
| 490 | + if (!bg || bg->isArena() || bg->isRated()) |
| 491 | + return; |
| 492 | + |
| 493 | + // Decide on physical head counts only: the pending reservations that never |
| 494 | + // materialised are exactly what produced the imbalance, so the invited ledger |
| 495 | + // must not steer the repair. Each flip shrinks the diff by 2, so the loop |
| 496 | + // terminates when the teams are within 1 or no flippable candidate is left. |
| 497 | + while (true) |
| 498 | + { |
| 499 | + uint32 const countA = bg->GetPlayersCountByTeam(TEAM_ALLIANCE); |
| 500 | + uint32 const countH = bg->GetPlayersCountByTeam(TEAM_HORDE); |
| 501 | + uint32 const diff = countA > countH ? countA - countH : countH - countA; |
| 502 | + |
| 503 | + if (diff < 2) |
| 504 | + break; |
| 505 | + |
| 506 | + TeamId const larger = countA > countH ? TEAM_ALLIANCE : TEAM_HORDE; |
| 507 | + TeamId const smaller = larger == TEAM_ALLIANCE ? TEAM_HORDE : TEAM_ALLIANCE; |
| 508 | + |
| 509 | + // Prefer flipping a faked player whose real faction is the smaller side: |
| 510 | + // the flip just unfakes him back to native (least disruption). Otherwise |
| 511 | + // take any flippable player on the larger side. |
| 512 | + Player* toFlip = nullptr; |
| 513 | + Player* fallback = nullptr; |
| 514 | + |
| 515 | + for (auto const& [guid, player] : bg->GetPlayers()) |
| 516 | + { |
| 517 | + if (!player || player->GetBgTeamId() != larger) |
| 518 | + continue; |
| 519 | + |
| 520 | + // Never split a real premade. Inside the BG the social party is the |
| 521 | + // original group -- GetGroup() is the BG raid at this point. |
| 522 | + if (IsPartyCommittedToBG(player, player->GetOriginalGroup(), bg)) |
| 523 | + continue; |
| 524 | + |
| 525 | + if (IsPlayerFake(player) && player->GetTeamId(true) == smaller) |
| 526 | + { |
| 527 | + toFlip = player; |
| 528 | + break; |
| 529 | + } |
| 530 | + |
| 531 | + if (!fallback) |
| 532 | + fallback = player; |
| 533 | + } |
| 534 | + |
| 535 | + if (!toFlip) |
| 536 | + toFlip = fallback; |
| 537 | + |
| 538 | + // Nothing left to flip: any residual imbalance is rooted in premades we |
| 539 | + // won't split. Open the match as-is -- the existing 5-minute premature |
| 540 | + // finish path handles a still-degenerate game, exactly as today. |
| 541 | + if (!toFlip) |
| 542 | + break; |
| 543 | + |
| 544 | + // Keep both the physical counts and the invited ledger zero-sum with the |
| 545 | + // player's future leave-time decrement (mirrors BalanceTeamsOnEntry). |
| 546 | + bg->UpdatePlayersCountByTeam(larger, true); |
| 547 | + bg->UpdatePlayersCountByTeam(smaller, false); |
| 548 | + bg->DecreaseInvitedCount(larger); |
| 549 | + bg->IncreaseInvitedCount(smaller); |
| 550 | + toFlip->GetBGData().bgTeamId = smaller; |
| 551 | + |
| 552 | + // Move him into the smaller side's BG raid. Remove from the old raid |
| 553 | + // first: AddOrSetPlayerToCorrectBgGroup early-returns while the player is |
| 554 | + // still in a BG group. |
| 555 | + if (Group* oldRaid = bg->GetBgRaid(larger)) |
| 556 | + if (oldRaid->IsMember(toFlip->GetGUID())) |
| 557 | + if (!oldRaid->RemoveMember(toFlip->GetGUID())) // group was disbanded |
| 558 | + bg->SetBgRaid(larger, nullptr); |
| 559 | + bg->AddOrSetPlayerToCorrectBgGroup(toFlip, smaller); |
| 560 | + |
| 561 | + // Apply/clear/redo the fake for the new side. |
| 562 | + EnforceBGTeamConsistency(toFlip); |
| 563 | + |
| 564 | + // The flip changed his race/faction; refresh every client's cached |
| 565 | + // identity for him (and his for theirs) so nobody keeps the pre-flip |
| 566 | + // race in their name-query cache -- same path a fresh entrant takes via |
| 567 | + // OnBattlegroundAddPlayer. |
| 568 | + FitPlayerInTeam(toFlip, bg); |
| 569 | + |
| 570 | + // AV forced reactions track the assigned side, so refresh them for a |
| 571 | + // player who is now cross-faction (a now-native player had them cleared |
| 572 | + // by the unfake). Mirrors ValidatePlayerForBG's entry-time handling. |
| 573 | + if (!IsPlayingNative(toFlip) && bg->GetMapId() == MapAlteracValley) |
| 574 | + { |
| 575 | + if (smaller == TEAM_HORDE) |
| 576 | + { |
| 577 | + toFlip->GetReputationMgr().ApplyForceReaction(FACTION_FROSTWOLF_CLAN, REP_FRIENDLY, true); |
| 578 | + toFlip->GetReputationMgr().ApplyForceReaction(FACTION_STORMPIKE_GUARD, REP_HOSTILE, true); |
| 579 | + } |
| 580 | + else |
| 581 | + { |
| 582 | + toFlip->GetReputationMgr().ApplyForceReaction(FACTION_FROSTWOLF_CLAN, REP_HOSTILE, true); |
| 583 | + toFlip->GetReputationMgr().ApplyForceReaction(FACTION_STORMPIKE_GUARD, REP_FRIENDLY, true); |
| 584 | + } |
| 585 | + |
| 586 | + toFlip->GetReputationMgr().SendForceReactions(); |
| 587 | + } |
| 588 | + |
| 589 | + // Move him from his old base to the smaller side's. |
| 590 | + Position const* startPos = bg->GetTeamStartPosition(smaller); |
| 591 | + toFlip->TeleportTo(bg->GetMapId(), startPos->GetPositionX(), startPos->GetPositionY(), |
| 592 | + startPos->GetPositionZ(), startPos->GetOrientation()); |
| 593 | + |
| 594 | + LOG_DEBUG("module", "mod-cfbg: BalanceTeamsAtStart flipped {} to {} in instance {} ({}v{})", |
| 595 | + toFlip->GetName(), static_cast<uint32>(smaller), bg->GetInstanceID(), |
| 596 | + bg->GetPlayersCountByTeam(TEAM_ALLIANCE), bg->GetPlayersCountByTeam(TEAM_HORDE)); |
| 597 | + } |
| 598 | +} |
| 599 | + |
455 | 600 | uint32 CFBG::GetMorphFromRace(uint8 race, uint8 gender) |
456 | 601 | { |
457 | 602 | switch (race) |
@@ -779,8 +924,18 @@ std::array<uint32, 2> CFBG::GetProjectedBaseCounts(Battleground* bg, Battlegroun |
779 | 924 | // accept deleted their ginfo; AddPlayer has not run yet). The BG's invited |
780 | 925 | // ledger still holds every reservation, so clamp up to it; max() degrades |
781 | 926 | // gracefully if either register is skewed. |
782 | | - counts[TEAM_ALLIANCE] = std::max(counts[TEAM_ALLIANCE], bg->GetInvitedCount(TEAM_ALLIANCE)); |
783 | | - counts[TEAM_HORDE] = std::max(counts[TEAM_HORDE], bg->GetInvitedCount(TEAM_HORDE)); |
| 927 | + uint32 const computedA = counts[TEAM_ALLIANCE]; |
| 928 | + uint32 const computedH = counts[TEAM_HORDE]; |
| 929 | + counts[TEAM_ALLIANCE] = std::max(computedA, bg->GetInvitedCount(TEAM_ALLIANCE)); |
| 930 | + counts[TEAM_HORDE] = std::max(computedH, bg->GetInvitedCount(TEAM_HORDE)); |
| 931 | + |
| 932 | + // The ledger exceeding the physical + invited-queued tally is the signature |
| 933 | + // of a leaked reservation steering selection. In-flight accepts trip this |
| 934 | + // briefly and legitimately, so it stays at debug for operators hunting a |
| 935 | + // persistent skew. |
| 936 | + if (counts[TEAM_ALLIANCE] > computedA || counts[TEAM_HORDE] > computedH) |
| 937 | + LOG_DEBUG("module", "mod-cfbg: instance {} projections clamped by invited ledger (A {}->{}, H {}->{}), possible phantom reservation", |
| 938 | + bg->GetInstanceID(), computedA, counts[TEAM_ALLIANCE], computedH, counts[TEAM_HORDE]); |
784 | 939 |
|
785 | 940 | return counts; |
786 | 941 | } |
|
0 commit comments