From ee7407007ec1a7f9412ff294be83c54719cc33d9 Mon Sep 17 00:00:00 2001 From: FrancescoBorzi Date: Thu, 2 Jul 2026 17:08:20 +0200 Subject: [PATCH 1/4] fix: keep WG native-priority census fair for trickle joins With an empty zone and trickle joins the first accepter latches a 1/0 census, whose floored fair share (1+0)/2 = 0 made every majority joiner flip: three same-faction players ended up 0v3 on the wrong side. The fair share is now a ceiling, so the lone first accepter counts as fair share and stays native, and past the fair share a majority joiner flips only when his real side currently outnumbers the other in the live war sets; on a tie he stays native. --- src/CFBG.cpp | 15 ++++++++++++--- src/CFBG.h | 8 +++++--- src/CFBG_SC.cpp | 8 +++++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/CFBG.cpp b/src/CFBG.cpp index b2d9b97..12258f5 100644 --- a/src/CFBG.cpp +++ b/src/CFBG.cpp @@ -593,14 +593,14 @@ void CFBG::ClearWGWarAssignments() _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; } @@ -619,7 +619,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) diff --git a/src/CFBG.h b/src/CFBG.h index 0cfb317..50e5b14 100644 --- a/src/CFBG.h +++ b/src/CFBG.h @@ -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); diff --git a/src/CFBG_SC.cpp b/src/CFBG_SC.cpp index 3447810..0824721 100644 --- a/src/CFBG_SC.cpp +++ b/src/CFBG_SC.cpp @@ -310,7 +310,13 @@ class CFBG_Battlefield : public BattlefieldScript uint32 allianceInvited = static_cast(bf->GetInvitedPlayersMap(TEAM_ALLIANCE).size()); uint32 hordeInvited = static_cast(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(bf->GetPlayersInWarSet(TEAM_ALLIANCE).size()); + uint32 hordeInWar = static_cast(bf->GetPlayersInWarSet(TEAM_HORDE).size()); + + assignedTeam = sCFBG->ResolveWGWarTeam(player, allianceInvited, hordeInvited, allianceInWar, hordeInWar); if (sCFBG->IsEnableWGTeamLock()) sCFBG->SetWGWarAssignment(player->GetGUID(), assignedTeam); From ddfbcda91e298a75173359deb89fe11e4d97bbbe Mon Sep 17 00:00:00 2001 From: FrancescoBorzi Date: Thu, 2 Jul 2026 17:08:57 +0200 Subject: [PATCH 2/4] fix: key WG war vacancy and invites on the assigned team Core keys the war vacancy gate, kick and invite bookings and the Players[] insert on GetTeamId(), which for a war-locked flipped player returning to the zone is still the REAL team until the join-war hook re-fakes him. That mis-keyed him against the wrong side's cap: a wrongful 10s battlefield-full kick when his real side was full, or overfilling a side past Wintergrasp.PlayerMax. Handle the enter-zone hook, which core fires before any of those containers are consulted, and re-fake a war-locked player there. Also stop fresh assignments from flipping into a side already at Wintergrasp.PlayerMax; stored locks are honored unchanged. --- src/CFBG.cpp | 9 +++++++++ src/CFBG_SC.cpp | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/CFBG.cpp b/src/CFBG.cpp index 12258f5..03c8749 100644 --- a/src/CFBG.cpp +++ b/src/CFBG.cpp @@ -588,6 +588,15 @@ 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; diff --git a/src/CFBG_SC.cpp b/src/CFBG_SC.cpp index 0824721..27da9f6 100644 --- a/src/CFBG_SC.cpp +++ b/src/CFBG_SC.cpp @@ -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 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()) @@ -318,6 +348,10 @@ class CFBG_Battlefield : public BattlefieldScript 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); } @@ -333,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); } From 0d438dac851bb60e1736dc68a1abd6ca96d9d896 Mon Sep 17 00:00:00 2001 From: FrancescoBorzi Date: Thu, 2 Jul 2026 17:09:13 +0200 Subject: [PATCH 3/4] fix: skip team reconciliation once the BG reached WAIT_LEAVE EndBattleground restores every player's real identity, but a cross-faction player who dies at the victory screen and reclaims within the WAIT_LEAVE window went through the resurrect reconciliation, which re-faked him for the rest of the window. Early-return on WAIT_LEAVE exactly; entry-time calls run at WAIT_JOIN or IN_PROGRESS and the in-progress resurrect reapply is unaffected. --- src/CFBG.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CFBG.cpp b/src/CFBG.cpp index 03c8749..0b526f1 100644 --- a/src/CFBG.cpp +++ b/src/CFBG.cpp @@ -320,6 +320,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. From 00c50e8dee574c85f61f1d26224595f11875f14c Mon Sep 17 00:00:00 2001 From: FrancescoBorzi Date: Thu, 2 Jul 2026 17:09:42 +0200 Subject: [PATCH 4/4] fix: restore faked players when CFBG is disabled via reload Disabling CFBG.Enable and running .reload config early-returned out of LoadConfig, stranding every online faked player cross-faction until relog and orphaning the Player*-keyed store entries, which can corrupt a later Player object reusing the same address. On a true to false transition, restore every online faked player and wholesale-clear all four Player*-keyed stores without dereferencing stored keys (a faked player who logged out mid-war may have left a dangling key). Also give _IsEnableSystem an initializer so the transition read is well-defined on the first load; startup never runs the sweep. --- src/CFBG.cpp | 22 ++++++++++++++++++++++ src/CFBG.h | 4 +++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/CFBG.cpp b/src/CFBG.cpp index 0b526f1..5613750 100644 --- a/src/CFBG.cpp +++ b/src/CFBG.cpp @@ -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" @@ -92,9 +93,30 @@ CFBG* CFBG::instance() void CFBG::LoadConfig() { + bool const wasEnabled = _IsEnableSystem; _IsEnableSystem = sConfigMgr->GetOption("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("CFBG.Battlefield.Enable", true); _IsEnableWGTeamLock = sConfigMgr->GetOption("CFBG.Battlefield.TeamLock.Enable", true); diff --git a/src/CFBG.h b/src/CFBG.h index 50e5b14..e3fe327 100644 --- a/src/CFBG.h +++ b/src/CFBG.h @@ -229,7 +229,9 @@ class CFBG std::array _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;