From ade323f7b373cf38e83b79037ff06b1694b3dfc8 Mon Sep 17 00:00:00 2001 From: luis Date: Thu, 12 Mar 2026 08:27:04 -0300 Subject: [PATCH] =?UTF-8?q?Pet=20Battles:=20PR=20readiness=20fixes=20?= =?UTF-8?q?=E2=80=94=20validation,=20timeouts,=20logging,=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add per-round AFK timeout (30s + 15s grace, auto-forfeit) - Add input validation: MoveType range check, duplicate input guard, battle state checks, distance check for wild battles, queue loadout check - Fix 28 TC_LOG_ERROR → TC_LOG_DEBUG/WARN for non-error messages - Send SMSG_BATTLE_PETS_HEALED after HealBattlePetsPct - Send SMSG_PET_BATTLE_MAX_GAME_LENGTH_WARNING 5 min before timeout - Fix CMSG_BATTLE_PET_SUMMON threading (PROCESS_INPLACE → THREADUNSAFE) - Remove unused ResolveSpeed() and opponentTeam variable - Fix TOOD typo, add ChatRestricted TODO comment --- src/server/game/BattlePets/BattlePetMgr.cpp | 6 +- src/server/game/BattlePets/PetBattle.cpp | 73 +++++++++++------- src/server/game/BattlePets/PetBattle.h | 2 +- src/server/game/Handlers/BattlePetHandler.cpp | 75 +++++++++++++++---- .../game/Server/Packets/BattlePetPackets.h | 4 + src/server/game/Server/Protocol/Opcodes.cpp | 2 +- 6 files changed, 119 insertions(+), 43 deletions(-) diff --git a/src/server/game/BattlePets/BattlePetMgr.cpp b/src/server/game/BattlePets/BattlePetMgr.cpp index 666a92e45..574f56b07 100644 --- a/src/server/game/BattlePets/BattlePetMgr.cpp +++ b/src/server/game/BattlePets/BattlePetMgr.cpp @@ -75,7 +75,7 @@ namespace BattlePets speed *= battlePetBreedQuality->StateMultiplier; break; } - // TOOD: add check if pet has existing quality + // TODO: add check if pet has existing quality } // scale stats depending on level @@ -881,6 +881,10 @@ namespace BattlePets } SendUpdates(updates, false); + + // Notify the client that battle pets have been healed (triggers UI feedback) + WorldPackets::BattlePet::BattlePetsHealed healed; + _owner->SendPacket(healed.Write()); } void BattlePetMgr::UpdateBattlePetData(ObjectGuid guid) diff --git a/src/server/game/BattlePets/PetBattle.cpp b/src/server/game/BattlePets/PetBattle.cpp index 39c40bc8e..62f8356fe 100644 --- a/src/server/game/BattlePets/PetBattle.cpp +++ b/src/server/game/BattlePets/PetBattle.cpp @@ -136,7 +136,7 @@ namespace PetBattles ++loaded; } - TC_LOG_ERROR("server.loading", "PetBattle LoadWildPetAbilities: Species={} Level={} entries={} loaded={} abilities=[{},{},{}]", + TC_LOG_DEBUG("server.loading", "PetBattle LoadWildPetAbilities: Species={} Level={} entries={} loaded={} abilities=[{},{},{}]", pet.Species, pet.Level, speciesAbilities->size(), loaded, pet.AbilityIDs[0], pet.AbilityIDs[1], pet.AbilityIDs[2]); } @@ -355,6 +355,32 @@ namespace PetBattles } } + // Per-round AFK timeout ? auto-forfeit if a player hasn't submitted input + if (_state == PET_BATTLE_STATE_ROUND_IN_PROGRESS || _state == PET_BATTLE_STATE_WAITING_FOR_FRONT_PET) + { + _roundTimerSecs++; + // Grace period: round time + 15 seconds + if (_roundTimerSecs > PET_BATTLE_MAX_ROUND_TIME + 15) + { + if (_battleType == PET_BATTLE_TYPE_PVP || _battleType == PET_BATTLE_TYPE_LFPB) + { + for (uint8 i = 0; i < MAX_PET_BATTLE_PLAYERS; ++i) + { + if (!_teams[i].HasInputThisRound) + { + Forfeit(i); + return; + } + } + } + else + { + Forfeit(PET_BATTLE_TEAM_1); + return; + } + } + } + // Handle finish delay ? wait for death animation before sending FinalRound if (_state == PET_BATTLE_STATE_FINAL_ROUND && _finishDelayMs > 0) { @@ -382,6 +408,7 @@ namespace PetBattles team.PendingAbilityID = abilityID; team.PendingNewFrontPet = newFrontPet; team.HasInputThisRound = true; + _roundTimerSecs = 0; } bool PetBattle::BothTeamsReady() const @@ -398,6 +425,7 @@ namespace PetBattles { _state = PET_BATTLE_STATE_ROUND_IN_PROGRESS; _currentRound++; + _roundTimerSecs = 0; _roundEffects.clear(); _petKilledThisRound.fill(false); _needsFrontPetSwap.fill(false); @@ -427,7 +455,7 @@ namespace PetBattles std::swap(firstTeam, secondTeam); } - TC_LOG_ERROR("server.loading", "PetBattle ProcessRound: BEFORE TURNS - Team0 pet[{}] HP={}/{} Team1 pet[{}] HP={}/{}", + TC_LOG_DEBUG("server.loading", "PetBattle ProcessRound: BEFORE TURNS - Team0 pet[{}] HP={}/{} Team1 pet[{}] HP={}/{}", _teams[0].FrontPetIndex, _teams[0].Pets[_teams[0].FrontPetIndex].Health, _teams[0].Pets[_teams[0].FrontPetIndex].MaxHealth, _teams[1].FrontPetIndex, _teams[1].Pets[_teams[1].FrontPetIndex].Health, _teams[1].Pets[_teams[1].FrontPetIndex].MaxHealth); @@ -436,7 +464,7 @@ namespace PetBattles if (!IsFinished()) ProcessTurnForTeam(secondTeam); - TC_LOG_ERROR("server.loading", "PetBattle ProcessRound: AFTER TURNS - Team0 pet[{}] HP={}/{} Team1 pet[{}] HP={}/{} effects={}", + TC_LOG_DEBUG("server.loading", "PetBattle ProcessRound: AFTER TURNS - Team0 pet[{}] HP={}/{} Team1 pet[{}] HP={}/{} effects={}", _teams[0].FrontPetIndex, _teams[0].Pets[_teams[0].FrontPetIndex].Health, _teams[0].Pets[_teams[0].FrontPetIndex].MaxHealth, _teams[1].FrontPetIndex, _teams[1].Pets[_teams[1].FrontPetIndex].Health, _teams[1].Pets[_teams[1].FrontPetIndex].MaxHealth, _roundEffects.size()); @@ -492,7 +520,7 @@ namespace PetBattles { if (!_teams[i].Pets[_teams[i].FrontPetIndex].IsAlive()) { - TC_LOG_ERROR("server.loading", "PetBattle: Team {} front pet {} died (HP={}), needs swap", + TC_LOG_DEBUG("server.loading", "PetBattle: Team {} front pet {} died (HP={}), needs swap", i, _teams[i].FrontPetIndex, _teams[i].Pets[_teams[i].FrontPetIndex].Health); _needsFrontPetSwap[i] = true; @@ -503,7 +531,7 @@ namespace PetBattles int8 nextAlive = _teams[i].GetFirstAlivePetIndex(); if (nextAlive >= 0) { - TC_LOG_ERROR("server.loading", "PetBattle: Wild/NPC team auto-swap {} -> {}", _teams[i].FrontPetIndex, nextAlive); + TC_LOG_DEBUG("server.loading", "PetBattle: Wild/NPC team auto-swap {} -> {}", _teams[i].FrontPetIndex, nextAlive); _teams[i].FrontPetIndex = nextAlive; _needsFrontPetSwap[i] = false; @@ -525,7 +553,7 @@ namespace PetBattles if (anyPlayerNeedsSwap) { - TC_LOG_ERROR("server.loading", "PetBattle: Setting state WAITING_FOR_FRONT_PET, player needs to pick replacement"); + TC_LOG_DEBUG("server.loading", "PetBattle: Setting state WAITING_FOR_FRONT_PET, player needs to pick replacement"); _state = PET_BATTLE_STATE_WAITING_FOR_FRONT_PET; return; } @@ -549,7 +577,6 @@ namespace PetBattles void PetBattle::ProcessTurnForTeam(uint8 teamIdx) { PetBattleTeamData& team = _teams[teamIdx]; - uint8 opponentTeam = teamIdx == PET_BATTLE_TEAM_1 ? PET_BATTLE_TEAM_2 : PET_BATTLE_TEAM_1; PetBattlePetData& activePet = team.Pets[team.FrontPetIndex]; // Stunned pets skip their turn @@ -575,7 +602,7 @@ namespace PetBattles return; } - TC_LOG_ERROR("server.loading", "PetBattle ProcessTurnForTeam[{}]: MoveType={} AbilityID={} FrontPet={} PetAlive={}", + TC_LOG_DEBUG("server.loading", "PetBattle ProcessTurnForTeam[{}]: MoveType={} AbilityID={} FrontPet={} PetAlive={}", teamIdx, int(team.PendingMoveType), team.PendingAbilityID, team.FrontPetIndex, activePet.IsAlive()); switch (team.PendingMoveType) @@ -596,7 +623,7 @@ namespace PetBattles if (activePet.AbilityCooldowns[i] > 0) { abilityOnCooldown = true; - TC_LOG_ERROR("server.loading", "PetBattle: Ability {} on cooldown ({})", team.PendingAbilityID, activePet.AbilityCooldowns[i]); + TC_LOG_DEBUG("server.loading", "PetBattle: Ability {} on cooldown ({})", team.PendingAbilityID, activePet.AbilityCooldowns[i]); break; } @@ -619,7 +646,7 @@ namespace PetBattles // Apply ability effects through the DB2 chain uint32 effectsBefore = _roundEffects.size(); ApplyAbilityEffects(teamIdx, team.FrontPetIndex, team.PendingAbilityID); - TC_LOG_ERROR("server.loading", "PetBattle: ApplyAbilityEffects({}) generated {} effects", + TC_LOG_DEBUG("server.loading", "PetBattle: ApplyAbilityEffects({}) generated {} effects", team.PendingAbilityID, _roundEffects.size() - effectsBefore); break; } @@ -706,7 +733,7 @@ namespace PetBattles int8 nextAlive = wildTeam.GetFirstAlivePetIndex(); if (nextAlive >= 0) { - TC_LOG_ERROR("server.loading", "PetBattle: Captured wild pet[{}], swapping to next alive wild pet[{}]", + TC_LOG_DEBUG("server.loading", "PetBattle: Captured wild pet[{}], swapping to next alive wild pet[{}]", wildTeam.FrontPetIndex, nextAlive); wildTeam.FrontPetIndex = nextAlive; @@ -769,7 +796,7 @@ namespace PetBattles // Get ability turns from DB2 index std::vector const* turns = sPetBattleMgr->GetAbilityTurnsFull(abilityID); - TC_LOG_ERROR("server.loading", "PetBattle ApplyAbilityEffects: abilityID={} turns={}", abilityID, turns ? turns->size() : 0); + TC_LOG_DEBUG("server.loading", "PetBattle ApplyAbilityEffects: abilityID={} turns={}", abilityID, turns ? turns->size() : 0); if (!turns || turns->empty()) { // Fallback: apply simple damage if no DB2 turn data exists @@ -855,7 +882,7 @@ namespace PetBattles // Get effects for this turn from DB2 index std::vector const* effects = sPetBattleMgr->GetTurnEffectsFull(turn->ID); - TC_LOG_ERROR("server.loading", "PetBattle: TurnID={} turnIndex={} effects={}", turn->ID, turnIndex, effects ? effects->size() : 0); + TC_LOG_DEBUG("server.loading", "PetBattle: TurnID={} turnIndex={} effects={}", turn->ID, turnIndex, effects ? effects->size() : 0); if (!effects || effects->empty()) { // Multi-turn turn with no DB2 effects ? emit a STATUS_CHANGE so the client @@ -876,7 +903,7 @@ namespace PetBattles // Process each effect in order for (BattlePetAbilityEffectEntry const* effectEntry : *effects) { - TC_LOG_ERROR("server.loading", "PetBattle: ProcessEffect effectID={} propsID={} Params=[{},{},{},{},{},{}]", + TC_LOG_DEBUG("server.loading", "PetBattle: ProcessEffect effectID={} propsID={} Params=[{},{},{},{},{},{}]", effectEntry->ID, effectEntry->BattlePetEffectPropertiesID, effectEntry->Param[0], effectEntry->Param[1], effectEntry->Param[2], effectEntry->Param[3], effectEntry->Param[4], effectEntry->Param[5]); @@ -931,10 +958,10 @@ namespace PetBattles // We map the DB2 BattlePetEffectPropertiesID to our action types // In practice, the ID maps to specific effect behaviors BattlePetEffectPropertiesEntry const* effectProps = sBattlePetEffectPropertiesStore.LookupEntry(effectPropsID); - TC_LOG_ERROR("server.loading", "PetBattle ProcessEffect: propsID={} found={} basePower={} accuracy={}", + TC_LOG_DEBUG("server.loading", "PetBattle ProcessEffect: propsID={} found={} basePower={} accuracy={}", effectPropsID, effectProps != nullptr, basePower, accuracy); if (effectProps) - TC_LOG_ERROR("server.loading", "PetBattle ProcessEffect: effectCategory(ParamTypeEnum[0])={}", effectProps->ParamTypeEnum[0]); + TC_LOG_DEBUG("server.loading", "PetBattle ProcessEffect: effectCategory(ParamTypeEnum[0])={}", effectProps->ParamTypeEnum[0]); if (!effectProps) { // If no properties entry, treat as simple damage with basePower @@ -1405,7 +1432,7 @@ namespace PetBattles break; } default: - TC_LOG_ERROR("server.loading", "PetBattle ProcessEffect: UNHANDLED effectCategory={} basePower={} defenderAlive={}", + TC_LOG_WARN("server.loading", "PetBattle ProcessEffect: UNHANDLED effectCategory={} basePower={} defenderAlive={}", effectCategory, basePower, defender.IsAlive()); // Unhandled effect category - treat as damage if basePower > 0 if (basePower > 0 && defender.IsAlive()) @@ -1957,12 +1984,6 @@ namespace PetBattles // Speed resolution // ============================================================================ - void PetBattle::ResolveSpeed() - { - // Speed already factored into ProcessRound() turn ordering - // This method is kept for compatibility - } - // ============================================================================ // Death checking // ============================================================================ @@ -2317,7 +2338,7 @@ namespace PetBattles if (availableAbilities.empty()) { - TC_LOG_ERROR("server.loading", "PetBattle GenerateWildTeamInput: NO available abilities! AbilityIDs=[{},{},{}] CDs=[{},{},{}] -> PASS", + TC_LOG_DEBUG("server.loading", "PetBattle GenerateWildTeamInput: NO available abilities! AbilityIDs=[{},{},{}] CDs=[{},{},{}] -> PASS", frontPet.AbilityIDs[0], frontPet.AbilityIDs[1], frontPet.AbilityIDs[2], frontPet.AbilityCooldowns[0], frontPet.AbilityCooldowns[1], frontPet.AbilityCooldowns[2]); SubmitInput(PET_BATTLE_TEAM_2, PET_BATTLE_MOVE_PASS, 0, -1); @@ -2368,7 +2389,7 @@ namespace PetBattles cumulative += std::max(0.1f, opt.priority); if (roll <= cumulative) { - TC_LOG_ERROR("server.loading", "PetBattle GenerateWildTeamInput: SELECTED ability={} (avail={} roll={:.1f}/{:.1f})", + TC_LOG_DEBUG("server.loading", "PetBattle GenerateWildTeamInput: SELECTED ability={} (avail={} roll={:.1f}/{:.1f})", opt.abilityID, availableAbilities.size(), roll, totalWeight); SubmitInput(PET_BATTLE_TEAM_2, PET_BATTLE_MOVE_ABILITY, opt.abilityID, -1); return; @@ -2376,7 +2397,7 @@ namespace PetBattles } // Fallback: use first available ability - TC_LOG_ERROR("server.loading", "PetBattle GenerateWildTeamInput: FALLBACK ability={}", availableAbilities[0].abilityID); + TC_LOG_DEBUG("server.loading", "PetBattle GenerateWildTeamInput: FALLBACK ability={}", availableAbilities[0].abilityID); SubmitInput(PET_BATTLE_TEAM_2, PET_BATTLE_MOVE_ABILITY, availableAbilities[0].abilityID, -1); } diff --git a/src/server/game/BattlePets/PetBattle.h b/src/server/game/BattlePets/PetBattle.h index f68422dde..cdb3dd166 100644 --- a/src/server/game/BattlePets/PetBattle.h +++ b/src/server/game/BattlePets/PetBattle.h @@ -308,7 +308,6 @@ namespace PetBattles void ApplyPassiveOnDamageDealt(uint8 attackerTeam, uint8 attackerPet, uint8 defenderTeam, uint8 defenderPet, int32 damage); bool ApplyPassiveOnDeath(uint8 teamIdx, uint8 petIdx); - void ResolveSpeed(); void ProcessTurnForTeam(uint8 teamIdx); void CheckDeaths(); void AwardExperience(); @@ -327,6 +326,7 @@ namespace PetBattles uint8 _winnerTeam = 0; bool _canAwardXP = true; bool _maxLengthWarningSent = false; + uint32 _roundTimerSecs = 0; // seconds since last input was accepted uint32 _nextAuraInstanceID = 1; std::array _teams; diff --git a/src/server/game/Handlers/BattlePetHandler.cpp b/src/server/game/Handlers/BattlePetHandler.cpp index c9cdff74f..6753526fc 100644 --- a/src/server/game/Handlers/BattlePetHandler.cpp +++ b/src/server/game/Handlers/BattlePetHandler.cpp @@ -475,6 +475,15 @@ void WorldSession::HandlePetBattleRequestWild(WorldPackets::BattlePet::PetBattle return; } + // Distance check ? player must be reasonably close to the creature + if (!player->IsWithinDistInMap(creature, 50.0f)) + { + WorldPackets::BattlePet::PetBattleRequestFailed failed; + failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_TARGET_OUT_OF_RANGE; + SendPacket(failed.Write()); + return; + } + // Validate that the creature has a valid BattlePetSpecies entry (Species=0 crashes client) if (!BattlePets::BattlePetMgr::GetBattlePetSpeciesByCreature(creature->GetEntry())) { @@ -539,24 +548,24 @@ void WorldSession::HandlePetBattleRequestWild(WorldPackets::BattlePet::PetBattle for (uint8 t = 0; t < 2; ++t) { auto const& pu = initialUpdate.Players[t]; - TC_LOG_ERROR("server.loading", "PetBattle InitialUpdate Player[{}]: CharID={} TrapAbilityID={} TrapStatus={} RoundTimeSecs={} FrontPet={} InputFlags={} PetCount={}", + TC_LOG_DEBUG("server.loading", "PetBattle InitialUpdate Player[{}]: CharID={} TrapAbilityID={} TrapStatus={} RoundTimeSecs={} FrontPet={} InputFlags={} PetCount={}", t, pu.CharacterID.ToString(), pu.TrapAbilityID, pu.TrapStatus, pu.RoundTimeSecs, pu.FrontPet, pu.InputFlags, pu.Pets.size()); for (uint8 p = 0; p < pu.Pets.size(); ++p) { auto const& pet = pu.Pets[p]; - TC_LOG_ERROR("server.loading", " Pet[{}]: GUID={} Species={} Display={} Collar={} Lvl={} Xp={} HP={}/{} Pow={} Spd={} NpcTM={} Quality={} Status={} Slot={} Abilities={} Auras={} States={} Name='{}'", + TC_LOG_DEBUG("server.loading", " Pet[{}]: GUID={} Species={} Display={} Collar={} Lvl={} Xp={} HP={}/{} Pow={} Spd={} NpcTM={} Quality={} Status={} Slot={} Abilities={} Auras={} States={} Name='{}'", p, pet.BattlePetGUID.ToString(), pet.SpeciesID, pet.DisplayID, pet.CollarID, pet.Level, pet.Xp, pet.CurHealth, pet.MaxHealth, pet.Power, pet.Speed, pet.NpcTeamMemberID, pet.BreedQuality, pet.StatusFlags, pet.Slot, pet.Abilities.size(), pet.Auras.size(), pet.States.size(), pet.CustomName); for (uint8 a = 0; a < pet.Abilities.size(); ++a) - TC_LOG_ERROR("server.loading", " Ability[{}]: ID={} CD={} LD={} Idx={} Pboid={}", + TC_LOG_DEBUG("server.loading", " Ability[{}]: ID={} CD={} LD={} Idx={} Pboid={}", a, pet.Abilities[a].AbilityID, pet.Abilities[a].CooldownRemaining, pet.Abilities[a].LockdownRemaining, pet.Abilities[a].AbilityIndex, pet.Abilities[a].Pboid); for (uint8 s = 0; s < pet.States.size(); ++s) - TC_LOG_ERROR("server.loading", " State[{}]: ID={} Val={}", + TC_LOG_DEBUG("server.loading", " State[{}]: ID={} Val={}", s, pet.States[s].StateID, pet.States[s].StateValue); } } - TC_LOG_ERROR("server.loading", "PetBattle InitialUpdate: CurRound={} State={} NpcCreature={} NpcDisplay={} WildGUID={} IsPVP={} CanAwardXP={} PktSize={}", + TC_LOG_DEBUG("server.loading", "PetBattle InitialUpdate: CurRound={} State={} NpcCreature={} NpcDisplay={} WildGUID={} IsPVP={} CanAwardXP={} PktSize={}", initialUpdate.CurRound, initialUpdate.CurPetBattleState, initialUpdate.NpcCreatureID, initialUpdate.NpcDisplayID, initialUpdate.InitialWildPetGUID.ToString(), initialUpdate.IsPVP, initialUpdate.CanAwardXP, pkt->size()); @@ -729,8 +738,12 @@ void WorldSession::HandlePetBattleInput(WorldPackets::BattlePet::PetBattleInput& if (!battle) return; - // Client sends MoveType=FinalRoundOk while waiting for FinalNotify ? ignore it - if (battle->IsFinalRound()) + // Only accept input during active round states + if (battle->IsFinalRound() || battle->IsFinished()) + return; + + // Validate MoveType range + if (petBattleInput.MoveType < 0 || petBattleInput.MoveType > static_cast(PetBattles::PET_BATTLE_MOVE_PASS)) return; // Determine which team this player is @@ -738,6 +751,10 @@ void WorldSession::HandlePetBattleInput(WorldPackets::BattlePet::PetBattleInput& if (battle->GetTeam(PetBattles::PET_BATTLE_TEAM_2).PlayerGUID == player->GetGUID()) teamIdx = PetBattles::PET_BATTLE_TEAM_2; + // Don't accept duplicate input for the same round + if (battle->GetTeam(teamIdx).HasInputThisRound) + return; + battle->SubmitInput(teamIdx, PetBattles::PetBattleMoveType(petBattleInput.MoveType), petBattleInput.AbilityID, @@ -767,7 +784,7 @@ void WorldSession::HandlePetBattleInput(WorldPackets::BattlePet::PetBattleInput& BuildRoundCooldowns(roundResult.Cooldowns, battle); BuildPetXDied(roundResult.PetXDied, battle); - TC_LOG_ERROR("server.loading", "PetBattle ROUND_RESULT: Round={} State={} FinalRound={} Effects={} Cooldowns={} Deaths={} P0Flags={} P1Flags={}", + TC_LOG_DEBUG("server.loading", "PetBattle ROUND_RESULT: Round={} State={} FinalRound={} Effects={} Cooldowns={} Deaths={} P0Flags={} P1Flags={}", roundResult.CurRound, roundResult.NextPetBattleState, battle->IsFinalRound(), roundResult.Effects.size(), roundResult.Cooldowns.size(), roundResult.PetXDied.size(), @@ -777,7 +794,7 @@ void WorldSession::HandlePetBattleInput(WorldPackets::BattlePet::PetBattleInput& for (std::size_t e = 0; e < roundResult.Effects.size(); ++e) { auto const& eff = roundResult.Effects[e]; - TC_LOG_ERROR("server.loading", " Effect[{}]: AbilEffID={} Flags=0x{:X} Idx={} CasterPBOID={} StackDepth={} Targets={}", + TC_LOG_DEBUG("server.loading", " Effect[{}]: AbilEffID={} Flags=0x{:X} Idx={} CasterPBOID={} StackDepth={} Targets={}", e, eff.AbilityEffectID, eff.Flags, eff.EffectIndex, eff.CasterPBOID, eff.StackDepth, eff.Targets.size()); for (std::size_t tgt = 0; tgt < eff.Targets.size(); ++tgt) { @@ -785,7 +802,7 @@ void WorldSession::HandlePetBattleInput(WorldPackets::BattlePet::PetBattleInput& std::string paramStr; for (int32 p : t.Params) paramStr += std::to_string(p) + " "; - TC_LOG_ERROR("server.loading", " Target[{}]: Type={} Remaining(PBOID)={} Params=[{}]", + TC_LOG_DEBUG("server.loading", " Target[{}]: Type={} Remaining(PBOID)={} Params=[{}]", tgt, t.Type, t.Remaining, paramStr); } } @@ -811,6 +828,10 @@ void WorldSession::HandlePetBattleReplaceFrontPet(WorldPackets::BattlePet::PetBa if (!battle) return; + // Only allow replacement during WAITING_FOR_FRONT_PET or ROUND_IN_PROGRESS (voluntary swap) + if (battle->IsFinalRound() || battle->IsFinished()) + return; + uint8 teamIdx = PetBattles::PET_BATTLE_TEAM_1; if (battle->GetTeam(PetBattles::PET_BATTLE_TEAM_2).PlayerGUID == player->GetGUID()) teamIdx = PetBattles::PET_BATTLE_TEAM_2; @@ -819,22 +840,22 @@ void WorldSession::HandlePetBattleReplaceFrontPet(WorldPackets::BattlePet::PetBa int8 newPetIdx = petBattleReplaceFrontPet.FrontPetIndex; PetBattles::PetBattleTeamData& team = battle->GetTeam(teamIdx); - TC_LOG_ERROR("server.loading", "PetBattle ReplaceFrontPet: team={} newPetIdx={} petCount={} battleState={} needsSwap={}", + TC_LOG_DEBUG("server.loading", "PetBattle ReplaceFrontPet: team={} newPetIdx={} petCount={} battleState={} needsSwap={}", teamIdx, newPetIdx, team.PetCount, int(battle->GetBattleState()), battle->NeedsFrontPetSwap(teamIdx)); if (newPetIdx < 0 || newPetIdx >= team.PetCount) { - TC_LOG_ERROR("server.loading", "PetBattle ReplaceFrontPet: REJECTED - invalid index"); + TC_LOG_DEBUG("server.loading", "PetBattle ReplaceFrontPet: REJECTED - invalid index"); return; } if (!team.Pets[newPetIdx].IsAlive()) { - TC_LOG_ERROR("server.loading", "PetBattle ReplaceFrontPet: REJECTED - pet {} is dead (HP={})", + TC_LOG_DEBUG("server.loading", "PetBattle ReplaceFrontPet: REJECTED - pet {} is dead (HP={})", newPetIdx, team.Pets[newPetIdx].Health); return; } - TC_LOG_ERROR("server.loading", "PetBattle ReplaceFrontPet: Swapping team {} front pet {} -> {}", + TC_LOG_DEBUG("server.loading", "PetBattle ReplaceFrontPet: Swapping team {} front pet {} -> {}", teamIdx, team.FrontPetIndex, newPetIdx); // Set the new front pet and transition state back to ROUND_IN_PROGRESS @@ -1030,6 +1051,32 @@ void WorldSession::HandleJoinPetBattleQueue(WorldPackets::BattlePet::JoinPetBatt return; } + // Validate that player has at least one alive pet in battle slots + { + BattlePets::BattlePetMgr* petMgr = GetBattlePetMgr(); + bool hasPet = false; + for (uint8 i = 0; i < uint8(BattlePets::BattlePetSlot::Count); ++i) + { + WorldPackets::BattlePet::BattlePetSlot* slot = petMgr->GetSlot(BattlePets::BattlePetSlot(i)); + if (slot && !slot->Locked && !slot->Pet.Guid.IsEmpty()) + { + BattlePets::BattlePet* pet = petMgr->GetPet(slot->Pet.Guid); + if (pet && pet->PacketInfo.Health > 0) + { + hasPet = true; + break; + } + } + } + if (!hasPet) + { + WorldPackets::BattlePet::PetBattleQueueStatus status; + status.Status = 0; + SendPacket(status.Write()); + return; + } + } + sPetBattleMgr->JoinQueue(player->GetGUID()); WorldPackets::BattlePet::PetBattleQueueStatus status; diff --git a/src/server/game/Server/Packets/BattlePetPackets.h b/src/server/game/Server/Packets/BattlePetPackets.h index c6af9b4e5..16a7fb397 100644 --- a/src/server/game/Server/Packets/BattlePetPackets.h +++ b/src/server/game/Server/Packets/BattlePetPackets.h @@ -679,6 +679,10 @@ namespace WorldPackets WorldPacket const* Write() override { return &_worldPacket; } }; + // TODO: SMSG_PET_BATTLE_CHAT_RESTRICTED should be sent when a player attempts to use + // restricted chat channels (e.g., General, Trade) while in an active pet battle. + // The check should be added to the chat message handler (ChatHandler.cpp) when the + // player's PetBattle state is active. class PetBattleChatRestricted final : public ServerPacket { public: diff --git a/src/server/game/Server/Protocol/Opcodes.cpp b/src/server/game/Server/Protocol/Opcodes.cpp index 9d5c1469c..1c3b83b3d 100644 --- a/src/server/game/Server/Protocol/Opcodes.cpp +++ b/src/server/game/Server/Protocol/Opcodes.cpp @@ -238,7 +238,7 @@ void OpcodeTable::InitializeClientOpcodes() DEFINE_HANDLER(CMSG_BATTLE_PET_REQUEST_JOURNAL_LOCK, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleBattlePetRequestJournalLock); DEFINE_HANDLER(CMSG_BATTLE_PET_SET_BATTLE_SLOT, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleBattlePetSetBattleSlot); DEFINE_HANDLER(CMSG_BATTLE_PET_SET_FLAGS, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleBattlePetSetFlags); - DEFINE_HANDLER(CMSG_BATTLE_PET_SUMMON, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleBattlePetSummon); + DEFINE_HANDLER(CMSG_BATTLE_PET_SUMMON, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleBattlePetSummon); DEFINE_HANDLER(CMSG_BATTLE_PET_UPDATE_DISPLAY_NOTIFY, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleBattlePetUpdateDisplayNotify); DEFINE_HANDLER(CMSG_BATTLE_PET_UPDATE_NOTIFY, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleBattlePetUpdateNotify); DEFINE_HANDLER(CMSG_BEGIN_TRADE, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleBeginTradeOpcode);