Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions conf/CFBG.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,17 @@
# grouped players are never moved.
# Default: 1 - (Enabled)
# 0 - (Disabled)
#
# CFBG.BalanceTeamsAtStart.Enabled
# Description: When the gates open, if declined or ignored invites left the
# physical teams uneven by 2 or more, flip surplus entrants to
# the smaller side so the match starts within 1 of even (e.g. a
# 4v1 opens 3v2). Real battleground premades are never split; a
# solo player merely sitting in a social/questing party still
# counts as flippable. Decisions use live head counts only, so
# reservations that never showed up don't skew the repair.
# Default: 1 - (Enabled)
# 0 - (Disabled)

CFBG.Enable = 1
CFBG.Battlefield.Enable = 1
Expand All @@ -141,3 +152,4 @@ CFBG.ResetCooldowns = 0
CFBG.Show.PlayerName = 0
CFBG.RandomRaceSelection = 1
CFBG.BalanceTeamsOnEntry.Enabled = 1
CFBG.BalanceTeamsAtStart.Enabled = 1
173 changes: 164 additions & 9 deletions src/CFBG.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
#include "Chat.h"
#include "Config.h"
#include "Containers.h"
#include "Group.h"
#include "Language.h"
#include "Log.h"
#include "ObjectAccessor.h"
#include "Opcodes.h"
#include "ReputationMgr.h"
Expand Down Expand Up @@ -136,6 +138,7 @@ void CFBG::LoadConfig()
_IsEnableBalanceClassLowLevel = sConfigMgr->GetOption<bool>("CFBG.BalancedTeams.Class.LowLevel", true);
_IsEnableResetCooldowns = sConfigMgr->GetOption<bool>("CFBG.ResetCooldowns", false);
_IsEnableBalanceTeamsOnEntry = sConfigMgr->GetOption<bool>("CFBG.BalanceTeamsOnEntry.Enabled", true);
_IsEnableBalanceTeamsAtStart = sConfigMgr->GetOption<bool>("CFBG.BalanceTeamsAtStart.Enabled", true);
_showPlayerName = sConfigMgr->GetOption<bool>("CFBG.Show.PlayerName", false);
_EvenTeamsMaxPlayersThreshold = sConfigMgr->GetOption<uint32>("CFBG.EvenTeams.MaxPlayersThreshold", 0);
_MaxPlayersCountInGroup = sConfigMgr->GetOption<uint32>("CFBG.Players.Count.In.Group", 3);
Expand Down Expand Up @@ -341,11 +344,6 @@ void CFBG::EnforceBGTeamConsistency(Player* player)
if (!bg || bg->isArena())
return;

// EndBattleground already restored real identities; re-faking a ghost who
// reclaims during WAIT_LEAVE would double-morph him for the rest of the window.
if (bg->GetStatus() == STATUS_WAIT_LEAVE)
return;

TeamId const assigned = player->GetBgTeamId();

// Native: must not carry a fake.
Expand Down Expand Up @@ -392,8 +390,11 @@ void CFBG::BalanceTeamsOnEntry(Battleground* bg, Player* player)
if (bg->isArena() || bg->isRated())
return;

// Solo entrants only: never split a party across teams.
if (player->GetGroup())
// Never split a genuine BG premade materialising here, but a solo-queued
// player who merely sits in a social/questing party (e.g. a duo auto-queued
// as two separate solo entries) is still eligible. At this hook -- before BG
// raid placement -- GetGroup() is the social party.
if (IsPartyCommittedToBG(player, player->GetGroup(), bg))
return;

// Genuine first entry only: skip relog re-adds (already in the BG), otherwise
Expand Down Expand Up @@ -452,6 +453,150 @@ void CFBG::BalanceTeamsOnEntry(Battleground* bg, Player* player)
startPos->GetPositionZ(), startPos->GetOrientation());
}

bool CFBG::IsPartyCommittedToBG(Player* player, Group* group, Battleground* bg)
{
if (!group)
return false;

for (auto const& slot : group->GetMemberSlots())
{
if (slot.guid == player->GetGUID())
continue;

// Already standing in this instance.
if (bg->GetPlayers().find(slot.guid) != bg->GetPlayers().end())
return true;

// Or still porting in on an invite to it. An offline member can't be on
// his way, so he never blocks the flip.
Player* member = ObjectAccessor::FindConnectedPlayer(slot.guid);
if (member && member->IsInvitedForBattlegroundInstance(bg->GetInstanceID()))
return true;
}

return false;
}

void CFBG::BalanceTeamsAtStart(Battleground* bg)
{
// The gates just opened. Team selection was balanced when the invites went
// out, but same-side no-shows with an empty backfill queue can leave the
// physical teams grossly uneven (4v1). Nothing re-checks the split before the
// doors open, so do it here: flip surplus entrants onto the smaller side
// until the diff is at most 1.
if (!IsEnableSystem() || !IsEnableBalanceTeamsAtStart())
return;

if (!bg || bg->isArena() || bg->isRated())
return;

// Decide on physical head counts only: the pending reservations that never
// materialised are exactly what produced the imbalance, so the invited ledger
// must not steer the repair. Each flip shrinks the diff by 2, so the loop
// terminates when the teams are within 1 or no flippable candidate is left.
while (true)
{
uint32 const countA = bg->GetPlayersCountByTeam(TEAM_ALLIANCE);
uint32 const countH = bg->GetPlayersCountByTeam(TEAM_HORDE);
uint32 const diff = countA > countH ? countA - countH : countH - countA;

if (diff < 2)
break;

TeamId const larger = countA > countH ? TEAM_ALLIANCE : TEAM_HORDE;
TeamId const smaller = larger == TEAM_ALLIANCE ? TEAM_HORDE : TEAM_ALLIANCE;

// Prefer flipping a faked player whose real faction is the smaller side:
// the flip just unfakes him back to native (least disruption). Otherwise
// take any flippable player on the larger side.
Player* toFlip = nullptr;
Player* fallback = nullptr;

for (auto const& [guid, player] : bg->GetPlayers())
{
if (!player || player->GetBgTeamId() != larger)
continue;

// Never split a real premade. Inside the BG the social party is the
// original group -- GetGroup() is the BG raid at this point.
if (IsPartyCommittedToBG(player, player->GetOriginalGroup(), bg))
continue;

if (IsPlayerFake(player) && player->GetTeamId(true) == smaller)
{
toFlip = player;
break;
}

if (!fallback)
fallback = player;
}

if (!toFlip)
toFlip = fallback;

// Nothing left to flip: any residual imbalance is rooted in premades we
// won't split. Open the match as-is -- the existing 5-minute premature
// finish path handles a still-degenerate game, exactly as today.
if (!toFlip)
break;

// Keep both the physical counts and the invited ledger zero-sum with the
// player's future leave-time decrement (mirrors BalanceTeamsOnEntry).
bg->UpdatePlayersCountByTeam(larger, true);
bg->UpdatePlayersCountByTeam(smaller, false);
bg->DecreaseInvitedCount(larger);
bg->IncreaseInvitedCount(smaller);
toFlip->GetBGData().bgTeamId = smaller;

// Move him into the smaller side's BG raid. Remove from the old raid
// first: AddOrSetPlayerToCorrectBgGroup early-returns while the player is
// still in a BG group.
if (Group* oldRaid = bg->GetBgRaid(larger))
if (oldRaid->IsMember(toFlip->GetGUID()))
if (!oldRaid->RemoveMember(toFlip->GetGUID())) // group was disbanded
bg->SetBgRaid(larger, nullptr);
bg->AddOrSetPlayerToCorrectBgGroup(toFlip, smaller);

// Apply/clear/redo the fake for the new side.
EnforceBGTeamConsistency(toFlip);

// The flip changed his race/faction; refresh every client's cached
// identity for him (and his for theirs) so nobody keeps the pre-flip
// race in their name-query cache -- same path a fresh entrant takes via
// OnBattlegroundAddPlayer.
FitPlayerInTeam(toFlip, bg);

// AV forced reactions track the assigned side, so refresh them for a
// player who is now cross-faction (a now-native player had them cleared
// by the unfake). Mirrors ValidatePlayerForBG's entry-time handling.
if (!IsPlayingNative(toFlip) && bg->GetMapId() == MapAlteracValley)
{
if (smaller == TEAM_HORDE)
{
toFlip->GetReputationMgr().ApplyForceReaction(FACTION_FROSTWOLF_CLAN, REP_FRIENDLY, true);
toFlip->GetReputationMgr().ApplyForceReaction(FACTION_STORMPIKE_GUARD, REP_HOSTILE, true);
}
else
{
toFlip->GetReputationMgr().ApplyForceReaction(FACTION_FROSTWOLF_CLAN, REP_HOSTILE, true);
toFlip->GetReputationMgr().ApplyForceReaction(FACTION_STORMPIKE_GUARD, REP_FRIENDLY, true);
}

toFlip->GetReputationMgr().SendForceReactions();
}

// Move him from his old base to the smaller side's.
Position const* startPos = bg->GetTeamStartPosition(smaller);
toFlip->TeleportTo(bg->GetMapId(), startPos->GetPositionX(), startPos->GetPositionY(),
startPos->GetPositionZ(), startPos->GetOrientation());

LOG_DEBUG("module", "mod-cfbg: BalanceTeamsAtStart flipped {} to {} in instance {} ({}v{})",
toFlip->GetName(), static_cast<uint32>(smaller), bg->GetInstanceID(),
bg->GetPlayersCountByTeam(TEAM_ALLIANCE), bg->GetPlayersCountByTeam(TEAM_HORDE));
}
}

uint32 CFBG::GetMorphFromRace(uint8 race, uint8 gender)
{
switch (race)
Expand Down Expand Up @@ -779,8 +924,18 @@ std::array<uint32, 2> CFBG::GetProjectedBaseCounts(Battleground* bg, Battlegroun
// accept deleted their ginfo; AddPlayer has not run yet). The BG's invited
// ledger still holds every reservation, so clamp up to it; max() degrades
// gracefully if either register is skewed.
counts[TEAM_ALLIANCE] = std::max(counts[TEAM_ALLIANCE], bg->GetInvitedCount(TEAM_ALLIANCE));
counts[TEAM_HORDE] = std::max(counts[TEAM_HORDE], bg->GetInvitedCount(TEAM_HORDE));
uint32 const computedA = counts[TEAM_ALLIANCE];
uint32 const computedH = counts[TEAM_HORDE];
counts[TEAM_ALLIANCE] = std::max(computedA, bg->GetInvitedCount(TEAM_ALLIANCE));
counts[TEAM_HORDE] = std::max(computedH, bg->GetInvitedCount(TEAM_HORDE));

// The ledger exceeding the physical + invited-queued tally is the signature
// of a leaked reservation steering selection. In-flight accepts trip this
// briefly and legitimately, so it stays at debug for operators hunting a
// persistent skew.
if (counts[TEAM_ALLIANCE] > computedA || counts[TEAM_HORDE] > computedH)
LOG_DEBUG("module", "mod-cfbg: instance {} projections clamped by invited ledger (A {}->{}, H {}->{}), possible phantom reservation",
bg->GetInstanceID(), computedA, counts[TEAM_ALLIANCE], computedH, counts[TEAM_HORDE]);

return counts;
}
Expand Down
15 changes: 15 additions & 0 deletions src/CFBG.h
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class CFBG
inline bool IsEnableEvenTeams() const { return _IsEnableEvenTeams; }
inline bool IsEnableResetCooldowns() const { return _IsEnableResetCooldowns; }
inline bool IsEnableBalanceTeamsOnEntry() const { return _IsEnableBalanceTeamsOnEntry; }
inline bool IsEnableBalanceTeamsAtStart() const { return _IsEnableBalanceTeamsAtStart; }
inline uint32 EvenTeamsMaxPlayersThreshold() const { return _EvenTeamsMaxPlayersThreshold; }
inline uint32 GetMaxPlayersCountInGroup() const { return _MaxPlayersCountInGroup; }
inline uint8 GetBalanceClassMinLevel() const { return _balanceClassMinLevel; }
Expand Down Expand Up @@ -165,6 +166,12 @@ class CFBG
// Forces race/faction/m_team/fake-store into agreement with the player's
// assigned BG team (GetBgTeamId()); idempotent and self-correcting.
void EnforceBGTeamConsistency(Player* player);

// Doors-open head-count repair: when declined/no-show invites left the teams
// grossly uneven (diff >= 2) once the gates open, flip surplus solo entrants
// to the smaller side so the match starts within 1 of even. Real BG premades
// are never split. Fired from OnBattlegroundStart.
void BalanceTeamsAtStart(Battleground* bg);
void SetFakeRaceAndMorph(Player* player);
void SetFakeRaceAndMorphForBF(Player* player, TeamId assignedTeam);
void SetFactionForRace(Player* player, uint8 Race, TeamId teamId);
Expand Down Expand Up @@ -202,6 +209,13 @@ class CFBG
// Arrival-time head-count correction for solo entrants.
void BalanceTeamsOnEntry(Battleground* bg, Player* player);

// True when another member of `player`'s social party is already committed to
// this bg instance (already entered, or holding an invite to it). Flipping the
// player would then split a real premade that is materializing here, so the
// head-count repairs leave them put. `group` is the social party: GetGroup()
// before the BG raid is assigned (entry hook), GetOriginalGroup() once inside.
bool IsPartyCommittedToBG(Player* player, Group* group, Battleground* bg);

RandomSkinInfo GetRandomRaceMorph(TeamId team, uint8 playerClass, uint8 gender);

uint32 GetMorphFromRace(uint8 race, uint8 gender);
Expand Down Expand Up @@ -242,6 +256,7 @@ class CFBG
bool _IsEnableEvenTeams;
bool _IsEnableResetCooldowns;
bool _IsEnableBalanceTeamsOnEntry;
bool _IsEnableBalanceTeamsAtStart;
bool _showPlayerName;
bool _randomizeRaces;
uint32 _EvenTeamsMaxPlayersThreshold;
Expand Down
45 changes: 29 additions & 16 deletions src/CFBG_SC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class CFBG_BG : public BGScript
CFBG_BG() : BGScript("CFBG_BG", {
ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_BEFORE_ADD_PLAYER,
ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_ADD_PLAYER,
ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_END_REWARD,
ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_START,
ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_REMOVE_PLAYER_AT_LEAVE,
ALLBATTLEGROUNDHOOK_ON_ADD_GROUP,
ALLBATTLEGROUNDHOOK_CAN_FILL_PLAYERS_TO_BG,
Expand All @@ -34,6 +34,11 @@ class CFBG_BG : public BGScript
sCFBG->ValidatePlayerForBG(bg, player);
}

void OnBattlegroundStart(Battleground* bg) override
{
sCFBG->BalanceTeamsAtStart(bg);
}

void OnBattlegroundAddPlayer(Battleground* bg, Player* player) override
{
sCFBG->FitPlayerInTeam(player, bg);
Expand All @@ -42,15 +47,6 @@ class CFBG_BG : public BGScript
player->RemoveArenaSpellCooldowns(true);
}

void OnBattlegroundEndReward(Battleground* bg, Player* player, TeamId /*winnerTeamId*/) override
{
if (!sCFBG->IsEnableSystem() || !bg || !player || bg->isArena())
return;

if (sCFBG->IsPlayerFake(player))
sCFBG->ClearFakePlayer(player);
}

void OnBattlegroundRemovePlayerAtLeave(Battleground* bg, Player* player) override
{
if (!sCFBG->IsEnableSystem() || bg->isArena())
Expand Down Expand Up @@ -104,6 +100,7 @@ class CFBG_Player : public PlayerScript
PLAYERHOOK_ON_LOGIN,
PLAYERHOOK_ON_LOGOUT,
PLAYERHOOK_ON_UPDATE_ZONE,
PLAYERHOOK_ON_UPDATE_FACTION,
PLAYERHOOK_CAN_JOIN_IN_BATTLEGROUND_QUEUE,
PLAYERHOOK_ON_BEFORE_UPDATE,
PLAYERHOOK_ON_BEFORE_SEND_CHAT_MESSAGE,
Expand Down Expand Up @@ -156,12 +153,12 @@ class CFBG_Player : public PlayerScript
if (!sCFBG->IsPlayerFake(player))
return;

// Battleground fakes are owned by the BG hooks (OnBattlegroundRemovePlayerAtLeave
// / OnBattlegroundEndReward), not this WG cleanup. A battleground zone is not a
// WG battlefield, so without this guard entering a BG would clear a cross-faction
// player's fake right after the entry morph (Battleground::AddPlayer runs before
// UpdateZone), leaving GetTeamId() on the real faction while bgTeamId stays on the
// assigned side -- the flag-capture/win desync. WG players are not InBattleground().
// Battleground fakes are owned by the BG hook OnBattlegroundRemovePlayerAtLeave,
// not this WG cleanup. A battleground zone is not a WG battlefield, so without
// this guard entering a BG would clear a cross-faction player's fake right after
// the entry morph (Battleground::AddPlayer runs before UpdateZone), leaving
// GetTeamId() on the real faction while bgTeamId stays on the assigned side --
// the flag-capture/win desync. WG players are not InBattleground().
if (player->InBattleground())
return;

Expand All @@ -170,6 +167,22 @@ class CFBG_Player : public PlayerScript
sCFBG->ClearFakePlayer(player);
}

// Core's Player::SetFactionForRace resets m_team to the real race, then
// applies the real faction template only when m_team matches that race --
// this hook is the designed cooperation point. Keep m_team on the fake side
// so a faked BG player's assigned faction survives core faction resets (a
// stray or redundant .gm off would otherwise revert nameplate/hostility/team
// until the next resurrect).
void OnPlayerUpdateFaction(Player* player) override
{
if (!sCFBG->IsEnableSystem() || !player->InBattleground())
return;

if (FakePlayer const* fake = sCFBG->GetFakePlayer(player))
if (player->GetTeamId() != fake->FakeTeamID)
player->setTeamId(fake->FakeTeamID);
}

bool OnPlayerCanJoinInBattlegroundQueue(Player* player, ObjectGuid /*BattlemasterGuid*/ , BattlegroundTypeId /*BGTypeID*/, uint8 joinAsGroup, GroupJoinBattlegroundResult& err) override
{
if (!sCFBG->IsEnableSystem())
Expand Down
Loading