diff --git a/lua_scripts/moonwell_game_mode_sync.lua b/lua_scripts/moonwell_game_mode_sync.lua new file mode 100644 index 0000000..315385e --- /dev/null +++ b/lua_scripts/moonwell_game_mode_sync.lua @@ -0,0 +1,86 @@ +local AIO = AIO or require("AIO") + +local ADDON_NAME = "MoonWellClient" +local HANDLER_NAME = "Init" +local GAME_MODE_NORMAL = 0 +local GAME_MODE_TRAITOR = 1 +local PLAYER_EVENT_ON_LOGIN = 3 +local AIO_INIT_SEND_DELAY_MS = 1000 + +local MoonWellAIO = rawget(_G, "MoonWellAIO") or {} +_G.MoonWellAIO = MoonWellAIO +local PendingSyncEvents = {} + +local function GetPlayerGameMode(player) + if not player then + return GAME_MODE_NORMAL + end + + local query = CharDBQuery( + "SELECT game_mode FROM mod_gamemodes_characters WHERE guid = " .. player:GetGUIDLow() .. " LIMIT 1" + ) + + if not query or query:IsNull(0) then + return GAME_MODE_NORMAL + end + + local gameMode = query:GetUInt8(0) + if gameMode == GAME_MODE_TRAITOR then + return GAME_MODE_TRAITOR + end + + return GAME_MODE_NORMAL +end + +function MoonWellAIO.GetPlayerGameMode(player) + return GetPlayerGameMode(player) +end + +function MoonWellAIO.SendPlayerGameMode(player) + if not player then + return GAME_MODE_NORMAL + end + + local gameMode = GetPlayerGameMode(player) + AIO.Handle(player, ADDON_NAME, HANDLER_NAME, gameMode) + return gameMode +end + +local function CancelPendingSync(playerGuidLow) + local eventId = PendingSyncEvents[playerGuidLow] + if eventId then + RemoveEventById(eventId) + PendingSyncEvents[playerGuidLow] = nil + end +end + +function MoonWellAIO.SchedulePlayerGameMode(player, delayMs) + if not player then + return + end + + local playerGuidLow = player:GetGUIDLow() + local playerGuid = player:GetGUID() + + CancelPendingSync(playerGuidLow) + + PendingSyncEvents[playerGuidLow] = CreateLuaEvent(function() + PendingSyncEvents[playerGuidLow] = nil + + local onlinePlayer = GetPlayerByGUID(playerGuid) + if onlinePlayer then + MoonWellAIO.SendPlayerGameMode(onlinePlayer) + end + end, delayMs or AIO_INIT_SEND_DELAY_MS, 1) +end + +-- Do not append MoonWellClient blocks directly into the AIO init packet: +-- the client addon may register its handlers slightly later than AIO itself. +AIO.AddOnInit(function(msg, player) + MoonWellAIO.SchedulePlayerGameMode(player, AIO_INIT_SEND_DELAY_MS) + return msg +end) + +RegisterPlayerEvent(PLAYER_EVENT_ON_LOGIN, function(_, player) + MoonWellAIO.SchedulePlayerGameMode(player, AIO_INIT_SEND_DELAY_MS) +end) diff --git a/modules/mod-eluna/src/ElunaLuaEngine_SC.cpp b/modules/mod-eluna/src/ElunaLuaEngine_SC.cpp index b2e2fbb..f815fea 100644 --- a/modules/mod-eluna/src/ElunaLuaEngine_SC.cpp +++ b/modules/mod-eluna/src/ElunaLuaEngine_SC.cpp @@ -1207,6 +1207,9 @@ public: void OnHeal(Unit* healer, Unit* receiver, uint32& gain) override { + if (!healer) + return; + if (healer->IsPlayer()) sEluna->OnPlayerHeal(healer->ToPlayer(), receiver, gain); @@ -1216,6 +1219,9 @@ public: void OnDamage(Unit* attacker, Unit* receiver, uint32& damage) override { + if (!attacker) + return; + if (attacker->IsPlayer()) sEluna->OnPlayerDamage(attacker->ToPlayer(), receiver, damage); diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp index cf5fe04..606c17c 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.cpp +++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp @@ -70,12 +70,13 @@ void CharacterDatabaseConnection::DoPrepareStatements() PrepareStatement(CHAR_INS_BATTLEGROUND_RANDOM, "INSERT INTO character_battleground_random (guid) VALUES (?)", CONNECTION_ASYNC); // Start LoginQueryHolder content - PrepareStatement(CHAR_SEL_CHARACTER, "SELECT guid, account, name, race, class, gender, level, xp, money, skin, face, hairStyle, hairColor, facialStyle, bankSlots, restState, playerFlags, " + PrepareStatement(CHAR_SEL_CHARACTER, "SELECT characters.guid, account, name, race, class, gender, level, xp, money, skin, face, hairStyle, hairColor, facialStyle, bankSlots, restState, playerFlags, " "position_x, position_y, position_z, map, orientation, taximask, cinematic, totaltime, leveltime, rest_bonus, logout_time, is_logout_resting, resettalents_cost, " "resettalents_time, trans_x, trans_y, trans_z, trans_o, transguid, extra_flags, stable_slots, at_login, zone, online, death_expire_time, taxi_path, instance_mode_mask, " "arenaPoints, totalHonorPoints, todayHonorPoints, yesterdayHonorPoints, totalKills, todayKills, yesterdayKills, chosenTitle, knownCurrencies, watchedFaction, drunk, " "health, power1, power2, power3, power4, power5, power6, power7, instance_id, talentGroupsCount, activeTalentGroup, exploredZones, equipmentCache, ammoId, " - "knownTitles, actionBars, grantableLevels, innTriggerId, extraBonusTalentCount, UNIX_TIMESTAMP(creation_date) FROM characters WHERE guid = ?", CONNECTION_ASYNC); + "knownTitles, actionBars, grantableLevels, innTriggerId, extraBonusTalentCount, UNIX_TIMESTAMP(creation_date), COALESCE(mgc.game_mode, 0) " + "FROM characters LEFT JOIN mod_gamemodes_characters AS mgc ON characters.guid = mgc.guid WHERE characters.guid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_CHARACTER_AURAS, "SELECT casterGuid, itemGuid, spell, effectMask, recalculateMask, stackCount, amount0, amount1, amount2, " "base_amount0, base_amount1, base_amount2, maxDuration, remainTime, remainCharges FROM character_aura WHERE guid = ?", CONNECTION_ASYNC); @@ -113,6 +114,8 @@ void CharacterDatabaseConnection::DoPrepareStatements() PrepareStatement(CHAR_SEL_CHARACTER_RANDOMBG, "SELECT guid FROM character_battleground_random WHERE guid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_CHARACTER_BANNED, "SELECT guid FROM character_banned WHERE guid = ? AND active = 1", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_CHARACTER_QUESTSTATUSREW, "SELECT quest FROM character_queststatus_rewarded WHERE guid = ? AND active = 1", CONNECTION_ASYNC); + PrepareStatement(CHAR_REP_CHAR_GAME_MODE, "REPLACE INTO mod_gamemodes_characters (guid, game_mode) VALUES (?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_DEL_CHAR_GAME_MODE, "DELETE FROM mod_gamemodes_characters WHERE guid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_ACCOUNT_INSTANCELOCKTIMES, "SELECT instanceId, releaseTime FROM account_instance_times WHERE accountId = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_BREW_OF_THE_MONTH, "SELECT lastEventId FROM character_brew_of_the_month WHERE guid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_REP_BREW_OF_THE_MONTH, "REPLACE INTO character_brew_of_the_month (guid, lastEventId) VALUES (?, ?)", CONNECTION_ASYNC); diff --git a/src/server/database/Database/Implementation/CharacterDatabase.h b/src/server/database/Database/Implementation/CharacterDatabase.h index 0329464..b999576 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.h +++ b/src/server/database/Database/Implementation/CharacterDatabase.h @@ -97,6 +97,8 @@ enum CharacterDatabaseStatements : uint32 CHAR_SEL_CHARACTER_RANDOMBG, CHAR_SEL_CHARACTER_BANNED, CHAR_SEL_CHARACTER_QUESTSTATUSREW, + CHAR_REP_CHAR_GAME_MODE, + CHAR_DEL_CHAR_GAME_MODE, CHAR_SEL_ACCOUNT_INSTANCELOCKTIMES, CHAR_SEL_MAILITEMS, CHAR_SEL_BREW_OF_THE_MONTH, diff --git a/src/server/game/Achievements/AchievementMgr.cpp b/src/server/game/Achievements/AchievementMgr.cpp index 2451aac..d983c24 100644 --- a/src/server/game/Achievements/AchievementMgr.cpp +++ b/src/server/game/Achievements/AchievementMgr.cpp @@ -333,7 +333,7 @@ bool AchievementCriteriaData::Meets(uint32 criteria_id, Player const* source, Un if (Player const* player = target->ToPlayer()) if (player->GetDeathTimer() != 0) // flag set == must be same team, not set == different team - return (player->GetTeamId() == source->GetTeamId()) == (player_dead.own_team_flag != 0); + return (player->GetBgTeamId() == source->GetBgTeamId()) == (player_dead.own_team_flag != 0); return false; case ACHIEVEMENT_CRITERIA_DATA_TYPE_S_AURA: return source->HasAuraEffect(aura.spell_id, aura.effect_idx); @@ -389,7 +389,7 @@ bool AchievementCriteriaData::Meets(uint32 criteria_id, Player const* source, Un if (!bg) return false; - uint32 score = bg->GetTeamScore(source->GetTeamId() == TEAM_ALLIANCE ? TEAM_HORDE : TEAM_ALLIANCE); + uint32 score = bg->GetTeamScore(source->GetBgTeamId() == TEAM_ALLIANCE ? TEAM_HORDE : TEAM_ALLIANCE); return score >= bg_loss_team_score.min_score && score <= bg_loss_team_score.max_score; } case ACHIEVEMENT_CRITERIA_DATA_TYPE_INSTANCE_SCRIPT: @@ -462,7 +462,7 @@ bool AchievementCriteriaData::Meets(uint32 criteria_id, Player const* source, Un uint32 winnnerScore = bg->GetTeamScore(winnerTeam); uint32 loserScore = bg->GetTeamScore(TeamId(!uint32(winnerTeam))); - return source->GetTeamId() == winnerTeam && winnnerScore == teams_scores.winner_score && loserScore == teams_scores.loser_score; + return source->GetBgTeamId() == winnerTeam && winnnerScore == teams_scores.winner_score && loserScore == teams_scores.loser_score; } default: break; @@ -635,7 +635,7 @@ void AchievementMgr::LoadFromDB(PreparedQueryResult achievementResult, PreparedQ // title achievement rewards are retroactive if (AchievementReward const* reward = sAchievementMgr->GetAchievementReward(achievement)) - if (uint32 titleId = reward->titleId[Player::TeamIdForRace(GetPlayer()->getRace())]) + if (uint32 titleId = reward->titleId[uint8(GetPlayer()->GetTeamId())]) if (CharTitlesEntry const* titleEntry = sCharTitlesStore.LookupEntry(titleId)) if (!GetPlayer()->HasTitle(titleEntry)) GetPlayer()->SetTitle(titleEntry); @@ -2448,8 +2448,8 @@ bool AchievementMgr::CanUpdateCriteria(AchievementCriteriaEntry const* criteria, if (achievement->mapID != -1 && GetPlayer()->GetMapId() != uint32(achievement->mapID)) return false; - if ((achievement->requiredFaction == ACHIEVEMENT_FACTION_HORDE && GetPlayer()->GetTeamId(true) != TEAM_HORDE) || - (achievement->requiredFaction == ACHIEVEMENT_FACTION_ALLIANCE && GetPlayer()->GetTeamId(true) != TEAM_ALLIANCE)) + if ((achievement->requiredFaction == ACHIEVEMENT_FACTION_HORDE && GetPlayer()->GetTeamId() != TEAM_HORDE) || + (achievement->requiredFaction == ACHIEVEMENT_FACTION_ALLIANCE && GetPlayer()->GetTeamId() != TEAM_ALLIANCE)) return false; for (uint32 i = 0; i < MAX_CRITERIA_REQUIREMENTS; ++i) diff --git a/src/server/game/DungeonFinding/LFGMgr.cpp b/src/server/game/DungeonFinding/LFGMgr.cpp index 61724fa..1105d92 100644 --- a/src/server/game/DungeonFinding/LFGMgr.cpp +++ b/src/server/game/DungeonFinding/LFGMgr.cpp @@ -430,7 +430,7 @@ namespace lfg { if (!itemRequirement->checkLeaderOnly || !group || group->GetLeaderGUID() == player->GetGUID()) { - if (itemRequirement->faction == TEAM_NEUTRAL || itemRequirement->faction == player->GetTeamId(true)) + if (itemRequirement->faction == TEAM_NEUTRAL || itemRequirement->faction == player->GetTeamId()) { if (!player->HasItemCount(itemRequirement->id, 1)) { @@ -446,7 +446,7 @@ namespace lfg { if (!questRequirement->checkLeaderOnly || !group || group->GetLeaderGUID() == player->GetGUID()) { - if (questRequirement->faction == TEAM_NEUTRAL || questRequirement->faction == player->GetTeamId(true)) + if (questRequirement->faction == TEAM_NEUTRAL || questRequirement->faction == player->GetTeamId()) { if (!player->GetQuestRewardStatus(questRequirement->id)) { @@ -468,7 +468,7 @@ namespace lfg { if (!achievementRequirement->checkLeaderOnly || !group || group->GetLeaderGUID() == player->GetGUID()) { - if (achievementRequirement->faction == TEAM_NEUTRAL || achievementRequirement->faction == player->GetTeamId(true)) + if (achievementRequirement->faction == TEAM_NEUTRAL || achievementRequirement->faction == player->GetTeamId()) { if (!player->HasAchieved(achievementRequirement->id)) { diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index d5cf492..0eab56b 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -147,6 +147,80 @@ enum CharacterCustomizeFlags CHAR_CUSTOMIZE_FLAG_RACE = 0x00100000 // name, gender, race, etc... }; +namespace +{ + constexpr uint32 ALLIANCE_TRAITOR_REPUTATION_FACTIONS[] = { 72, 47, 69, 54, 930, 890, 730, 509 }; + constexpr uint32 HORDE_TRAITOR_REPUTATION_FACTIONS[] = { 76, 81, 68, 530, 911, 889, 729, 510 }; + constexpr uint32 TRAITOR_REPUTATION_FACTION_COUNT = 8; + + bool IsGameModeEnabled(PlayerGameMode gameMode) + { + if (gameMode == PLAYER_GAMEMODE_NORMAL) + return true; + + if (!sConfigMgr->GetOption("GameModes.Enable", true)) + return false; + + switch (gameMode) + { + case PLAYER_GAMEMODE_TRAITOR: + return sConfigMgr->GetOption("GameModes.Traitor.Enable", true); + case PLAYER_GAMEMODE_NORMAL: + default: + return true; + } + } + + TeamId GetOppositeTeamId(TeamId team) + { + switch (team) + { + case TEAM_ALLIANCE: + return TEAM_HORDE; + case TEAM_HORDE: + return TEAM_ALLIANCE; + default: + return TEAM_NEUTRAL; + } + } + + PlayerGameMode NormalizeGameModeValue(uint8 gameMode) + { + return gameMode == PLAYER_GAMEMODE_TRAITOR ? PLAYER_GAMEMODE_TRAITOR : PLAYER_GAMEMODE_NORMAL; + } + + PlayerGameMode GameModeFromOutfitId(uint8 outfitId) + { + if (outfitId == 0) + return PLAYER_GAMEMODE_NORMAL; + + // We repurpose the create packet's outfit byte for game mode selection. + // The stock client normally sends 0 here, so treat any non-zero custom value + // as a request for the only create-time custom mode we currently support. + PlayerGameMode gameMode = NormalizeGameModeValue(outfitId); + + if (gameMode == PLAYER_GAMEMODE_NORMAL) + gameMode = PLAYER_GAMEMODE_TRAITOR; + + return IsGameModeEnabled(gameMode) ? gameMode : PLAYER_GAMEMODE_NORMAL; + } + + uint8 GetRepresentativeRaceForTeam(TeamId team) + { + return team == TEAM_HORDE ? RACE_ORC : RACE_HUMAN; + } + + PlayerInfo const* GetRepresentativePlayerInfoForTeam(TeamId team, uint8 classId) + { + uint8 representativeRace = GetRepresentativeRaceForTeam(team); + + if (PlayerInfo const* info = sObjectMgr->GetPlayerInfo(representativeRace, classId)) + return info; + + return sObjectMgr->GetPlayerInfo(representativeRace, CLASS_WARRIOR); + } +} + static uint32 copseReclaimDelay[MAX_DEATH_COUNT] = { 30, 60, 120 }; // we can disable this warning for this since it only @@ -492,6 +566,7 @@ bool Player::Create(ObjectGuid::LowType guidlow, CharacterCreateInfo* createInfo // also do it in Player::BuildEnumData, Player::LoadFromDB Object::_Create(guidlow, 0, HighGuid::Player); + SetGameMode(GameModeFromOutfitId(m_outfitId)); m_name = createInfo->Name; @@ -506,8 +581,6 @@ bool Player::Create(ObjectGuid::LowType guidlow, CharacterCreateInfo* createInfo for (uint8 i = 0; i < PLAYER_SLOTS_COUNT; i++) m_items[i] = nullptr; - Relocate(info->positionX, info->positionY, info->positionZ, info->orientation); - ChrClassesEntry const* cEntry = sChrClassesStore.LookupEntry(createInfo->Class); if (!cEntry) { @@ -516,8 +589,6 @@ bool Player::Create(ObjectGuid::LowType guidlow, CharacterCreateInfo* createInfo return false; } - SetMap(sMapMgr->CreateMap(info->mapId, this)); - uint8 powertype = cEntry->powerType; SetObjectScale(1.0f); @@ -525,8 +596,6 @@ bool Player::Create(ObjectGuid::LowType guidlow, CharacterCreateInfo* createInfo m_realRace = createInfo->Race; // set real race flag m_race = createInfo->Race; // set real race flag - SetFactionForRace(createInfo->Race); - if (!IsValidGender(createInfo->Gender)) { LOG_ERROR("entities.player", "Player::Create: Possible hacking-attempt: Account {} tried creating a character named '{}' with an invalid gender ({}) - refusing to do so", @@ -535,8 +604,14 @@ bool Player::Create(ObjectGuid::LowType guidlow, CharacterCreateInfo* createInfo } uint32 RaceClassGender = (createInfo->Race) | (createInfo->Class << 8) | (createInfo->Gender << 16); - SetUInt32Value(UNIT_FIELD_BYTES_0, (RaceClassGender | (powertype << 24))); + + SetFactionForRace(createInfo->Race); + + WorldLocation startPosition = GetStartPosition(); + Relocate(startPosition); + SetMap(sMapMgr->CreateMap(startPosition.GetMapId(), this)); + InitDisplayIds(); if (sWorld->getIntConfig(CONFIG_GAME_TYPE) == REALM_TYPE_PVP || sWorld->getIntConfig(CONFIG_GAME_TYPE) == REALM_TYPE_RPPVP) { @@ -4068,6 +4143,10 @@ void Player::DeleteFromDB(ObjectGuid::LowType lowGuid, uint32 accountId, bool up stmt->SetData(0, lowGuid); trans->Append(stmt); + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_GAME_MODE); + stmt->SetData(0, lowGuid); + trans->Append(stmt); + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_PLAYER_ACCOUNT_DATA); stmt->SetData(0, lowGuid); trans->Append(stmt); @@ -5853,17 +5932,46 @@ TeamId Player::TeamIdForRace(uint8 race) void Player::SetFactionForRace(uint8 race) { - m_team = TeamIdForRace(race); + TeamId nativeTeam = TeamIdForRace(race); + m_team = IsTraitor() ? GetOppositeTeamId(nativeTeam) : nativeTeam; sScriptMgr->OnPlayerUpdateFaction(this); - if (GetTeamId(true) != GetTeamId()) - return; - - ChrRacesEntry const* rEntry = sChrRacesStore.LookupEntry(race); + uint8 factionRace = IsTraitor() ? GetRepresentativeRaceForTeam(m_team) : race; + ChrRacesEntry const* rEntry = sChrRacesStore.LookupEntry(factionRace); SetFaction(rEntry ? rEntry->FactionID : 0); } +PlayerGameMode Player::ResolveGameModeFromStorage(uint32 playerFlags, uint8 gameMode) +{ + PlayerGameMode resolvedGameMode = NormalizeGameModeValue(gameMode); + + if (resolvedGameMode == PLAYER_GAMEMODE_NORMAL && (playerFlags & PLAYER_FLAGS_TRAITOR)) + resolvedGameMode = PLAYER_GAMEMODE_TRAITOR; + + return IsGameModeEnabled(resolvedGameMode) ? resolvedGameMode : PLAYER_GAMEMODE_NORMAL; +} + +PlayerInfo const* Player::GetStartPlayerInfo() const +{ + if (IsTraitor()) + return GetRepresentativePlayerInfoForTeam(GetTeamId(), getClass()); + + return sObjectMgr->GetPlayerInfo(getRace(true), getClass()); +} + +void Player::SetGameMode(PlayerGameMode gameMode) +{ + m_gameMode = NormalizeGameModeValue(static_cast(gameMode)); + if (!IsGameModeEnabled(m_gameMode)) + m_gameMode = PLAYER_GAMEMODE_NORMAL; + + if (m_gameMode == PLAYER_GAMEMODE_TRAITOR) + SetPlayerFlag(PLAYER_FLAGS_TRAITOR); + else + RemovePlayerFlag(PLAYER_FLAGS_TRAITOR); +} + ReputationRank Player::GetReputationRank(uint32 faction) const { FactionEntry const* factionEntry = sFactionStore.LookupEntry(faction); @@ -6089,6 +6197,78 @@ void Player::RewardReputation(Quest const* quest) } } +void Player::ApplyTraitorReputationState() +{ + if (!IsTraitor()) + return; + + auto normalizeFactionState = [this](FactionEntry const* factionEntry, bool visible, bool atWar) + { + if (!factionEntry || factionEntry->reputationListID < 0) + return; + + FactionState* factionState = const_cast(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; + }; + + auto setFactionStanding = [this, &normalizeFactionState](uint32 factionId, int32 targetDisplayed, bool visible, bool atWar) + { + FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId); + if (!factionEntry) + return; + + GetReputationMgr().SetOneFactionReputation(factionEntry, static_cast(targetDisplayed), false); + normalizeFactionState(factionEntry, visible, atWar); + }; + + auto normalizeFactionAndParent = [&normalizeFactionState](uint32 factionId, bool visible, bool atWar) + { + if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId)) + { + normalizeFactionState(factionEntry, visible, atWar); + + if (factionEntry->team) + normalizeFactionState(sFactionStore.LookupEntry(factionEntry->team), visible, false); + } + }; + + TeamId nativeTeam = GetTeamId(true); + uint32 const* friendlyFactions = nativeTeam == TEAM_ALLIANCE ? HORDE_TRAITOR_REPUTATION_FACTIONS : ALLIANCE_TRAITOR_REPUTATION_FACTIONS; + uint32 const* hostileFactions = nativeTeam == TEAM_ALLIANCE ? ALLIANCE_TRAITOR_REPUTATION_FACTIONS : HORDE_TRAITOR_REPUTATION_FACTIONS; + + if (HasAtLoginFlag(AT_LOGIN_FIRST)) + { + for (uint32 i = 0; i < TRAITOR_REPUTATION_FACTION_COUNT; ++i) + setFactionStanding(friendlyFactions[i], 0, true, false); + + for (uint32 i = 0; i < TRAITOR_REPUTATION_FACTION_COUNT; ++i) + setFactionStanding(hostileFactions[i], -6000, false, true); + } + + for (uint32 i = 0; i < TRAITOR_REPUTATION_FACTION_COUNT; ++i) + { + normalizeFactionAndParent(friendlyFactions[i], true, false); + normalizeFactionAndParent(hostileFactions[i], false, true); + } +} + void Player::RewardExtraBonusTalentPoints(uint32 bonusTalentPoints) { if (bonusTalentPoints) @@ -10304,7 +10484,7 @@ bool Player::ActivateTaxiPathTo(std::vector const& nodes, Creature* npc // only one mount ID for both sides. Probably not good to use 315 in case DBC nodes // change but I couldn't find a suitable alternative. OK to use class because only DK // can use this taxi. - uint32 mount_display_id = sObjectMgr->GetTaxiMountDisplayId(sourcenode, GetTeamId(true), npc == nullptr || (sourcenode == 315 && IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_TAXI))); + uint32 mount_display_id = sObjectMgr->GetTaxiMountDisplayId(sourcenode, GetTeamId(), npc == nullptr || (sourcenode == 315 && IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_TAXI))); // in spell case allow 0 model if ((mount_display_id == 0 && spellid == 0) || sourcepath == 0) @@ -10399,7 +10579,7 @@ void Player::ContinueTaxiFlight() LOG_DEBUG("entities.unit", "WORLD: Restart character {} taxi flight", GetGUID().ToString()); - uint32 mountDisplayId = sObjectMgr->GetTaxiMountDisplayId(sourceNode, GetTeamId(true), true); + uint32 mountDisplayId = sObjectMgr->GetTaxiMountDisplayId(sourceNode, GetTeamId(), true); if (!mountDisplayId) return; @@ -10678,7 +10858,7 @@ bool Player::BuyItemFromVendorSlot(ObjectGuid vendorguid, uint32 vendorslot, uin return false; } - if (!IsGameMaster() && ((pProto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && GetTeamId(true) == TEAM_ALLIANCE) || (pProto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && GetTeamId(true) == TEAM_HORDE))) + if (!IsGameMaster() && ((pProto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && GetTeamId() == TEAM_ALLIANCE) || (pProto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && GetTeamId() == TEAM_HORDE))) { return false; } @@ -11341,7 +11521,7 @@ void Player::ReportedAfkBy(Player* reporter) WorldLocation Player::GetStartPosition() const { - PlayerInfo const* info = sObjectMgr->GetPlayerInfo(getRace(true), getClass()); + PlayerInfo const* info = GetStartPlayerInfo(); uint32 mapId = info->mapId; if (IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_INIT) && HasSpell(50977)) return WorldLocation(0, 2352.0f, -5709.0f, 154.5f, 0.0f); @@ -14701,6 +14881,7 @@ void Player::_SaveEntryPoint(CharacterDatabaseTransaction trans) stmt->SetData(7, m_entryPointData.taxiPath[1]); stmt->SetData(8, m_entryPointData.mountSpell); trans->Append(stmt); + } void Player::DeleteEquipmentSet(uint64 setGuid) @@ -14768,6 +14949,25 @@ void Player::_SaveCharacter(bool create, CharacterDatabaseTransaction trans) uint8 index = 0; auto finiteAlways = [](float f) { return std::isfinite(f) ? f : 0.0f; }; + PlayerGameMode persistedGameMode = GetGameMode(); + + // Character enum and login restore traitor mode from persisted storage, not from the + // transient create packet. Re-derive the initial mode from the raw create byte before + // the first save in case later create initialization has reset the in-memory state. + if (create && persistedGameMode == PLAYER_GAMEMODE_NORMAL && m_outfitId != 0) + { + persistedGameMode = GameModeFromOutfitId(m_outfitId); + + if (persistedGameMode != GetGameMode()) + { + SetGameMode(persistedGameMode); + SetFactionForRace(getRace(true)); + } + } + + uint32 playerFlagsForSave = GetPlayerFlags(); + if (create && persistedGameMode == PLAYER_GAMEMODE_TRAITOR) + playerFlagsForSave |= PLAYER_FLAGS_TRAITOR; if (create) { @@ -14790,7 +14990,7 @@ void Player::_SaveCharacter(bool create, CharacterDatabaseTransaction trans) stmt->SetData(index++, GetByteValue(PLAYER_BYTES_2, 0)); stmt->SetData(index++, GetByteValue(PLAYER_BYTES_2, 2)); stmt->SetData(index++, GetByteValue(PLAYER_BYTES_2, 3)); - stmt->SetData(index++, (uint32)GetPlayerFlags()); + stmt->SetData(index++, playerFlagsForSave); stmt->SetData(index++, (uint16)GetMapId()); stmt->SetData(index++, (uint32)GetInstanceId()); stmt->SetData(index++, (uint8(GetDungeonDifficulty()) | uint8(GetRaidDifficulty()) << 4)); @@ -14908,7 +15108,7 @@ void Player::_SaveCharacter(bool create, CharacterDatabaseTransaction trans) stmt->SetData(index++, GetByteValue(PLAYER_BYTES_2, 0)); stmt->SetData(index++, GetByteValue(PLAYER_BYTES_2, 2)); stmt->SetData(index++, GetByteValue(PLAYER_BYTES_2, 3)); - stmt->SetData(index++, GetPlayerFlags()); + stmt->SetData(index++, playerFlagsForSave); if (!IsBeingTeleported()) { @@ -15036,6 +15236,20 @@ void Player::_SaveCharacter(bool create, CharacterDatabaseTransaction trans) } trans->Append(stmt); + + if (persistedGameMode == PLAYER_GAMEMODE_TRAITOR) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_CHAR_GAME_MODE); + stmt->SetData(0, GetGUID().GetCounter()); + stmt->SetData(1, uint8(persistedGameMode)); + } + else + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_GAME_MODE); + stmt->SetData(0, GetGUID().GetCounter()); + } + + trans->Append(stmt); } void Player::_LoadGlyphs(PreparedQueryResult result) diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 7ae8990..f347b5e 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -454,6 +454,13 @@ enum DrunkenState #define MAX_DRUNKEN 4 +enum PlayerGameMode : uint8 +{ + PLAYER_GAMEMODE_NORMAL = 0, + PLAYER_GAMEMODE_TRAITOR = 1, + PLAYER_GAMEMODE_MAX +}; + enum PlayerFlags : uint32 { PLAYER_FLAGS_GROUP_LEADER = 0x00000001, @@ -1156,7 +1163,7 @@ public: PlayerSocial* GetSocial() { return m_social; } PlayerTaxi m_taxi; - void InitTaxiNodesForLevel() { m_taxi.InitTaxiNodesForLevel(getRace(), getClass(), GetLevel()); } + void InitTaxiNodesForLevel() { m_taxi.InitTaxiNodesForLevel(IsTraitor() ? (GetTeamId() == TEAM_ALLIANCE ? RACE_HUMAN : RACE_ORC) : getRace(), getClass(), GetLevel()); } bool ActivateTaxiPathTo(std::vector const& nodes, Creature* npc = nullptr, uint32 spellid = 1); bool ActivateTaxiPathTo(uint32 taxi_path_id, uint32 spellid = 1); void CleanupAfterTaxiFlight(); @@ -2126,7 +2133,13 @@ public: void CheckAreaExploreAndOutdoor(); static TeamId TeamIdForRace(uint8 race); + [[nodiscard]] static PlayerGameMode ResolveGameModeFromStorage(uint32 playerFlags, uint8 gameMode); [[nodiscard]] TeamId GetTeamId(bool original = false) const { return original ? TeamIdForRace(getRace(true)) : m_team; }; + [[nodiscard]] PlayerGameMode GetGameMode() const { return m_gameMode; } + [[nodiscard]] bool HasGameMode(PlayerGameMode gameMode) const { return m_gameMode == gameMode; } + [[nodiscard]] bool IsTraitor() const { return HasGameMode(PLAYER_GAMEMODE_TRAITOR); } + [[nodiscard]] PlayerInfo const* GetStartPlayerInfo() const; + void SetGameMode(PlayerGameMode gameMode); void SetFactionForRace(uint8 race); void setTeamId(TeamId teamid) { m_team = teamid; }; @@ -2150,6 +2163,7 @@ public: [[nodiscard]] ReputationRank GetReputationRank(uint32 faction_id) const; void RewardReputation(Unit* victim); void RewardReputation(Quest const* quest); + void ApplyTraitorReputationState(); float CalculateReputationGain(ReputationSource source, uint32 creatureOrQuestLevel, float rep, int32 faction, bool noQuestBonus = false); diff --git a/src/server/game/Entities/Player/PlayerStorage.cpp b/src/server/game/Entities/Player/PlayerStorage.cpp index 3439936..f2de6e7 100644 --- a/src/server/game/Entities/Player/PlayerStorage.cpp +++ b/src/server/game/Entities/Player/PlayerStorage.cpp @@ -65,6 +65,7 @@ #include "Util.h" #include "World.h" #include "WorldPacket.h" +#include /// @todo: this import is not necessary for compilation and marked as unused by the IDE // however, for some reasons removing it would cause a damn linking issue @@ -2378,12 +2379,12 @@ InventoryResult Player::CanUseItem(ItemTemplate const* proto) const return EQUIP_ERR_ITEM_NOT_FOUND; } - if (proto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && GetTeamId(true) != TEAM_HORDE) + if (proto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && GetTeamId() != TEAM_HORDE) { return EQUIP_ERR_YOU_CAN_NEVER_USE_THAT_ITEM; } - if (proto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && GetTeamId(true) != TEAM_ALLIANCE) + if (proto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && GetTeamId() != TEAM_ALLIANCE) { return EQUIP_ERR_YOU_CAN_NEVER_USE_THAT_ITEM; } @@ -5014,8 +5015,8 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons //"arenaPoints, totalHonorPoints, todayHonorPoints, yesterdayHonorPoints, totalKills, todayKills, yesterdayKills, chosenTitle, knownCurrencies, watchedFaction, drunk, " // 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 //"health, power1, power2, power3, power4, power5, power6, power7, instance_id, talentGroupsCount, activeTalentGroup, exploredZones, equipmentCache, ammoId, knownTitles, - // 70 71 72 73 74 - //"actionBars, grantableLevels, innTriggerId, extraBonusTalentCount, UNIX_TIMESTAMP(creation_date) FROM characters WHERE guid = '{}'", guid); + // 70 71 72 73 74 75 + //"actionBars, grantableLevels, innTriggerId, extraBonusTalentCount, UNIX_TIMESTAMP(creation_date), game_mode FROM characters WHERE guid = '{}'", guid); PreparedQueryResult result = holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_FROM); if (!result) @@ -5093,6 +5094,8 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons // load character creation date, relevant for achievements of type average SetCreationTime(fields[74].Get()); + PlayerGameMode storedGameMode = ResolveGameModeFromStorage(fields[16].Get(), fields[75].Get()); + SetGameMode(storedGameMode); // load achievements before anything else to prevent multiple gains for the same achievement/criteria on every loading (as loading does call UpdateAchievementCriteria) m_achievementMgr->LoadFromDB(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_ACHIEVEMENTS), holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_CRITERIA_PROGRESS), holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_OFFLINE_ACHIEVEMENTS_UPDATES)); @@ -5112,6 +5115,7 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons SetByteValue(PLAYER_BYTES_3, 0, fields[5].Get()); SetByteValue(PLAYER_BYTES_3, 1, fields[54].Get()); ReplaceAllPlayerFlags((PlayerFlags)fields[16].Get()); + SetGameMode(storedGameMode); SetInt32Value(PLAYER_FIELD_WATCHED_FACTION_INDEX, fields[53].Get()); SetUInt64Value(PLAYER_FIELD_KNOWN_CURRENCIES, fields[52].Get()); @@ -5149,12 +5153,22 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons InitPrimaryProfessions(); // to max set before any spell loaded + bool isFirstLogin = (fields[38].Get() & AT_LOGIN_FIRST) != 0; + // init saved position, and fix it later if problematic int32 transLowGUID = fields[35].Get(); Relocate(fields[17].Get(), fields[18].Get(), fields[19].Get(), fields[21].Get()); uint32 mapId = fields[20].Get(); uint32 instanceId = fields[63].Get(); + if (isFirstLogin && IsTraitor()) + { + WorldLocation startPosition = GetStartPosition(); + Relocate(startPosition); + mapId = startPosition.GetMapId(); + instanceId = 0; + } + uint32 dungeonDiff = fields[43].Get() & 0x0F; if (dungeonDiff >= MAX_DUNGEON_DIFFICULTY) dungeonDiff = DUNGEON_DIFFICULTY_NORMAL; @@ -5311,7 +5325,7 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons else if (!taxi_nodes.empty()) { instanceId = 0; - if (!m_taxi.LoadTaxiDestinationsFromString(taxi_nodes, GetTeamId(true))) + if (!m_taxi.LoadTaxiDestinationsFromString(taxi_nodes, GetTeamId())) { // xinef: could no load valid data for taxi, relocate to homebind and clear m_taxi.ClearTaxiDestinations(); @@ -6851,7 +6865,7 @@ bool Player::Satisfy(DungeonProgressionRequirements const* ar, uint32 target_map missingItems = &missingLeaderItems; } - if (itemRequirement->faction == TEAM_NEUTRAL || itemRequirement->faction == checkPlayer->GetTeamId(true)) + if (itemRequirement->faction == TEAM_NEUTRAL || itemRequirement->faction == checkPlayer->GetTeamId()) { if (!checkPlayer->HasItemCount(itemRequirement->id, 1)) { @@ -6873,7 +6887,7 @@ bool Player::Satisfy(DungeonProgressionRequirements const* ar, uint32 target_map missingAchievements = &missingLeaderAchievements; } - if (achievementRequirement->faction == TEAM_NEUTRAL || achievementRequirement->faction == GetTeamId(true)) + if (achievementRequirement->faction == TEAM_NEUTRAL || achievementRequirement->faction == GetTeamId()) { if (!checkPlayer || !checkPlayer->HasAchieved(achievementRequirement->id)) { @@ -6895,7 +6909,7 @@ bool Player::Satisfy(DungeonProgressionRequirements const* ar, uint32 target_map missingQuests = &missingLeaderQuests; } - if (questRequirement->faction == TEAM_NEUTRAL || questRequirement->faction == checkPlayer->GetTeamId(true)) + if (questRequirement->faction == TEAM_NEUTRAL || questRequirement->faction == checkPlayer->GetTeamId()) { if (!checkPlayer->GetQuestRewardStatus(questRequirement->id)) { @@ -7082,8 +7096,9 @@ bool Player::CheckInstanceCount(uint32 instanceId) const bool Player::_LoadHomeBind(PreparedQueryResult result) { - PlayerInfo const* info = sObjectMgr->GetPlayerInfo(getRace(true), getClass()); - if (!info) + PlayerInfo const* info = GetStartPlayerInfo(); + PlayerInfo const* nativeInfo = sObjectMgr->GetPlayerInfo(getRace(true), getClass()); + if (!info || !nativeInfo) { LOG_ERROR("entities.player", "Player (Name {}) has incorrect race/class pair. Can't be loaded.", GetName()); return false; @@ -7115,6 +7130,25 @@ bool Player::_LoadHomeBind(PreparedQueryResult result) } } + if (ok && IsTraitor()) + { + auto sameCoord = [](float left, float right) { return std::fabs(left - right) < 0.01f; }; + bool matchesNativeHomebind = m_homebindMapId == nativeInfo->mapId && + m_homebindAreaId == nativeInfo->areaId && + sameCoord(m_homebindX, nativeInfo->positionX) && + sameCoord(m_homebindY, nativeInfo->positionY) && + sameCoord(m_homebindZ, nativeInfo->positionZ); + + bool matchesEffectiveHomebind = m_homebindMapId == info->mapId && + m_homebindAreaId == info->areaId && + sameCoord(m_homebindX, info->positionX) && + sameCoord(m_homebindY, info->positionY) && + sameCoord(m_homebindZ, info->positionZ); + + if (matchesNativeHomebind && !matchesEffectiveHomebind) + SetHomebind(WorldLocation(info->mapId, info->positionX, info->positionY, info->positionZ, 0.0f), info->areaId); + } + if (!ok) { m_homebindMapId = info->mapId; diff --git a/src/server/game/Handlers/CharacterHandler.cpp b/src/server/game/Handlers/CharacterHandler.cpp index b23dc92..0d9b128 100644 --- a/src/server/game/Handlers/CharacterHandler.cpp +++ b/src/server/game/Handlers/CharacterHandler.cpp @@ -548,7 +548,9 @@ void WorldSession::HandleCharCreateOpcode(WorldPacket& recvData) return; } - if ((haveSameRace && skipCinematics == 1) || skipCinematics == 2) + if (newChar->IsTraitor()) + newChar->setCinematic(1); + else if ((haveSameRace && skipCinematics == 1) || skipCinematics == 2) newChar->setCinematic(1); // not show intro newChar->SetAtLoginFlag(AT_LOGIN_FIRST); // First login @@ -558,6 +560,7 @@ void WorldSession::HandleCharCreateOpcode(WorldPacket& recvData) // Player created, save it now newChar->SaveToDB(characterTransaction, true, false); + createInfo->CharCount++; LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_DEL_REALM_CHARACTERS_BY_REALM); diff --git a/src/server/game/Loot/LootMgr.cpp b/src/server/game/Loot/LootMgr.cpp index 74e8b75..91bec1d 100644 --- a/src/server/game/Loot/LootMgr.cpp +++ b/src/server/game/Loot/LootMgr.cpp @@ -446,12 +446,12 @@ bool LootItem::AllowedForPlayer(Player const* player, ObjectGuid source) const } // not show loot for not own team - if (pProto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && player->GetTeamId(true) != TEAM_HORDE) + if (pProto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && player->GetTeamId() != TEAM_HORDE) { return false; } - if (pProto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && player->GetTeamId(true) != TEAM_ALLIANCE) + if (pProto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && player->GetTeamId() != TEAM_ALLIANCE) { return false; }