From 6516b32fc6e5c60b9e1ffb0ccc4b5a406f8d948b Mon Sep 17 00:00:00 2001 From: sindoring Date: Thu, 2 Apr 2026 21:51:05 +0400 Subject: [PATCH] wip --- .../mod_gamemodes_characters.sql | 45 +++ modules/mod-gamemodes/conf/conf.sh.dist | 1 + .../conf/mod_gamemodes.conf.dist | 25 ++ .../base/mod_gamemodes_characters.sql | 6 + modules/mod-gamemodes/include.sh | 1 + modules/mod-gamemodes/src/GameModes.cpp | 69 +++++ modules/mod-gamemodes/src/GameModes.h | 37 +++ .../mod-gamemodes/src/gamemodes_loader.cpp | 6 + .../mod-gamemodes/src/modes/TraitorMode.cpp | 291 ++++++++++++++++++ modules/mod-gamemodes/src/modes/TraitorMode.h | 49 +++ setup-modules.sh | 44 ++- .../Implementation/CharacterDatabase.cpp | 12 +- src/server/game/Entities/Player/Player.cpp | 13 +- src/server/game/Entities/Player/Player.h | 5 +- .../game/Entities/Player/PlayerQuest.cpp | 10 + .../game/Entities/Player/PlayerStorage.cpp | 1 + .../game/Entities/Player/PlayerUpdates.cpp | 6 +- src/server/game/Reputation/ReputationMgr.cpp | 26 +- .../Scripting/ScriptDefines/PlayerScript.cpp | 5 + .../Scripting/ScriptDefines/PlayerScript.h | 2 + src/server/game/Scripting/ScriptMgr.h | 1 + 21 files changed, 636 insertions(+), 19 deletions(-) create mode 100644 data/sql/base/db_characters/mod_gamemodes_characters.sql create mode 100644 modules/mod-gamemodes/conf/conf.sh.dist create mode 100644 modules/mod-gamemodes/conf/mod_gamemodes.conf.dist create mode 100644 modules/mod-gamemodes/data/sql/db-characters/base/mod_gamemodes_characters.sql create mode 100644 modules/mod-gamemodes/include.sh create mode 100644 modules/mod-gamemodes/src/GameModes.cpp create mode 100644 modules/mod-gamemodes/src/GameModes.h create mode 100644 modules/mod-gamemodes/src/gamemodes_loader.cpp create mode 100644 modules/mod-gamemodes/src/modes/TraitorMode.cpp create mode 100644 modules/mod-gamemodes/src/modes/TraitorMode.h diff --git a/data/sql/base/db_characters/mod_gamemodes_characters.sql b/data/sql/base/db_characters/mod_gamemodes_characters.sql new file mode 100644 index 0000000..70ef5c6 --- /dev/null +++ b/data/sql/base/db_characters/mod_gamemodes_characters.sql @@ -0,0 +1,45 @@ +-- MySQL dump 10.13 Distrib 8.4.3, for Win64 (x86_64) +-- +-- Host: localhost Database: acore_characters +-- ------------------------------------------------------ +-- Server version 8.4.3 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8mb4 */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `mod_gamemodes_characters` +-- + +DROP TABLE IF EXISTS `mod_gamemodes_characters`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `mod_gamemodes_characters` ( + `guid` int unsigned NOT NULL, + `game_mode` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '0 = NORMAL, 1 = TRAITOR', + PRIMARY KEY (`guid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Character Game Modes'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `mod_gamemodes_characters` +-- + +LOCK TABLES `mod_gamemodes_characters` WRITE; +/*!40000 ALTER TABLE `mod_gamemodes_characters` DISABLE KEYS */; +/*!40000 ALTER TABLE `mod_gamemodes_characters` ENABLE KEYS */; +UNLOCK TABLES; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; diff --git a/modules/mod-gamemodes/conf/conf.sh.dist b/modules/mod-gamemodes/conf/conf.sh.dist new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/modules/mod-gamemodes/conf/conf.sh.dist @@ -0,0 +1 @@ + diff --git a/modules/mod-gamemodes/conf/mod_gamemodes.conf.dist b/modules/mod-gamemodes/conf/mod_gamemodes.conf.dist new file mode 100644 index 0000000..927cff5 --- /dev/null +++ b/modules/mod-gamemodes/conf/mod_gamemodes.conf.dist @@ -0,0 +1,25 @@ +# +# Game Modes module configuration +# + +######################################## +# GameModes settings +######################################## + +# +# GameModes.Enable +# Description: Enable game modes system +# Default: 1 - (Enabled) +# 0 - (Disabled) +# + +GameModes.Enable = 1 + +# +# GameModes.Traitor.Enable +# Description: Enable Traitor game mode +# Default: 1 - (Enabled) +# 0 - (Disabled) +# + +GameModes.Traitor.Enable = 1 diff --git a/modules/mod-gamemodes/data/sql/db-characters/base/mod_gamemodes_characters.sql b/modules/mod-gamemodes/data/sql/db-characters/base/mod_gamemodes_characters.sql new file mode 100644 index 0000000..d4b4c4b --- /dev/null +++ b/modules/mod-gamemodes/data/sql/db-characters/base/mod_gamemodes_characters.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS `mod_gamemodes_characters`; +CREATE TABLE `mod_gamemodes_characters` ( + `guid` INT UNSIGNED NOT NULL, + `game_mode` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 = NORMAL, 1 = TRAITOR', + PRIMARY KEY (`guid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/modules/mod-gamemodes/include.sh b/modules/mod-gamemodes/include.sh new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/modules/mod-gamemodes/include.sh @@ -0,0 +1 @@ + diff --git a/modules/mod-gamemodes/src/GameModes.cpp b/modules/mod-gamemodes/src/GameModes.cpp new file mode 100644 index 0000000..6cb1ee8 --- /dev/null +++ b/modules/mod-gamemodes/src/GameModes.cpp @@ -0,0 +1,69 @@ +#include "GameModes.h" +#include "DatabaseEnv.h" +#include "Log.h" + +GameModeMgr* GameModeMgr::Instance() +{ + static GameModeMgr instance; + return &instance; +} + +void GameModeMgr::LoadFromDB() +{ + uint32 oldMSTime = getMSTime(); + _gameModes.clear(); + + QueryResult result = CharacterDatabase.Query("SELECT `guid`, `game_mode` FROM `mod_gamemodes_characters`"); + if (!result) + { + LOG_INFO("module", ">> Loaded 0 character game modes. Table `mod_gamemodes_characters` is empty."); + return; + } + + uint32 count = 0; + do + { + Field* fields = result->Fetch(); + ObjectGuid::LowType guid = fields[0].Get(); + uint8 mode = fields[1].Get(); + + if (mode >= GAMEMODE_MAX) + { + LOG_ERROR("module", "mod-gamemodes: Invalid game mode {} for character {}, skipping.", mode, guid); + continue; + } + + _gameModes[guid] = static_cast(mode); + ++count; + } while (result->NextRow()); + + LOG_INFO("module", ">> Loaded {} character game modes in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); +} + +void GameModeMgr::SaveGameMode(ObjectGuid::LowType guid, GameMode mode) +{ + _gameModes[guid] = mode; + + CharacterDatabase.Execute("REPLACE INTO `mod_gamemodes_characters` (`guid`, `game_mode`) VALUES ({}, {})", + guid, static_cast(mode)); +} + +void GameModeMgr::DeleteGameMode(ObjectGuid::LowType guid) +{ + _gameModes.erase(guid); + + CharacterDatabase.Execute("DELETE FROM `mod_gamemodes_characters` WHERE `guid` = {}", guid); +} + +GameMode GameModeMgr::GetGameMode(ObjectGuid::LowType guid) const +{ + auto it = _gameModes.find(guid); + if (it != _gameModes.end()) + return it->second; + return GAMEMODE_NORMAL; +} + +bool GameModeMgr::HasGameMode(ObjectGuid::LowType guid, GameMode mode) const +{ + return GetGameMode(guid) == mode; +} diff --git a/modules/mod-gamemodes/src/GameModes.h b/modules/mod-gamemodes/src/GameModes.h new file mode 100644 index 0000000..0109203 --- /dev/null +++ b/modules/mod-gamemodes/src/GameModes.h @@ -0,0 +1,37 @@ +#ifndef MOD_GAMEMODES_H +#define MOD_GAMEMODES_H + +#include "Player.h" +#include + +enum GameMode : uint8 +{ + GAMEMODE_NORMAL = 0, + GAMEMODE_TRAITOR = 1, + GAMEMODE_MAX +}; + +// Custom bit in SMSG_CHAR_ENUM character flags. +// Chosen as a high unused bit so the packet layout stays unchanged. +constexpr uint32 GAMEMODE_CHARACTER_FLAG_TRAITOR = 0x40000000; + +class GameModeMgr +{ +public: + static GameModeMgr* Instance(); + + void LoadFromDB(); + void SaveGameMode(ObjectGuid::LowType guid, GameMode mode); + void DeleteGameMode(ObjectGuid::LowType guid); + + GameMode GetGameMode(ObjectGuid::LowType guid) const; + bool HasGameMode(ObjectGuid::LowType guid, GameMode mode) const; + +private: + GameModeMgr() = default; + std::unordered_map _gameModes; +}; + +#define sGameModeMgr GameModeMgr::Instance() + +#endif // MOD_GAMEMODES_H diff --git a/modules/mod-gamemodes/src/gamemodes_loader.cpp b/modules/mod-gamemodes/src/gamemodes_loader.cpp new file mode 100644 index 0000000..a4f6013 --- /dev/null +++ b/modules/mod-gamemodes/src/gamemodes_loader.cpp @@ -0,0 +1,6 @@ +void AddSC_TraitorMode(); + +void Addmod_gamemodesScripts() +{ + AddSC_TraitorMode(); +} diff --git a/modules/mod-gamemodes/src/modes/TraitorMode.cpp b/modules/mod-gamemodes/src/modes/TraitorMode.cpp new file mode 100644 index 0000000..293efb2 --- /dev/null +++ b/modules/mod-gamemodes/src/modes/TraitorMode.cpp @@ -0,0 +1,291 @@ +#include "TraitorMode.h" +#include "GameModes.h" +#include "Config.h" +#include "DBCStores.h" +#include "DatabaseEnv.h" +#include "Log.h" +#include "ObjectMgr.h" +#include "ReputationMgr.h" +#include "ScriptMgr.h" +#include "WorldPacket.h" +#include "WorldSession.h" + +namespace TraitorMode +{ + namespace + { + void NormalizeFactionState(Player* player, FactionEntry const* factionEntry, bool visible, bool atWar) + { + if (!factionEntry || factionEntry->reputationListID < 0) + return; + + FactionState* factionState = const_cast(player->GetReputationMgr().GetState(factionEntry)); + if (!factionState) + return; + + factionState->Flags &= ~(FACTION_FLAG_VISIBLE | FACTION_FLAG_HIDDEN | FACTION_FLAG_INVISIBLE_FORCED | + FACTION_FLAG_PEACE_FORCED | FACTION_FLAG_INACTIVE); + + if (visible) + factionState->Flags |= FACTION_FLAG_VISIBLE; + else + factionState->Flags |= FACTION_FLAG_HIDDEN | FACTION_FLAG_INVISIBLE_FORCED; + + if (atWar) + factionState->Flags |= FACTION_FLAG_AT_WAR; + else + factionState->Flags &= ~FACTION_FLAG_AT_WAR; + + factionState->needSend = true; + factionState->needSave = true; + } + + void SetFactionStanding(Player* player, uint32 factionId, int32 targetDisplayed, bool visible, bool atWar) + { + FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId); + if (!factionEntry) + return; + + // ReputationMgr::GetBaseReputation now always uses the player's actual race + // mask, so SetOneFactionReputation is safe to call directly. + player->GetReputationMgr().SetOneFactionReputation(factionEntry, static_cast(targetDisplayed), false); + NormalizeFactionState(player, factionEntry, visible, atWar); + } + + void NormalizeTraitorReputationStates(Player* player) + { + TeamId originalTeam = player->GetTeamId(true); + uint32 const* newFactions = (originalTeam == TEAM_ALLIANCE) + ? HORDE_REPUTATION_FACTIONS + : ALLIANCE_REPUTATION_FACTIONS; + uint32 const* oldFactions = (originalTeam == TEAM_ALLIANCE) + ? ALLIANCE_REPUTATION_FACTIONS + : HORDE_REPUTATION_FACTIONS; + + auto normalizeFactionAndParent = [player](uint32 factionId, bool visible, bool atWar) + { + if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId)) + { + NormalizeFactionState(player, factionEntry, visible, atWar); + + if (factionEntry->team) + NormalizeFactionState(player, sFactionStore.LookupEntry(factionEntry->team), visible, false); + } + }; + + for (uint32 i = 0; i < FACTION_COUNT; ++i) + { + normalizeFactionAndParent(newFactions[i], true, false); + normalizeFactionAndParent(oldFactions[i], false, true); + } + } + + void SendFactionVisiblePacket(Player* player, FactionEntry const* factionEntry) + { + if (!factionEntry || factionEntry->reputationListID < 0) + return; + + WorldPacket data(SMSG_SET_FACTION_VISIBLE, 4); + data << uint32(factionEntry->reputationListID); + player->SendDirectMessage(&data); + } + + void SendTraitorFactionVisibility(Player* player) + { + TeamId originalTeam = player->GetTeamId(true); + uint32 const* newFactions = (originalTeam == TEAM_ALLIANCE) + ? HORDE_REPUTATION_FACTIONS + : ALLIANCE_REPUTATION_FACTIONS; + auto sendFactionAndParent = [player](uint32 factionId) + { + if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId)) + { + SendFactionVisiblePacket(player, factionEntry); + + if (factionEntry->team) + SendFactionVisiblePacket(player, sFactionStore.LookupEntry(factionEntry->team)); + } + }; + + for (uint32 i = 0; i < FACTION_COUNT; ++i) + sendFactionAndParent(newFactions[i]); + } + } + + void ApplyFactionSwap(Player* player) + { + TeamId originalTeam = player->GetTeamId(true); + TeamId newTeam = (originalTeam == TEAM_ALLIANCE) ? TEAM_HORDE : TEAM_ALLIANCE; + + player->setTeamId(newTeam); + + // Derive faction template from the canonical race for the new team (same + // logic as SetFactionForRace), so we never rely on hardcoded DBC IDs. + uint8 representativeRace = (newTeam == TEAM_HORDE) ? HORDE_DEFAULT_RACE : ALLIANCE_DEFAULT_RACE; + if (ChrRacesEntry const* rEntry = sChrRacesStore.LookupEntry(representativeRace)) + player->SetFaction(rEntry->FactionID); + + LOG_INFO("module", "mod-gamemodes: ApplyFactionSwap for {} (GUID: {}), original team = {}, new team = {}, factionTemplate = {}", + player->GetName(), player->GetGUID().GetCounter(), + (originalTeam == TEAM_ALLIANCE ? "Alliance" : "Horde"), + (newTeam == TEAM_ALLIANCE ? "Alliance" : "Horde"), + player->GetFaction()); + } + + void InitTraitorReputations(Player* player) + { + TeamId originalTeam = player->GetTeamId(true); + + // New faction reputations -> Neutral (standing = 0) + const uint32* newFactions = (originalTeam == TEAM_ALLIANCE) + ? HORDE_REPUTATION_FACTIONS + : ALLIANCE_REPUTATION_FACTIONS; + + for (uint32 i = 0; i < FACTION_COUNT; ++i) + SetFactionStanding(player, newFactions[i], 0, true, false); + + // Old faction reputations -> Hostile (standing = -6000) + const uint32* oldFactions = (originalTeam == TEAM_ALLIANCE) + ? ALLIANCE_REPUTATION_FACTIONS + : HORDE_REPUTATION_FACTIONS; + + for (uint32 i = 0; i < FACTION_COUNT; ++i) + SetFactionStanding(player, oldFactions[i], -6000, false, true); + + LOG_INFO("module", "mod-gamemodes: InitTraitorReputations for {} (GUID: {})", + player->GetName(), player->GetGUID().GetCounter()); + } + + void TeleportToNewFactionStart(Player* player) + { + TeamId originalTeam = player->GetTeamId(true); + uint8 spawnRace = (originalTeam == TEAM_ALLIANCE) + ? HORDE_DEFAULT_RACE + : ALLIANCE_DEFAULT_RACE; + + PlayerInfo const* info = sObjectMgr->GetPlayerInfo(spawnRace, player->getClass()); + if (!info) + { + // Fallback: try warrior (class 1) for that race + info = sObjectMgr->GetPlayerInfo(spawnRace, 1); + } + + if (!info) + { + LOG_ERROR("module", "mod-gamemodes: No spawn info for race {} class {}", spawnRace, player->getClass()); + return; + } + + player->TeleportTo(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation); + + LOG_INFO("module", "mod-gamemodes: Teleported {} to faction start (map {}, {:.1f}, {:.1f}, {:.1f})", + player->GetName(), info->mapId, info->positionX, info->positionY, info->positionZ); + } +} + +class TraitorModePlayer : public PlayerScript +{ +public: + TraitorModePlayer() : PlayerScript("TraitorModePlayer", + {PLAYERHOOK_ON_CREATE, PLAYERHOOK_ON_LOGIN, PLAYERHOOK_ON_REPUTATION_LOAD_FROM_DB, PLAYERHOOK_ON_UPDATE_FACTION}) {} + + // OnPlayerCreate fires on a temporary Player object AFTER SaveToDB. + // Changes to faction/reputation here are lost. We only save the game mode to DB. + void OnPlayerCreate(Player* player) override + { + LOG_INFO("module", "mod-gamemodes: OnPlayerCreate for {} (GUID: {}), OutfitId = {}", + player->GetName(), player->GetGUID().GetCounter(), player->GetOutfitId()); + + if (!sConfigMgr->GetOption("GameModes.Enable", true)) + return; + if (!sConfigMgr->GetOption("GameModes.Traitor.Enable", true)) + return; + + if (player->GetOutfitId() != GAMEMODE_TRAITOR) + return; + + sGameModeMgr->SaveGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR); + CharacterDatabase.Execute("UPDATE `characters` SET `cinematic` = 1 WHERE `guid` = {}", player->GetGUID().GetCounter()); + LOG_INFO("module", "mod-gamemodes: Saved TRAITOR mode for {} (GUID: {})", + player->GetName(), player->GetGUID().GetCounter()); + } + + // Reputation load completes before initial packets are sent to the client. + // Traitor mode must rewrite faction/reputation state here, otherwise the client + // builds the reputation window from the original race defaults. + void OnPlayerReputationLoadFromDB(Player* player) override + { + if (!sConfigMgr->GetOption("GameModes.Enable", true)) + return; + if (!sConfigMgr->GetOption("GameModes.Traitor.Enable", true)) + return; + + if (!sGameModeMgr->HasGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR)) + return; + + TraitorMode::ApplyFactionSwap(player); + + if (player->HasAtLoginFlag(AT_LOGIN_FIRST)) + TraitorMode::InitTraitorReputations(player); + TraitorMode::NormalizeTraitorReputationStates(player); + } + + void OnPlayerLogin(Player* player) override + { + if (!sConfigMgr->GetOption("GameModes.Enable", true)) + return; + if (!sConfigMgr->GetOption("GameModes.Traitor.Enable", true)) + return; + + if (!sGameModeMgr->HasGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR)) + return; + + TraitorMode::ApplyFactionSwap(player); + TraitorMode::NormalizeTraitorReputationStates(player); + player->GetReputationMgr().SendInitialReputations(); + TraitorMode::SendTraitorFactionVisibility(player); + player->SetPlayerFlag(PLAYER_FLAGS_TRAITOR); + + if (player->HasAtLoginFlag(AT_LOGIN_FIRST)) + TraitorMode::TeleportToNewFactionStart(player); + + LOG_INFO("module", "mod-gamemodes: OnPlayerLogin END for {} — TeamId = {}, FactionTemplate = {}, OriginalTeam = {}", + player->GetName(), + static_cast(player->GetTeamId()), + player->GetFaction(), + static_cast(player->GetTeamId(true))); + } + + // OnPlayerUpdateFaction fires every time SetFactionForRace is called + // (login, race change, etc). Re-apply the swap so it's never lost. + void OnPlayerUpdateFaction(Player* player) override + { + if (!sConfigMgr->GetOption("GameModes.Enable", true)) + return; + if (!sConfigMgr->GetOption("GameModes.Traitor.Enable", true)) + return; + + if (!sGameModeMgr->HasGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR)) + return; + + TraitorMode::ApplyFactionSwap(player); + } +}; + +class TraitorModeWorld : public WorldScript +{ +public: + TraitorModeWorld() : WorldScript("TraitorModeWorld", + {WORLDHOOK_ON_LOAD_CUSTOM_DATABASE_TABLE}) {} + + void OnLoadCustomDatabaseTable() override + { + sGameModeMgr->LoadFromDB(); + } +}; + +void AddSC_TraitorMode() +{ + new TraitorModePlayer(); + new TraitorModeWorld(); +} diff --git a/modules/mod-gamemodes/src/modes/TraitorMode.h b/modules/mod-gamemodes/src/modes/TraitorMode.h new file mode 100644 index 0000000..5f41ece --- /dev/null +++ b/modules/mod-gamemodes/src/modes/TraitorMode.h @@ -0,0 +1,49 @@ +#ifndef MOD_GAMEMODES_TRAITOR_H +#define MOD_GAMEMODES_TRAITOR_H + +#include "Player.h" + +namespace TraitorMode +{ + // Reputation faction IDs: capital cities + PvP battleground factions + constexpr uint32 ALLIANCE_REPUTATION_FACTIONS[] = { + 72, // Stormwind + 47, // Ironforge + 69, // Darnassus + 54, // Gnomeregan Exiles + 930, // Exodar + 890, // Silverwing Sentinels (WSG) + 730, // Stormpike Guard (AV) + 509, // League of Arathor (AB) + }; + + constexpr uint32 HORDE_REPUTATION_FACTIONS[] = { + 76, // Orgrimmar + 81, // Thunder Bluff + 68, // Undercity + 530, // Darkspear Trolls + 911, // Silvermoon City + 889, // Warsong Outriders (WSG) + 729, // Frostwolf Clan (AV) + 510, // The Defilers (AB) + }; + + constexpr uint32 FACTION_COUNT = 8; + + // Default spawn points for each faction (race 2 = Orc for Horde, race 1 = Human for Alliance) + constexpr uint8 HORDE_DEFAULT_RACE = 2; // Orc + constexpr uint8 ALLIANCE_DEFAULT_RACE = 1; // Human + + // Apply faction swap on login/faction update + void ApplyFactionSwap(Player* player); + + // Set new faction reputations to Neutral (standing 0) + void InitTraitorReputations(Player* player); + + // Teleport traitor to the opposite faction's starting zone + void TeleportToNewFactionStart(Player* player); +} + +void AddSC_TraitorMode(); + +#endif // MOD_GAMEMODES_TRAITOR_H diff --git a/setup-modules.sh b/setup-modules.sh index f959eba..6467522 100644 --- a/setup-modules.sh +++ b/setup-modules.sh @@ -82,6 +82,9 @@ FORCE_PLAYERBOTS="${FORCE_PLAYERBOTS:-${ACORE_PLAYERBOTS_FORCE:-0}}" : "${ACORE_QUEST_PARTY_ENABLE:=1}" : "${ACORE_QUEST_PARTY_MESSAGE:=0}" +: "${ACORE_GAMEMODES_ENABLE:=1}" +: "${ACORE_GAMEMODES_TRAITOR_ENABLE:=1}" + : "${ACORE_ELUNA_ENABLED:=true}" : "${ACORE_ELUNA_TRACEBACK:=false}" : "${ACORE_ELUNA_SCRIPT_PATH:=lua_scripts}" @@ -266,8 +269,9 @@ database_service_running() { docker compose ps --status running --services 2>/dev/null | grep -qx 'ac-database' } -import_world_sql_file() { +import_sql_file() { local file_path="$1" + local database="$2" if [[ ! -f "$file_path" ]]; then log "skip missing ${file_path#$ROOT_DIR/}" @@ -275,13 +279,21 @@ import_world_sql_file() { fi if (( DRY_RUN )); then - log "would import ${file_path#$ROOT_DIR/} into acore_world" + log "would import ${file_path#$ROOT_DIR/} into ${database}" return fi docker compose exec -T ac-database bash -lc \ - "mysql --default-character-set=utf8mb4 -uroot -p\"\$MYSQL_ROOT_PASSWORD\" acore_world" < "$file_path" - log "imported ${file_path#$ROOT_DIR/} into acore_world" + "mysql --default-character-set=utf8mb4 -uroot -p\"\$MYSQL_ROOT_PASSWORD\" ${database}" < "$file_path" + log "imported ${file_path#$ROOT_DIR/} into ${database}" +} + +import_world_sql_file() { + import_sql_file "$1" "acore_world" +} + +import_characters_sql_file() { + import_sql_file "$1" "acore_characters" } import_local_lua_module_sql() { @@ -300,6 +312,15 @@ import_local_lua_module_sql() { import_world_sql_file "$start_zone_race_teleporters_sql" } +import_gamemodes_sql() { + if ! database_service_running; then + log "skip gamemodes SQL: ac-database is not running" + return + fi + + import_characters_sql_file "$ROOT_DIR/modules/mod-gamemodes/data/sql/db-characters/base/mod_gamemodes_characters.sql" +} + ensure_world_conf() { if [[ -f "$WORLD_CONF" ]]; then return @@ -339,6 +360,8 @@ sync_module_dists() { "$MODULES_ETC_DIR/mod_learnspells.conf.dist" sync_file "$ROOT_DIR/modules/mod-playerbots/conf/playerbots.conf.dist" \ "$MODULES_ETC_DIR/playerbots.conf.dist" + sync_file "$ROOT_DIR/modules/mod-gamemodes/conf/mod_gamemodes.conf.dist" \ + "$MODULES_ETC_DIR/mod_gamemodes.conf.dist" sync_file "$ROOT_DIR/modules/mod-quest-loot-party/conf/mod-quest-loot-party.conf.dist" \ "$MODULES_ETC_DIR/mod-quest-loot-party.conf.dist" } @@ -482,6 +505,16 @@ playerbots_supported() { return 0 } +write_gamemodes_conf() { + write_file "$MODULES_ETC_DIR/mod_gamemodes.conf.dist" <OutfitId; /// @todo: need more checks against packet modifications // should check that skin, face, hair* are valid via DBC per race/class // also do it in Player::BuildEnumData, Player::LoadFromDB @@ -1111,8 +1111,8 @@ bool Player::BuildEnumData(PreparedQueryResult result, WorldPacket* data) // characters.hairColor, characters.facialStyle, character.level, characters.zone, characters.map, characters.position_x, characters.position_y, characters.position_z, // 16 17 18 19 20 21 22 23 // guild_member.guildid, characters.playerFlags, characters.at_login, character_pet.entry, character_pet.modelid, character_pet.level, characters.equipmentCache, character_banned.guid, - // 24 25 - // characters.extra_flags, character_declinedname.genitive + // 24 25 26 + // characters.extra_flags, mod_gamemodes_characters.game_mode, character_declinedname.genitive Field* fields = result->Fetch(); @@ -1184,12 +1184,15 @@ bool Player::BuildEnumData(PreparedQueryResult result, WorldPacket* data) charFlags |= CHARACTER_FLAG_LOCKED_BY_BILLING; if (sWorld->getBoolConfig(CONFIG_DECLINED_NAMES_USED)) { - if (!fields[25].Get().empty()) + if (!fields[26].Get().empty()) charFlags |= CHARACTER_FLAG_DECLINED; } else charFlags |= CHARACTER_FLAG_DECLINED; + if (fields[25].Get() == 1 /* GAMEMODE_TRAITOR */) + charFlags |= CHARACTER_FLAG_UNK31; + *data << uint32(charFlags); // character flags // character customize flags @@ -5972,7 +5975,7 @@ void Player::RewardReputation(Unit* victim) ChampioningFaction = GetChampioningFaction(); } - TeamId teamId = GetTeamId(true); // Always check player original reputation when rewarding + TeamId teamId = GetTeamId(); if (Rep->RepFaction1 && (!Rep->TeamDependent || teamId == TEAM_ALLIANCE)) { diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 389586d..7ae8990 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -486,7 +486,7 @@ enum PlayerFlags : uint32 PLAYER_FLAGS_UNK27 = 0x08000000, PLAYER_FLAGS_UNK28 = 0x10000000, PLAYER_FLAGS_UNK29 = 0x20000000, - PLAYER_FLAGS_UNK30 = 0x40000000, + PLAYER_FLAGS_TRAITOR = 0x40000000, // mod-gamemodes: player is playing for the opposite faction PLAYER_FLAGS_UNK31 = 0x80000000, }; @@ -2130,6 +2130,8 @@ public: void SetFactionForRace(uint8 race); void setTeamId(TeamId teamid) { m_team = teamid; }; + [[nodiscard]] uint8 GetOutfitId() const { return m_outfitId; } + void InitDisplayIds(); bool IsAtGroupRewardDistance(WorldObject const* pRewardSource) const; @@ -2823,6 +2825,7 @@ protected: ObjectGuid m_lootGuid; TeamId m_team; + uint8 m_outfitId{0}; uint32 m_nextSave; // pussywizard uint16 m_additionalSaveTimer; // pussywizard uint8 m_additionalSaveMask; // pussywizard diff --git a/src/server/game/Entities/Player/PlayerQuest.cpp b/src/server/game/Entities/Player/PlayerQuest.cpp index edfe48c..8505e5b 100644 --- a/src/server/game/Entities/Player/PlayerQuest.cpp +++ b/src/server/game/Entities/Player/PlayerQuest.cpp @@ -1106,8 +1106,18 @@ bool Player::SatisfyQuestRace(Quest const* qInfo, bool msg) const uint32 reqraces = qInfo->GetAllowableRaces(); if (reqraces == 0) return true; + if ((reqraces & getRaceMask()) == 0) { + // Allow faction-swapped characters to take quests restricted to their active faction, + // while still respecting truly race-specific quests on the opposite side. + if (GetTeamId() != GetTeamId(true)) + { + uint32 currentFactionRaceMask = (GetTeamId() == TEAM_ALLIANCE) ? RACEMASK_ALLIANCE : RACEMASK_HORDE; + if ((reqraces & currentFactionRaceMask) == reqraces) + return true; + } + if (msg) SendCanTakeQuestResponse(INVALIDREASON_QUEST_FAILED_WRONG_RACE); return false; diff --git a/src/server/game/Entities/Player/PlayerStorage.cpp b/src/server/game/Entities/Player/PlayerStorage.cpp index e81d68d..3439936 100644 --- a/src/server/game/Entities/Player/PlayerStorage.cpp +++ b/src/server/game/Entities/Player/PlayerStorage.cpp @@ -5543,6 +5543,7 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons // must be before inventory (some items required reputation check) m_reputationMgr->LoadFromDB(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_REPUTATION)); + sScriptMgr->OnPlayerReputationLoadFromDB(this); // xinef: load mails before inventory, so problematic items can be added to already loaded mails _LoadMail(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_MAILS), holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_MAIL_ITEMS)); diff --git a/src/server/game/Entities/Player/PlayerUpdates.cpp b/src/server/game/Entities/Player/PlayerUpdates.cpp index f4e5819..3eb1de2 100644 --- a/src/server/game/Entities/Player/PlayerUpdates.cpp +++ b/src/server/game/Entities/Player/PlayerUpdates.cpp @@ -1244,7 +1244,7 @@ void Player::UpdateArea(uint32 newArea) else RemoveByteFlag(UNIT_FIELD_BYTES_2, 1, UNIT_BYTE2_FLAG_SANCTUARY); - uint32 const areaRestFlag = (GetTeamId(true) == TEAM_ALLIANCE) + uint32 const areaRestFlag = (GetTeamId() == TEAM_ALLIANCE) ? AREA_FLAG_REST_ZONE_ALLIANCE : AREA_FLAG_REST_ZONE_HORDE; if (area && area->flags & areaRestFlag) @@ -1305,12 +1305,12 @@ void Player::UpdateZone(uint32 newZone, uint32 newArea, bool force) { case AREATEAM_ALLY: pvpInfo.IsInHostileArea = - GetTeamId(true) != TEAM_ALLIANCE && + GetTeamId() != TEAM_ALLIANCE && (sWorld->IsPvPRealm() || zone->flags & AREA_FLAG_CAPITAL); break; case AREATEAM_HORDE: pvpInfo.IsInHostileArea = - GetTeamId(true) != TEAM_HORDE && + GetTeamId() != TEAM_HORDE && (sWorld->IsPvPRealm() || zone->flags & AREA_FLAG_CAPITAL); break; case AREATEAM_NONE: diff --git a/src/server/game/Reputation/ReputationMgr.cpp b/src/server/game/Reputation/ReputationMgr.cpp index a02c3b6..17fd689 100644 --- a/src/server/game/Reputation/ReputationMgr.cpp +++ b/src/server/game/Reputation/ReputationMgr.cpp @@ -29,6 +29,20 @@ const int32 ReputationMgr::PointsInRank[MAX_REPUTATION_RANK] = {36000, 3000, 300 const int32 ReputationMgr::Reputation_Cap = 42999; const int32 ReputationMgr::Reputation_Bottom = -42000; +namespace +{ + uint32 GetEffectiveReputationRaceMask(Player const* player) + { + // Always use the player's actual race mask so the server BaseRep calculation + // matches what the client computes (client always uses the original race, not + // the current team). This is correct for both the race-change service (where + // getRace() already reflects the new race) and for game modes like Traitor + // that swap team without changing race (where the original race mask is the + // right baseline for reputation display and incremental gain alike). + return player->getRaceMask(); + } +} + ReputationRank ReputationMgr::ReputationToRank(int32 standing) { int32 limit = Reputation_Cap + 1; @@ -93,7 +107,7 @@ int32 ReputationMgr::GetBaseReputation(FactionEntry const* factionEntry) const if (!factionEntry) return 0; - uint32 raceMask = _player->getRaceMask(); + uint32 raceMask = GetEffectiveReputationRaceMask(_player); uint32 classMask = _player->getClassMask(); for (int i = 0; i < 4; i++) { @@ -147,7 +161,7 @@ uint32 ReputationMgr::GetDefaultStateFlags(FactionEntry const* factionEntry) con if (!factionEntry) return 0; - uint32 raceMask = _player->getRaceMask(); + uint32 raceMask = GetEffectiveReputationRaceMask(_player); uint32 classMask = _player->getClassMask(); for (int i = 0; i < 4; i++) { @@ -464,9 +478,17 @@ void ReputationMgr::SetVisible(FactionTemplateEntry const* factionTemplateEntry) return; if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionTemplateEntry->faction)) + { + if (_player->GetTeamId() != _player->GetTeamId(true)) + { + SetVisible(factionEntry); + return; + } + // Never show factions of the opposing team if (!(factionEntry->BaseRepRaceMask[1] & _player->getRaceMask() && factionEntry->BaseRepValue[1] == Reputation_Bottom)) SetVisible(factionEntry); + } } void ReputationMgr::SetVisible(FactionEntry const* factionEntry) diff --git a/src/server/game/Scripting/ScriptDefines/PlayerScript.cpp b/src/server/game/Scripting/ScriptDefines/PlayerScript.cpp index 64e0d2a..fb08253 100644 --- a/src/server/game/Scripting/ScriptDefines/PlayerScript.cpp +++ b/src/server/game/Scripting/ScriptDefines/PlayerScript.cpp @@ -215,6 +215,11 @@ void ScriptMgr::OnPlayerLoadFromDB(Player* player) CALL_ENABLED_HOOKS(PlayerScript, PLAYERHOOK_ON_LOAD_FROM_DB, script->OnPlayerLoadFromDB(player)); } +void ScriptMgr::OnPlayerReputationLoadFromDB(Player* player) +{ + CALL_ENABLED_HOOKS(PlayerScript, PLAYERHOOK_ON_REPUTATION_LOAD_FROM_DB, script->OnPlayerReputationLoadFromDB(player)); +} + void ScriptMgr::OnPlayerBeforeLogout(Player* player) { CALL_ENABLED_HOOKS(PlayerScript, PLAYERHOOK_ON_BEFORE_LOGOUT, script->OnPlayerBeforeLogout(player)); diff --git a/src/server/game/Scripting/ScriptDefines/PlayerScript.h b/src/server/game/Scripting/ScriptDefines/PlayerScript.h index f35acb2..45d22c1 100644 --- a/src/server/game/Scripting/ScriptDefines/PlayerScript.h +++ b/src/server/game/Scripting/ScriptDefines/PlayerScript.h @@ -67,6 +67,7 @@ enum PlayerHook PLAYERHOOK_ON_TEXT_EMOTE, PLAYERHOOK_ON_SPELL_CAST, PLAYERHOOK_ON_LOAD_FROM_DB, + PLAYERHOOK_ON_REPUTATION_LOAD_FROM_DB, PLAYERHOOK_ON_LOGIN, PLAYERHOOK_ON_BEFORE_LOGOUT, PLAYERHOOK_ON_LOGOUT, @@ -320,6 +321,7 @@ public: // Called during data loading virtual void OnPlayerLoadFromDB(Player* /*player*/) { }; + virtual void OnPlayerReputationLoadFromDB(Player* /*player*/) { }; // Called when a player logs in. virtual void OnPlayerLogin(Player* /*player*/) { } diff --git a/src/server/game/Scripting/ScriptMgr.h b/src/server/game/Scripting/ScriptMgr.h index e28d9e2..a2a4308 100644 --- a/src/server/game/Scripting/ScriptMgr.h +++ b/src/server/game/Scripting/ScriptMgr.h @@ -346,6 +346,7 @@ public: /* PlayerScript */ void OnPlayerSpellCast(Player* player, Spell* spell, bool skipCheck); void OnPlayerLogin(Player* player); void OnPlayerLoadFromDB(Player* player); + void OnPlayerReputationLoadFromDB(Player* player); void OnPlayerBeforeLogout(Player* player); void OnPlayerLogout(Player* player); void OnPlayerCreate(Player* player);