From f69d38c7f1ced8357a525269dbc96cca2c8089b7 Mon Sep 17 00:00:00 2001 From: Skjalf <47818697+Nyeriah@users.noreply.github.com> Date: Sun, 1 Mar 2026 22:47:27 -0300 Subject: [PATCH] feat: Implement crossfaction Wintergrasp --- conf/CFBG.conf.dist | 8 +++++ src/CFBG.cpp | 41 +++++++++++++++++++++- src/CFBG.h | 4 +++ src/CFBG_SC.cpp | 85 +++++++++++++++++++++++++++++++++++++++++++++ src/cfbg_loader.cpp | 2 ++ src/cs_cfbg.cpp | 54 ++++++++++++++++++++++++++++ 6 files changed, 193 insertions(+), 1 deletion(-) diff --git a/conf/CFBG.conf.dist b/conf/CFBG.conf.dist index f03160d..2e9753c 100644 --- a/conf/CFBG.conf.dist +++ b/conf/CFBG.conf.dist @@ -16,6 +16,13 @@ # Description: Enable mixed alliance and horde in one battleground # Default: 1 # +# CFBG.Battlefield.Enable +# Description: Enable cross-faction queue for Wintergrasp battlefield. +# Requires CFBG.Enable = 1. Players are assigned to the team +# with fewer members, applying a visual race/faction change +# for the duration of their stay in the zone. +# Default: 1 +# # CFBG.Include.Avg.Ilvl.Enable # Description: Enable check average item level for bg # Default: 0 @@ -63,6 +70,7 @@ # 0 - Player choice CFBG.Enable = 1 +CFBG.Battlefield.Enable = 1 CFBG.BalancedTeams = 1 CFBG.BalancedTeams.Class.LowLevel = 0 CFBG.BalancedTeams.Class.MinLevel = 10 diff --git a/src/CFBG.cpp b/src/CFBG.cpp index c36837a..6857586 100644 --- a/src/CFBG.cpp +++ b/src/CFBG.cpp @@ -181,6 +181,7 @@ void CFBG::LoadConfig() if (!_IsEnableSystem) return; + _IsEnableWGSystem = sConfigMgr->GetOption("CFBG.Battlefield.Enable", true); _IsEnableAvgIlvl = sConfigMgr->GetOption("CFBG.Include.Avg.Ilvl.Enable", false); _IsEnableBalancedTeams = sConfigMgr->GetOption("CFBG.BalancedTeams", false); _IsEnableEvenTeams = sConfigMgr->GetOption("CFBG.EvenTeams.Enabled", false); @@ -467,10 +468,48 @@ void CFBG::SetFakeRaceAndMorph(Player* player) _fakePlayerStore.emplace(player, std::move(fakePlayerInfo)); } +void CFBG::SetFakeRaceAndMorphForBF(Player* player, TeamId assignedTeam) +{ + if (!player || IsPlayerFake(player)) + return; + + TeamId realTeam = player->GetTeamId(true); + if (realTeam == assignedTeam) + return; + + // Generate a race/morph from the assigned team's faction (opposite of real faction) + 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 + }; + + player->setRace(fakePlayerInfo.FakeRace); + SetFactionForRace(player, fakePlayerInfo.FakeRace); + player->SetDisplayId(fakePlayerInfo.FakeMorph); + player->SetNativeDisplayId(fakePlayerInfo.FakeMorph); + + _fakePlayerStore.emplace(player, std::move(fakePlayerInfo)); +} + void CFBG::SetFactionForRace(Player* player, uint8 Race) { player->setTeamId(player->TeamIdForRace(Race)); - ChrRacesEntry const* DBCRace = sChrRacesStore.LookupEntry(Race); player->SetFaction(DBCRace ? DBCRace->FactionID : 0); } diff --git a/src/CFBG.h b/src/CFBG.h index 47e332e..84418f1 100644 --- a/src/CFBG.h +++ b/src/CFBG.h @@ -15,6 +15,7 @@ #include class Player; +class Battlefield; class Battleground; class BattlegroundQueue; class Group; @@ -119,6 +120,7 @@ class CFBG void LoadConfig(); inline bool IsEnableSystem() const { return _IsEnableSystem; } + inline bool IsEnableWGSystem() const { return _IsEnableWGSystem; } inline bool IsEnableAvgIlvl() const { return _IsEnableAvgIlvl; } inline bool IsEnableBalancedTeams() const { return _IsEnableBalancedTeams; } inline bool IsEnableBalanceClassLowLevel() const { return _IsEnableBalanceClassLowLevel; } @@ -147,6 +149,7 @@ class CFBG void ValidatePlayerForBG(Battleground* bg, Player* player); void SetFakeRaceAndMorph(Player* player); + void SetFakeRaceAndMorphForBF(Player* player, TeamId assignedTeam); void SetFactionForRace(Player* player, uint8 Race); void ClearFakePlayer(Player* player); void DoForgetPlayersInList(Player* player); @@ -192,6 +195,7 @@ class CFBG // For config bool _IsEnableSystem; + bool _IsEnableWGSystem; bool _IsEnableAvgIlvl; bool _IsEnableBalancedTeams; bool _IsEnableBalanceClassLowLevel; diff --git a/src/CFBG_SC.cpp b/src/CFBG_SC.cpp index 9645987..9515457 100644 --- a/src/CFBG_SC.cpp +++ b/src/CFBG_SC.cpp @@ -4,6 +4,8 @@ */ #include "CFBG.h" +#include "Battlefield.h" +#include "BattlefieldMgr.h" #include "Group.h" #include "Player.h" #include "ReputationMgr.h" @@ -118,6 +120,21 @@ class CFBG_Player : public PlayerScript sCFBG->FitPlayerInTeam(player, player->GetBattleground() && !player->GetBattleground()->isArena(), player->GetBattleground()); } + void OnPlayerLogout(Player* player) override + { + if (!sCFBG->IsEnableSystem() || !sCFBG->IsPlayerFake(player)) + return; + + // Only clear the WG fake state when the battlefield is not actively at + // war. During a running war the player may safely relog and rejoin + // their assigned faction, so we leave the fake state intact for that + // case. BG fakes are always cleaned up by OnBattlegroundRemovePlayerAtLeave + // and do not need to be handled here. + Battlefield* bf = sBattlefieldMgr->GetBattlefieldToZoneId(player->GetZoneId()); + if (bf && bf->GetTypeId() == BATTLEFIELD_WG && !bf->IsWarTime()) + sCFBG->ClearFakePlayer(player); + } + bool OnPlayerCanJoinInBattlegroundQueue(Player* player, ObjectGuid /*BattlemasterGuid*/ , BattlegroundTypeId /*BGTypeID*/, uint8 joinAsGroup, GroupJoinBattlegroundResult& err) override { if (!sCFBG->IsEnableSystem()) @@ -191,6 +208,73 @@ class CFBG_Player : public PlayerScript uint32 timeCheck = 10000; }; +class CFBG_Battlefield : public BattlefieldScript +{ +public: + CFBG_Battlefield() : BattlefieldScript("CFBG_Battlefield", { + BATTLEFIELDHOOK_ON_PLAYER_JOIN_WAR, + BATTLEFIELDHOOK_ON_PLAYER_LEAVE_ZONE + }) {} + + void OnBattlefieldPlayerJoinWar(Battlefield* bf, Player* player) override + { + if (!sCFBG->IsEnableSystem() || !sCFBG->IsEnableWGSystem()) + return; + + if (bf->GetTypeId() != BATTLEFIELD_WG) + return; + + if (sCFBG->IsPlayerFake(player)) + return; + + // This hook fires at the very start of OnBattlefieldPlayerJoinWar, BEFORE the + // player is inserted into any m_players[] bucket. That means: + // 1. GetPlayersInZoneCount reflects the current balanced distribution. + // 2. SetFakeRaceAndMorphForBF changes player->GetTeamId() before the + // bucket insert, so the player lands in the correct (assigned) bucket. + // 3. All subsequent Battlefield operations (queue, war invite, group + // assignment, leave cleanup) see the assigned team via GetTeamId(). + uint32 allianceCount = bf->GetPlayersInWarCount(TEAM_ALLIANCE); + uint32 hordeCount = bf->GetPlayersInWarCount(TEAM_HORDE); + + TeamId realTeam = player->GetTeamId(true); + TeamId assignedTeam = realTeam; + + LOG_ERROR("sql.sql", "Player {} entered WG with real team {}, alliance count {}, horde count {}", player->GetName(), realTeam, allianceCount, hordeCount); + + // Assign player to the team with fewer zone members to balance teams. + if (realTeam == TEAM_ALLIANCE && allianceCount > hordeCount) + assignedTeam = TEAM_HORDE; + else if (realTeam == TEAM_HORDE && hordeCount > allianceCount) + assignedTeam = TEAM_ALLIANCE; + + if (assignedTeam == realTeam) + return; + + // Apply visual + faction transformation so the player looks and acts as + // the assigned faction in-game (PvP targeting, phase shifts, etc.). + // This also calls player->setTeamId(assignedTeam) so all subsequent + // player->GetTeamId() calls return the assigned team. + sCFBG->SetFakeRaceAndMorphForBF(player, assignedTeam); + } + + void OnBattlefieldPlayerLeaveZone(Battlefield* bf, Player* player) override + { + if (!sCFBG->IsEnableSystem() || !sCFBG->IsEnableWGSystem()) + return; + + if (bf->GetTypeId() != BATTLEFIELD_WG) + return; + + // HandlePlayerLeaveZone has already cleaned up all Battlefield data + // structures using the assigned team (player->GetTeamId() still returns + // assignedTeam at that point). Now that cleanup is complete it is safe + // to restore the player's real race/faction. + if (sCFBG->IsPlayerFake(player)) + sCFBG->ClearFakePlayer(player); + } +}; + class CFBG_World : public WorldScript { public: @@ -208,5 +292,6 @@ void AddSC_CFBG() { new CFBG_BG(); new CFBG_Player(); + new CFBG_Battlefield(); new CFBG_World(); } diff --git a/src/cfbg_loader.cpp b/src/cfbg_loader.cpp index 2d6cf4d..75ef4cb 100644 --- a/src/cfbg_loader.cpp +++ b/src/cfbg_loader.cpp @@ -7,10 +7,12 @@ // From SC void AddSC_CFBG(); void AddSC_cfbg_commandscript(); +void AddSC_cfbg_bf_commandscript(); // Add all void Addmod_cfbgScripts() { AddSC_CFBG(); AddSC_cfbg_commandscript(); + AddSC_cfbg_bf_commandscript(); } diff --git a/src/cs_cfbg.cpp b/src/cs_cfbg.cpp index 2780096..53521f4 100644 --- a/src/cs_cfbg.cpp +++ b/src/cs_cfbg.cpp @@ -3,6 +3,8 @@ * Licence MIT https://opensource.org/MIT */ +#include "Battlefield.h" +#include "BattlefieldMgr.h" #include "Chat.h" #include "ObjectMgr.h" #include "Player.h" @@ -119,3 +121,55 @@ void AddSC_cfbg_commandscript() { new cfbg_commandscript(); } + +class cfbg_bf_commandscript : public CommandScript +{ +public: + cfbg_bf_commandscript() : CommandScript("cfbg_bf_commandscript") { } + + ChatCommandTable GetCommands() const override + { + static ChatCommandTable bfSubCommands = + { + { "list", HandleBFList, SEC_ADMINISTRATOR, Console::No }, + }; + + static ChatCommandTable commandTable = + { + { "bf", bfSubCommands }, + }; + + return commandTable; + } + + static bool HandleBFList(ChatHandler* handler, uint32 battleId) + { + Battlefield* bf = sBattlefieldMgr->GetBattlefieldByBattleId(battleId); + + if (!bf) + { + handler->SendErrorMessage("Battlefield {} not found.", battleId); + return false; + } + + uint32 const allianceZone = bf->GetPlayersInZoneCount(TEAM_ALLIANCE); + uint32 const hordeZone = bf->GetPlayersInZoneCount(TEAM_HORDE); + uint32 const allianceWar = bf->GetPlayersInWarCount(TEAM_ALLIANCE); + uint32 const hordeWar = bf->GetPlayersInWarCount(TEAM_HORDE); + uint32 const maxPerTeam = bf->GetMaxPlayersPerTeam(); + + handler->SendSysMessage(Acore::StringFormat("Battlefield {} | {}", battleId, + bf->IsWarTime() ? "WAR" : "PEACE").c_str()); + handler->SendSysMessage(Acore::StringFormat(" Alliance: {} in zone, {} in war / {} max", + allianceZone, allianceWar, maxPerTeam).c_str()); + handler->SendSysMessage(Acore::StringFormat(" Horde: {} in zone, {} in war / {} max", + hordeZone, hordeWar, maxPerTeam).c_str()); + + return true; + } +}; + +void AddSC_cfbg_bf_commandscript() +{ + new cfbg_bf_commandscript(); +}