diff --git a/src/CFBG.cpp b/src/CFBG.cpp index 6857586..fb18831 100644 --- a/src/CFBG.cpp +++ b/src/CFBG.cpp @@ -507,6 +507,60 @@ void CFBG::SetFakeRaceAndMorphForBF(Player* player, TeamId assignedTeam) _fakePlayerStore.emplace(player, std::move(fakePlayerInfo)); } +void CFBG::PrepareFakeTeamForBF(Player* player, TeamId assignedTeam) +{ + if (!player || IsPlayerFake(player)) + return; + + TeamId realTeam = player->GetTeamId(true); + if (realTeam == assignedTeam) + return; + + // Generate race/morph so the full FakePlayer record is ready for when + // ApplyFakeVisualsForBF fires later (on war accept). We do NOT call + // setRace / SetDisplayId / SetNativeDisplayId here — only the faction + // (team) is changed so that every subsequent core bucket write uses the + // assigned team. + RandomSkinInfo skinInfo{ GetRandomRaceMorph(realTeam, player->getClass(), player->getGender()) }; + + uint8 selectedRace = player->GetPlayerSetting("mod-cfbg", SETTING_CFBG_RACE).value; + + if (!RandomizeRaces() && selectedRace && IsRaceValidForFaction(realTeam, selectedRace)) + { + skinInfo.first = selectedRace; + skinInfo.second = GetMorphFromRace(skinInfo.first, player->getGender()); + } + + FakePlayer fakePlayerInfo + { + skinInfo.first, + skinInfo.second, + assignedTeam, + player->getRace(true), + player->GetDisplayId(), + player->GetNativeDisplayId(), + realTeam + }; + + // Team change only — visuals are deferred to ApplyFakeVisualsForBF. + SetFactionForRace(player, fakePlayerInfo.FakeRace); + + _fakePlayerStore.emplace(player, std::move(fakePlayerInfo)); +} + +void CFBG::ApplyFakeVisualsForBF(Player* player) +{ + FakePlayer const* fakeInfo = GetFakePlayer(player); + if (!fakeInfo) + return; + + // Visual changes deferred from PrepareFakeTeamForBF: apply them now that + // the player has actually accepted the war invitation. + player->setRace(fakeInfo->FakeRace); + player->SetDisplayId(fakeInfo->FakeMorph); + player->SetNativeDisplayId(fakeInfo->FakeMorph); +} + void CFBG::SetFactionForRace(Player* player, uint8 Race) { player->setTeamId(player->TeamIdForRace(Race)); diff --git a/src/CFBG.h b/src/CFBG.h index 84418f1..d7c35db 100644 --- a/src/CFBG.h +++ b/src/CFBG.h @@ -150,6 +150,8 @@ class CFBG void ValidatePlayerForBG(Battleground* bg, Player* player); void SetFakeRaceAndMorph(Player* player); void SetFakeRaceAndMorphForBF(Player* player, TeamId assignedTeam); + void PrepareFakeTeamForBF(Player* player, TeamId assignedTeam); + void ApplyFakeVisualsForBF(Player* player); void SetFactionForRace(Player* player, uint8 Race); void ClearFakePlayer(Player* player); void DoForgetPlayersInList(Player* player); diff --git a/src/CFBG_SC.cpp b/src/CFBG_SC.cpp index 48db69b..828dca9 100644 --- a/src/CFBG_SC.cpp +++ b/src/CFBG_SC.cpp @@ -213,6 +213,7 @@ class CFBG_Battlefield : public BattlefieldScript public: CFBG_Battlefield() : BattlefieldScript("CFBG_Battlefield", { BATTLEFIELDHOOK_ON_PLAYER_ENTER_ZONE, + BATTLEFIELDHOOK_BEFORE_INVITE_PLAYER_TO_WAR, BATTLEFIELDHOOK_ON_PLAYER_JOIN_WAR, BATTLEFIELDHOOK_ON_PLAYER_LEAVE_WAR, BATTLEFIELDHOOK_ON_PLAYER_LEAVE_ZONE @@ -254,13 +255,13 @@ class CFBG_Battlefield : public BattlefieldScript assignedTeam = TEAM_ALLIANCE; if (assignedTeam != realTeam) - sCFBG->SetFakeRaceAndMorphForBF(player, assignedTeam); + sCFBG->PrepareFakeTeamForBF(player, assignedTeam); // The player is NOT added to wgWarPlayers here; they are only added // once they actually accept the war invitation (OnBattlefieldPlayerJoinWar). } - void OnBattlefieldPlayerJoinWar(Battlefield* bf, Player* player) override + void OnBattlefieldBeforeInvitePlayerToWar(Battlefield* bf, Player* player) override { if (!sCFBG->IsEnableSystem() || !sCFBG->IsEnableWGSystem()) return; @@ -268,30 +269,53 @@ class CFBG_Battlefield : public BattlefieldScript if (bf->GetTypeId() != BATTLEFIELD_WG) return; - if (!sCFBG->IsPlayerFake(player)) - { - // Fallback path: player was in the zone before war started and still - // has their real team. Balance and assign now using the module's own - // tracking. The core's PlayerAcceptInviteToWar saves the pre-hook - // TeamId and will erase m_InvitedPlayers from the correct (real-team) - // bucket, so changing the team here does not leave a stale invite entry. - uint32 allianceCount = static_cast(_wgWarPlayers[TEAM_ALLIANCE].size()); - uint32 hordeCount = static_cast(_wgWarPlayers[TEAM_HORDE].size()); - - TeamId realTeam = player->GetTeamId(true); - TeamId assignedTeam = realTeam; - - if (realTeam == TEAM_ALLIANCE && allianceCount > hordeCount) - assignedTeam = TEAM_HORDE; - else if (realTeam == TEAM_HORDE && hordeCount > allianceCount) - assignedTeam = TEAM_ALLIANCE; - - if (assignedTeam != realTeam) - sCFBG->SetFakeRaceAndMorphForBF(player, assignedTeam); - } + // Players who entered the zone while war was already active are already + // assigned in OnBattlefieldPlayerEnterZone; nothing more to do here. + if (sCFBG->IsPlayerFake(player)) + return; + + // Fallback for players who were in the zone before war started. This + // hook fires after m_PlayersWillBeKick has been erased using the real + // team (correct), and before m_InvitedPlayers is written. Assigning + // the team here means the invite lands in the right bucket immediately, + // so PlayerAcceptInviteToWar sees a consistent team throughout. + // + // GetPlayersInWarCount is safe to use here: m_PlayersInWar was cleared + // at StartBattle and m_InvitedPlayers is being built up in this same + // loop, so there is no stale state to read. + uint32 allianceCount = bf->GetPlayersInWarCount(TEAM_ALLIANCE); + uint32 hordeCount = bf->GetPlayersInWarCount(TEAM_HORDE); - // Record this player under their (possibly just-changed) assigned team. + TeamId realTeam = player->GetTeamId(true); + TeamId assignedTeam = realTeam; + + if (realTeam == TEAM_ALLIANCE && allianceCount > hordeCount) + assignedTeam = TEAM_HORDE; + else if (realTeam == TEAM_HORDE && hordeCount > allianceCount) + assignedTeam = TEAM_ALLIANCE; + + if (assignedTeam != realTeam) + sCFBG->PrepareFakeTeamForBF(player, assignedTeam); + } + + void OnBattlefieldPlayerJoinWar(Battlefield* bf, Player* player) override + { + if (!sCFBG->IsEnableSystem() || !sCFBG->IsEnableWGSystem()) + return; + + if (bf->GetTypeId() != BATTLEFIELD_WG) + return; + + // By this point the player's team is already final: either assigned in + // OnBattlefieldPlayerEnterZone (zone entry during war) or in + // OnBattlefieldBeforeInvitePlayerToWar (pre-war zone players at invite + // time). Record them in the module's war tracking, then apply the + // visual transformation now that they have actually accepted. _wgWarPlayers[player->GetTeamId()].insert(player->GetGUID()); + + // Apply race/morph visuals deferred from the pre-invite phase. + // For players whose team was not reassigned this is a no-op. + sCFBG->ApplyFakeVisualsForBF(player); } void OnBattlefieldPlayerLeaveWar(Battlefield* bf, Player* player) override @@ -333,7 +357,7 @@ class CFBG_Battlefield : public BattlefieldScript // Populated when a player accepts a war invitation (JoinWar) and drained // when they leave the war (LeaveWar) or zone (LeaveZone catch-all). // Kept separately from the core's m_PlayersInWar / m_InvitedPlayers so that - // balance decisions are never based on stale core state. + // balance decisions during active war are always based on clean state. GuidUnorderedSet _wgWarPlayers[PVP_TEAMS_COUNT]; };