diff --git a/src/CFBG.cpp b/src/CFBG.cpp index b2d9b97..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); @@ -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. @@ -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; } @@ -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) diff --git a/src/CFBG.h b/src/CFBG.h index 0cfb317..e3fe327 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); @@ -227,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; diff --git a/src/CFBG_SC.cpp b/src/CFBG_SC.cpp index 3447810..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()) @@ -310,7 +340,17 @@ 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); + + // 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); @@ -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); }