Garrison: Fix 9 discrepancies found via WoD sniff analysis (12.0.1.66102)

P0: Fix mission rewards double-write in GetGarrisonInfoResult — inline
    Encounters/Rewards/OvermaxRewards are now cleared in mission structs
    sent via GarrisonInfo; rewards go only in the garrison-level parallel
    arrays, preventing client packet desync.

P1: Populate FollowerSoftCaps with 5 entries matching retail sniff data.
    Implement SMSG_GARRISON_MISSION_START_CONDITION_UPDATE packet and send
    it after GetGarrisonInfoResult. Update CMSG_GARRISON_START_MISSION
    Read() for 12.0.1 per-follower struct format (BoardIndex/Health/
    HasFollowerEntry), remove obsolete MissionBonusAbilityIDs.

P2: Implement SMSG_GARRISON_FOLLOWER_CHANGED_QUALITY packet, used by
    SetFollowerQuality. Send blueprint data for all garrison types.
    Read GarrTypeID in CMSG_GARRISON_CHECK_UPGRADEABLE and look up the
    correct garrison by type.

P3: Add FOLLOWER_TYPE_DELVES (22) enum value. Fix building removal
    packet ordering — GarrisonBuildingRemoved now sent before
    ClearBuildingInfo (PlotRemoved) when replacing a different type.
This commit is contained in:
luis
2026-02-27 20:31:25 -03:00
parent 13e2635433
commit d7a5e52d71
5 changed files with 133 additions and 24 deletions
+54 -13
View File
@@ -689,7 +689,18 @@ void Garrison::PlaceBuilding(uint32 garrPlotInstanceId, uint32 garrBuildingId)
{
oldBuildingId = plot->BuildingInfo.PacketInfo->GarrBuildingID;
if (sGarrBuildingStore.AssertEntry(oldBuildingId)->BuildingType != building->BuildingType)
{
// Send BuildingRemoved BEFORE ClearBuildingInfo (which sends PlotRemoved)
// so the client processes removal in the correct order
WorldPackets::Garrison::GarrisonBuildingRemoved buildingRemoved;
buildingRemoved.GarrTypeID = GetType();
buildingRemoved.Result = GARRISON_SUCCESS;
buildingRemoved.GarrPlotInstanceID = garrPlotInstanceId;
buildingRemoved.GarrBuildingID = oldBuildingId;
_owner->SendDirectMessage(buildingRemoved.Write());
plot->ClearBuildingInfo(GetType(), _owner);
}
}
plot->SetBuildingInfo(placeBuildingResult.BuildingInfo, _owner);
@@ -702,12 +713,17 @@ void Garrison::PlaceBuilding(uint32 garrPlotInstanceId, uint32 garrBuildingId)
if (oldBuildingId)
{
WorldPackets::Garrison::GarrisonBuildingRemoved buildingRemoved;
buildingRemoved.GarrTypeID = GetType();
buildingRemoved.Result = GARRISON_SUCCESS;
buildingRemoved.GarrPlotInstanceID = garrPlotInstanceId;
buildingRemoved.GarrBuildingID = oldBuildingId;
_owner->SendDirectMessage(buildingRemoved.Write());
GarrBuildingEntry const* oldBuilding = sGarrBuildingStore.AssertEntry(oldBuildingId);
// Same-type upgrade: BuildingRemoved wasn't sent above, send it now
if (oldBuilding->BuildingType == building->BuildingType)
{
WorldPackets::Garrison::GarrisonBuildingRemoved buildingRemoved;
buildingRemoved.GarrTypeID = GetType();
buildingRemoved.Result = GARRISON_SUCCESS;
buildingRemoved.GarrPlotInstanceID = garrPlotInstanceId;
buildingRemoved.GarrBuildingID = oldBuildingId;
_owner->SendDirectMessage(buildingRemoved.Write());
}
}
_owner->UpdateCriteria(CriteriaType::PlaceGarrisonBuilding, garrBuildingId);
@@ -1028,11 +1044,22 @@ void Garrison::BuildInfoPacket(WorldPackets::Garrison::GarrisonInfo& garrison) c
for (auto const& p : _followers)
garrison.Followers.push_back(&p.second.PacketInfo);
// Missions: inline Encounters/Rewards/OvermaxRewards must be empty in the
// GarrisonInfo mission structs ? the rewards go ONLY in the garrison-level
// parallel arrays. The client reads both, so writing them in both places
// causes a packet desync (double data). We build temporary copies with
// the inline vectors cleared.
_infoMissions.clear();
_infoMissions.reserve(_missions.size());
for (auto const& p : _missions)
{
garrison.Missions.push_back(&p.second.PacketInfo);
garrison.MissionRewards.push_back(p.second.PacketInfo.Rewards);
garrison.MissionOvermaxRewards.push_back(p.second.PacketInfo.OvermaxRewards);
auto& copy = _infoMissions.emplace_back(p.second.PacketInfo);
garrison.MissionRewards.push_back(copy.Rewards);
garrison.MissionOvermaxRewards.push_back(copy.OvermaxRewards);
copy.Encounters.clear();
copy.Rewards.clear();
copy.OvermaxRewards.clear();
garrison.Missions.push_back(&copy);
garrison.CanStartMission.push_back(p.second.PacketInfo.MissionState == 0);
}
@@ -1097,6 +1124,21 @@ void Garrison::SendMapData(Player* receiver) const
receiver->SendDirectMessage(mapData.Write());
}
void Garrison::SendMissionStartConditionUpdate() const
{
WorldPackets::Garrison::GarrisonMissionStartConditionUpdate update;
update.MissionRecIDs.reserve(_missions.size());
update.CanStartMission.reserve(_missions.size());
for (auto const& [dbId, mission] : _missions)
{
update.MissionRecIDs.push_back(mission.PacketInfo.MissionRecID);
update.CanStartMission.push_back(mission.PacketInfo.MissionState == 0);
}
_owner->SendDirectMessage(update.Write());
}
// ============================================================
// Follower management
// ============================================================
@@ -2441,10 +2483,9 @@ void Garrison::SetFollowerQuality(uint64 dbId, uint32 quality)
quality, GetFaction(), false);
}
WorldPackets::Garrison::GarrisonUpdateFollower updateFollower;
updateFollower.Result = GARRISON_SUCCESS;
updateFollower.Follower = follower->PacketInfo;
_owner->SendDirectMessage(updateFollower.Write());
WorldPackets::Garrison::GarrisonFollowerChangedQuality changedQuality;
changedQuality.Follower = follower->PacketInfo;
_owner->SendDirectMessage(changedQuality.Write());
}
void Garrison::SetFollowerLevel(uint64 dbId, uint32 level)
+5
View File
@@ -63,6 +63,7 @@ enum GarrisonFollowerType
FOLLOWER_TYPE_SHIPYARD = 2,
FOLLOWER_TYPE_CLASS_ORDER = 4,
FOLLOWER_TYPE_WAR_CAMPAIGN = 11,
FOLLOWER_TYPE_DELVES = 22,
FOLLOWER_TYPE_COVENANT = 123
};
@@ -386,6 +387,7 @@ public:
void SendRemoteInfo() const;
void SendBlueprintAndSpecializationData();
void SendMapData(Player* receiver) const;
void SendMissionStartConditionUpdate() const;
void ResetFollowerActivationLimit() { _followerActivationsRemainingToday = 1; }
@@ -423,6 +425,9 @@ private:
// Trophies
std::unordered_set<uint32 /*trophyID*/> _trophies;
// Temporary storage for BuildInfoPacket (mission copies with cleared inline rewards)
mutable std::vector<WorldPackets::Garrison::GarrisonMission> _infoMissions;
};
#endif // Garrison_h__
+15 -3
View File
@@ -36,7 +36,19 @@ void WorldSession::HandleGetGarrisonInfo(WorldPackets::Garrison::GetGarrisonInfo
for (auto const& [type, garrison] : _player->GetGarrisons())
garrison->BuildInfoPacket(garrisonInfo.Garrisons.emplace_back());
garrisonInfo.FollowerSoftCaps = {
{ FOLLOWER_TYPE_GARRISON, 20 },
{ FOLLOWER_TYPE_SHIPYARD, 6 },
{ FOLLOWER_TYPE_CLASS_ORDER, 6 },
{ FOLLOWER_TYPE_DELVES, 30 },
{ FOLLOWER_TYPE_COVENANT, 100 }
};
SendPacket(garrisonInfo.Write());
// Follow up with per-garrison mission start condition updates
for (auto const& [type, garrison] : _player->GetGarrisons())
garrison->SendMissionStartConditionUpdate();
}
void WorldSession::HandleGarrisonPurchaseBuilding(WorldPackets::Garrison::GarrisonPurchaseBuilding& garrisonPurchaseBuilding)
@@ -59,7 +71,7 @@ void WorldSession::HandleGarrisonCancelConstruction(WorldPackets::Garrison::Garr
void WorldSession::HandleGarrisonRequestBlueprintAndSpecializationData(WorldPackets::Garrison::GarrisonRequestBlueprintAndSpecializationData& /*garrisonRequestBlueprintAndSpecializationData*/)
{
if (Garrison* garrison = _player->GetGarrison())
for (auto const& [type, garrison] : _player->GetGarrisons())
garrison->SendBlueprintAndSpecializationData();
}
@@ -388,9 +400,9 @@ void WorldSession::HandleUpgradeGarrison(WorldPackets::Garrison::UpgradeGarrison
SendPacket(result.Write());
}
void WorldSession::HandleGarrisonCheckUpgradeable(WorldPackets::Garrison::GarrisonCheckUpgradeable& /*garrisonCheckUpgradeable*/)
void WorldSession::HandleGarrisonCheckUpgradeable(WorldPackets::Garrison::GarrisonCheckUpgradeable& garrisonCheckUpgradeable)
{
Garrison* garrison = _player->GetGarrison();
Garrison* garrison = _player->GetGarrison(static_cast<GarrisonType>(garrisonCheckUpgradeable.GarrTypeID));
GarrisonError upgradeResult = GARRISON_ERROR_UPGRADE_CONDITION_FAILED;
if (garrison)
@@ -492,18 +492,17 @@ namespace WorldPackets::Garrison
{
_worldPacket >> NpcGUID;
uint32 followerCount = 0;
uint32 bonusAbilityCount = 0;
_worldPacket >> followerCount;
_worldPacket >> MissionRecID;
_worldPacket >> bonusAbilityCount;
FollowerDBIDs.resize(followerCount);
for (uint32 i = 0; i < followerCount; ++i)
{
_worldPacket >> FollowerDBIDs[i];
MissionBonusAbilityIDs.resize(bonusAbilityCount);
for (uint32 i = 0; i < bonusAbilityCount; ++i)
_worldPacket >> MissionBonusAbilityIDs[i];
_worldPacket.read_skip<int32>(); // BoardIndex (unused, always -1)
_worldPacket.read_skip<int32>(); // Health (unused, always 0)
_worldPacket.read_skip<uint8>(); // HasFollowerEntry (unused, always false)
}
}
void GarrisonCompleteMission::Read()
@@ -595,6 +594,24 @@ namespace WorldPackets::Garrison
return &_worldPacket;
}
WorldPacket const* GarrisonMissionStartConditionUpdate::Write()
{
ASSERT(MissionRecIDs.size() == CanStartMission.size());
_worldPacket << Size<uint32>(MissionRecIDs);
_worldPacket << Size<uint32>(CanStartMission);
if (!MissionRecIDs.empty())
_worldPacket.append(MissionRecIDs.data(), MissionRecIDs.size());
for (bool canStart : CanStartMission)
_worldPacket << Bits<1>(canStart);
_worldPacket.FlushBits();
return &_worldPacket;
}
WorldPacket const* GarrisonIsUpgradeableResponse::Write()
{
_worldPacket << uint32(Result);
@@ -739,6 +756,13 @@ namespace WorldPackets::Garrison
return &_worldPacket;
}
WorldPacket const* GarrisonFollowerChangedQuality::Write()
{
_worldPacket << Follower;
return &_worldPacket;
}
WorldPacket const* GarrisonUpdateFollower::Write()
{
_worldPacket << uint32(Result);
@@ -812,6 +836,11 @@ namespace WorldPackets::Garrison
_worldPacket >> NpcGUID;
}
void GarrisonCheckUpgradeable::Read()
{
_worldPacket >> GarrTypeID;
}
void GarrisonSetBuildingActive::Read()
{
_worldPacket >> PlotInstanceID;
@@ -452,7 +452,6 @@ namespace WorldPackets
ObjectGuid NpcGUID;
std::vector<uint64> FollowerDBIDs;
uint32 MissionRecID = 0;
std::vector<uint64> MissionBonusAbilityIDs;
};
class GarrisonCompleteMission final : public ClientPacket
@@ -557,6 +556,17 @@ namespace WorldPackets
uint8 GarrTypeID = 0;
};
class GarrisonMissionStartConditionUpdate final : public ServerPacket
{
public:
explicit GarrisonMissionStartConditionUpdate() : ServerPacket(SMSG_GARRISON_MISSION_START_CONDITION_UPDATE) {}
WorldPacket const* Write() override;
std::vector<int32> MissionRecIDs;
std::vector<bool> CanStartMission;
};
class GarrisonIsUpgradeableResponse final : public ServerPacket
{
public:
@@ -779,6 +789,16 @@ namespace WorldPackets
GarrisonFollower Follower;
};
class GarrisonFollowerChangedQuality final : public ServerPacket
{
public:
explicit GarrisonFollowerChangedQuality() : ServerPacket(SMSG_GARRISON_FOLLOWER_CHANGED_QUALITY) {}
WorldPacket const* Write() override;
GarrisonFollower Follower;
};
class GarrisonUpdateFollower final : public ServerPacket
{
public:
@@ -863,7 +883,9 @@ namespace WorldPackets
public:
explicit GarrisonCheckUpgradeable(WorldPacket&& packet) : ClientPacket(CMSG_GARRISON_CHECK_UPGRADEABLE, std::move(packet)) {}
void Read() override {}
void Read() override;
uint32 GarrTypeID = 0;
};
class GarrisonSetBuildingActive final : public ClientPacket