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
51 changes: 48 additions & 3 deletions src/CFBG.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "Config.h"
#include "Containers.h"
#include "Language.h"
#include "ObjectAccessor.h"
#include "Opcodes.h"
#include "ReputationMgr.h"
#include "ScriptMgr.h"
Expand Down Expand Up @@ -92,9 +93,30 @@ CFBG* CFBG::instance()

void CFBG::LoadConfig()
{
bool const wasEnabled = _IsEnableSystem;
_IsEnableSystem = sConfigMgr->GetOption<bool>("CFBG.Enable", false);
if (!_IsEnableSystem)
{
// Live-disable via .reload: restore every online faked player and drop
// all Player*-keyed state; otherwise they stay cross-faction until
// relog and the stale keys can corrupt a later Player reusing the
// same address.
if (wasEnabled)
{
for (auto const& [guid, player] : ObjectAccessor::GetPlayers())
if (IsPlayerFake(player))
ClearFakePlayer(player);

// Anything left is a leaked entry whose Player was already
// deleted; drop it without dereferencing the key.
_fakePlayerStore.clear();
_fakeNamePlayersStore.clear();
_forgetBGPlayersStore.clear();
_forgetInListPlayersStore.clear();
}

return;
}

_IsEnableWGSystem = sConfigMgr->GetOption<bool>("CFBG.Battlefield.Enable", true);
_IsEnableWGTeamLock = sConfigMgr->GetOption<bool>("CFBG.Battlefield.TeamLock.Enable", true);
Expand Down Expand Up @@ -320,6 +342,11 @@ 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 @@ -588,19 +615,28 @@ void CFBG::SetWGWarAssignment(ObjectGuid guid, TeamId team)

void CFBG::ClearWGWarAssignments()
{
// A player re-faked at zone entry whose invite was still pending at war
// end is in no PlayersInWar set, so the war-end unfake loop misses him;
// sweep the assignments so no fake survives the war. Players inside a
// battleground are skipped: their fake belongs to the BG lifecycle.
for (auto const& [guid, teamId] : _wgWarAssignmentStore)
if (Player* player = ObjectAccessor::FindPlayer(guid))
if (!player->InBattleground() && IsPlayerFake(player))
ClearFakePlayer(player);

_wgWarAssignmentStore.clear();
_wgCensusValid = false;
_wgMajorityNativeKept = 0;
}

TeamId CFBG::ResolveWGWarTeam(Player* player, uint32 nativeAllianceInvited, uint32 nativeHordeInvited)
TeamId CFBG::ResolveWGWarTeam(Player* player, uint32 nativeAllianceInvited, uint32 nativeHordeInvited, uint32 allianceInWar, uint32 hordeInWar)
{
// Capture the native split once: at the first join PlayersInWar is empty
// and nobody is faked, so the invited counts are the true native census.
if (!_wgCensusValid)
{
_wgMajorityTeam = (nativeAllianceInvited >= nativeHordeInvited) ? TEAM_ALLIANCE : TEAM_HORDE;
_wgMajorityFairShare = (nativeAllianceInvited + nativeHordeInvited) / 2;
_wgMajorityFairShare = (nativeAllianceInvited + nativeHordeInvited + 1) / 2;
_wgMajorityNativeKept = 0;
_wgCensusValid = true;
}
Expand All @@ -619,7 +655,16 @@ TeamId CFBG::ResolveWGWarTeam(Player* player, uint32 nativeAllianceInvited, uint
return realTeam;
}

return (_wgMajorityTeam == TEAM_ALLIANCE) ? TEAM_HORDE : TEAM_ALLIANCE;
// Past the fair share: flip only when it does not worsen the live balance —
// with a sparse census (e.g. 1/0) and trickle joins, unconditional flips
// would stack the entire majority onto the other side.
uint32 const ownInWar = (realTeam == TEAM_ALLIANCE) ? allianceInWar : hordeInWar;
uint32 const otherInWar = (realTeam == TEAM_ALLIANCE) ? hordeInWar : allianceInWar;
if (ownInWar > otherInWar)
return (_wgMajorityTeam == TEAM_ALLIANCE) ? TEAM_HORDE : TEAM_ALLIANCE;

++_wgMajorityNativeKept;
return realTeam;
}

void CFBG::DoForgetPlayersInList(Player* player)
Expand Down
12 changes: 8 additions & 4 deletions src/CFBG.h
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,11 @@ class CFBG
void ClearWGWarAssignments();

// Native-priority WG balancing: keep the minority native, flip only the
// majority's surplus (latest accepters). Census captured on the first call
// of a war, reset in ClearWGWarAssignments.
TeamId ResolveWGWarTeam(Player* player, uint32 nativeAllianceInvited, uint32 nativeHordeInvited);
// majority's surplus (latest accepters). The fair share is a ceiling; past
// it a majority joiner flips only when the flip does not worsen the live
// war counts. Census captured on the first call of a war, reset in
// ClearWGWarAssignments.
TeamId ResolveWGWarTeam(Player* player, uint32 nativeAllianceInvited, uint32 nativeHordeInvited, uint32 allianceInWar, uint32 hordeInWar);

bool ShouldForgetInListPlayers(Player* player);
bool IsPlayingNative(Player* player);
Expand Down Expand Up @@ -227,7 +229,9 @@ class CFBG
std::array<CFBGRaceInfo, 9> _raceInfo{};

// For config
bool _IsEnableSystem;
// = false so LoadConfig's disable-transition read is well-defined on the
// very first load.
bool _IsEnableSystem = false;
bool _IsEnableWGSystem;
bool _IsEnableWGTeamLock;
bool _IsEnableWGNativePriority;
Expand Down
46 changes: 45 additions & 1 deletion src/CFBG_SC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -273,11 +273,41 @@ class CFBG_Battlefield : public BattlefieldScript
{
public:
CFBG_Battlefield() : BattlefieldScript("CFBG_Battlefield", {
BATTLEFIELDHOOK_ON_PLAYER_ENTER_ZONE,
BATTLEFIELDHOOK_ON_PLAYER_JOIN_WAR,
BATTLEFIELDHOOK_ON_WAR_END,
BATTLEFIELDHOOK_ON_PLAYER_KILL
}) {}

// Core fires this before any team-keyed container is consulted (war vacancy
// gate, kick/invite bookings, Players[] insert all key on GetTeamId()).
// Re-faking a war-locked flipped player here makes those key on the
// assigned side; otherwise a returning player is judged on his real team,
// risking a wrongful 10s "battlefield full" kick and side overfill.
void OnBattlefieldPlayerEnterZone(Battlefield* bf, Player* player) override
{
if (!sCFBG->IsEnableSystem() || !sCFBG->IsEnableWGSystem())
return;

if (bf->GetTypeId() != BATTLEFIELD_WG)
return;

if (!bf->IsWarTime() || !sCFBG->IsEnableWGTeamLock())
return;

if (sCFBG->IsPlayerFake(player))
return;

// Skip-class players normally have no stored assignment; the guard also
// covers a lock stored before a mid-war SkipClasses reload.
if (sCFBG->IsWGSkipClass(player->getClass()))
return;

std::optional<TeamId> locked = sCFBG->GetWGWarAssignment(player->GetGUID());
if (locked && *locked != player->GetTeamId(true))
sCFBG->SetFakeRaceAndMorphForBF(player, *locked);
}

void OnBattlefieldPlayerJoinWar(Battlefield* bf, Player* player) override
{
if (!sCFBG->IsEnableSystem() || !sCFBG->IsEnableWGSystem())
Expand Down Expand Up @@ -310,7 +340,17 @@ class CFBG_Battlefield : public BattlefieldScript
uint32 allianceInvited = static_cast<uint32>(bf->GetInvitedPlayersMap(TEAM_ALLIANCE).size());
uint32 hordeInvited = static_cast<uint32>(bf->GetInvitedPlayersMap(TEAM_HORDE).size());

assignedTeam = sCFBG->ResolveWGWarTeam(player, allianceInvited, hordeInvited);
// Live war counts for the no-worsen flip guard; the candidate is in
// neither set yet (hook fires before PlayersInWar.insert). These are
// assigned (post-fake) teams, exactly what balance must compare.
uint32 allianceInWar = static_cast<uint32>(bf->GetPlayersInWarSet(TEAM_ALLIANCE).size());
uint32 hordeInWar = static_cast<uint32>(bf->GetPlayersInWarSet(TEAM_HORDE).size());

assignedTeam = sCFBG->ResolveWGWarTeam(player, allianceInvited, hordeInvited, allianceInWar, hordeInWar);

// Never flip into a side already at Wintergrasp.PlayerMax.
if (assignedTeam != realTeam && !bf->HasWarVacancy(assignedTeam))
assignedTeam = realTeam;

if (sCFBG->IsEnableWGTeamLock())
sCFBG->SetWGWarAssignment(player->GetGUID(), assignedTeam);
Expand All @@ -327,6 +367,10 @@ class CFBG_Battlefield : public BattlefieldScript
else if (realTeam == TEAM_HORDE && hordeCount > allianceCount)
assignedTeam = TEAM_ALLIANCE;

// Never flip into a side already at Wintergrasp.PlayerMax.
if (assignedTeam != realTeam && !bf->HasWarVacancy(assignedTeam))
assignedTeam = realTeam;

if (sCFBG->IsEnableWGTeamLock())
sCFBG->SetWGWarAssignment(player->GetGUID(), assignedTeam);
}
Expand Down
Loading