Housing: Deep-dive initiative improvements from sniff/IDA analysis

Sniff analysis (223 packets across 12 .pkt files):
- Confirmed SMSG_INITIATIVE_SERVICE_STATUS wire format (1 byte, 0x80)
- Activity log responses contain packed GUIDs with double contribution scores
- Player initiative info uses float scale (ProgressRequired=1000.0f)
- Task progress is neighborhood-wide aggregate (confirmed by sniff values)

IDA binary analysis findings implemented:
- Add 8 new enums: NeighborhoodInitiativeUpdateStatus (Started/Milestone/
  Completed/Failed), ChestResult, TaskType (Single/RepeatableFinite/
  RepeatableInfinite), CompletionState, InitiativeFlags, MilestoneFlags,
  RewardFlags, NeighborhoodType
- Add 5 new SMSG opcodes: INITIATIVE_UPDATE_STATUS (1 byte status),
  INITIATIVE_POINTS_UPDATE (2 uint32: current/max), INITIATIVE_MILESTONE_UPDATE
  (3 bytes: index/reached/flags), INITIATIVE_CHEST_RESULT (1 uint32 result),
  INITIATIVE_TRACKED_UPDATED (packed GUID)
- Send InitiativeUpdateStatus on initiative start/complete/expire/milestone
- Send InitiativePointsUpdate after every task progress change
- Send InitiativeMilestoneUpdate when milestones are reached

Weighted cycle selection (IDA-verified):
- Build InitiativeCyclePriority index map during initialization
- SelectWeightedCycle() uses InitiativeCyclePriority.Weight for weighted
  random selection among candidate cycles
- CheckAndStartInitiatives() now uses weighted selection with fallback
  to equal-weight when no priority data exists

DB2 data fixes:
- Fix InitiativeReward.RewardData SQL column type from text to bigint
- Fix reward data values from empty string to 0
- Document that RewardType=0 with RewardAmount = favor points granted
  via Housing::AddFavor (HouseInitiativeFavor = AccountTransType 66)
This commit is contained in:
luis
2026-03-10 19:49:39 -03:00
parent 8b80a9f7ad
commit 1d444ee8f7
7 changed files with 742 additions and 374 deletions
File diff suppressed because it is too large Load Diff
+209 -8
View File
@@ -30,6 +30,7 @@
#include "NeighborhoodMgr.h"
#include "ObjectAccessor.h"
#include "Player.h"
#include "Random.h"
#include "WorldSession.h"
InitiativeManager& InitiativeManager::Instance()
@@ -130,8 +131,17 @@ void InitiativeManager::BuildDB2IndexMaps()
}
}
TC_LOG_DEBUG("housing", "InitiativeManager::BuildDB2IndexMaps: {} initiatives with tasks, {} cycles with milestones, {} initiative->cycle mappings",
uint32(_initiativeTasks.size()), uint32(_cycleMilestones.size()), uint32(_initiativeActiveCycle.size()));
// Build CycleID -> priority weights map for weighted selection
_cyclePriorities.clear();
for (InitiativeCyclePriorityEntry const* priority : sInitiativeCyclePriorityStore)
{
if (!priority)
continue;
_cyclePriorities[priority->InitiativeCycleID].emplace_back(priority->ID, priority->Weight);
}
TC_LOG_DEBUG("housing", "InitiativeManager::BuildDB2IndexMaps: {} initiatives with tasks, {} cycles with milestones, {} initiative->cycle mappings, {} cycle priorities",
uint32(_initiativeTasks.size()), uint32(_cycleMilestones.size()), uint32(_initiativeActiveCycle.size()), uint32(_cyclePriorities.size()));
}
void InitiativeManager::LoadFromDB()
@@ -296,6 +306,12 @@ void InitiativeManager::Update(uint32 diff)
initiative->InitiativeID, initiative->NeighborhoodGuid);
initiative->Completed = true;
PersistInitiative(*initiative);
// Broadcast failed status (expired without completion)
ObjectGuid nhObjGuid = ObjectGuid::Create<HighGuid::Housing>(4, 0, 0, initiative->NeighborhoodGuid);
Neighborhood* neighborhood = sNeighborhoodMgr.GetNeighborhood(nhObjGuid);
if (neighborhood)
SendInitiativeUpdateStatus(neighborhood, NI_UPDATE_STATUS_FAILED);
}
}
}
@@ -389,6 +405,17 @@ ActiveInitiative* InitiativeManager::StartInitiative(uint64 neighborhoodGuid, ui
ActiveInitiative* ptr = initiative.get();
_activeInitiatives[neighborhoodGuid].push_back(std::move(initiative));
// Broadcast initiative started status to neighborhood members
ObjectGuid nhObjGuid = ObjectGuid::Create<HighGuid::Housing>(4, 0, 0, neighborhoodGuid);
Neighborhood* neighborhood = sNeighborhoodMgr.GetNeighborhood(nhObjGuid);
if (neighborhood)
{
SendInitiativeUpdateStatus(neighborhood, NI_UPDATE_STATUS_STARTED);
uint32 maxPoints = CalculateMaxPoints(initiativeID);
SendInitiativePointsUpdate(neighborhood, 0, maxPoints);
}
return ptr;
}
@@ -443,7 +470,12 @@ void InitiativeManager::CompleteInitiative(uint64 neighborhoodGuid, uint32 initi
ObjectGuid nhObjGuid = ObjectGuid::Create<HighGuid::Housing>(4, 0, 0, neighborhoodGuid);
Neighborhood* neighborhood = sNeighborhoodMgr.GetNeighborhood(nhObjGuid);
if (neighborhood)
{
BroadcastInitiativeComplete(neighborhood, initiativeID);
SendInitiativeUpdateStatus(neighborhood, NI_UPDATE_STATUS_COMPLETED);
uint32 maxPoints = CalculateMaxPoints(initiativeID);
SendInitiativePointsUpdate(neighborhood, maxPoints, maxPoints);
}
TC_LOG_INFO("housing", "InitiativeManager::CompleteInitiative: Initiative {} completed in neighborhood {}",
initiativeID, neighborhoodGuid);
@@ -542,9 +574,19 @@ void InitiativeManager::UpdateTaskProgress(uint64 neighborhoodGuid, uint32 initi
initiative->Progress = static_cast<float>(completedTasks) / static_cast<float>(allTasks.size());
}
// Check milestones
// Send points update to neighborhood
ObjectGuid nhObjGuid = ObjectGuid::Create<HighGuid::Housing>(4, 0, 0, neighborhoodGuid);
Neighborhood* neighborhood = sNeighborhoodMgr.GetNeighborhood(nhObjGuid);
// Calculate current aggregate points: sum of all task progress values
uint32 currentPoints = 0;
for (auto const& [tid, tp] : initiative->TaskProgress)
currentPoints += tp.Progress;
uint32 maxPoints = CalculateMaxPoints(initiativeID);
if (neighborhood)
SendInitiativePointsUpdate(neighborhood, currentPoints, maxPoints);
// Check milestones
CheckMilestones(*initiative, neighborhood);
// Check if all tasks completed -> initiative complete
@@ -909,14 +951,16 @@ void InitiativeManager::CheckAndStartInitiatives()
if (active)
continue; // Already has an active initiative
// Pick the first available initiative from DB2
// In a full implementation, this would use InitiativeCycle and priority weighting
// Select initiative using weighted cycle priority (IDA-verified: server uses
// InitiativeCyclePriority.Weight for weighted random selection).
// Build candidate list excluding recently completed initiatives.
std::vector<std::pair<uint32, int32>> candidates; // initiativeID, weight
for (NeighborhoodInitiativeEntry const* entry : sNeighborhoodInitiativeStore)
{
if (!entry)
continue;
// Check if this initiative was already completed recently
// Skip if this initiative was already completed recently
bool recentlyCompleted = false;
auto itr = _activeInitiatives.find(nhGuid);
if (itr != _activeInitiatives.end())
@@ -933,10 +977,43 @@ void InitiativeManager::CheckAndStartInitiatives()
if (!recentlyCompleted)
{
StartInitiative(nhGuid, entry->ID);
break;
// Look up cycle priority weight for this initiative's active cycle
uint32 cycleID = SelectWeightedCycle(entry->ID);
int32 weight = 1; // default equal weight
auto prioItr = _cyclePriorities.find(cycleID);
if (prioItr != _cyclePriorities.end() && !prioItr->second.empty())
weight = std::max<int32>(1, prioItr->second[0].second);
candidates.emplace_back(entry->ID, weight);
}
}
if (!candidates.empty())
{
uint32 selectedID = candidates[0].first;
if (candidates.size() > 1)
{
// Weighted random selection
int32 totalWeight = 0;
for (auto const& [id, w] : candidates)
totalWeight += w;
int32 roll = irand(1, totalWeight);
int32 cumulative = 0;
for (auto const& [id, w] : candidates)
{
cumulative += w;
if (roll <= cumulative)
{
selectedID = id;
break;
}
}
}
StartInitiative(nhGuid, selectedID);
}
}
}
@@ -1173,10 +1250,134 @@ void InitiativeManager::CheckMilestones(ActiveInitiative& initiative, Neighborho
PersistMilestoneReached(initiative.DbId, milestone.MilestoneIndex, static_cast<uint32>(GameTime::GetGameTime()));
if (neighborhood)
{
BroadcastRewardAvailable(neighborhood, initiative.InitiativeID, milestone.MilestoneIndex);
SendInitiativeUpdateStatus(neighborhood, NI_UPDATE_STATUS_MILESTONE_COMPLETED);
SendInitiativeMilestoneUpdate(neighborhood, static_cast<uint8>(milestone.MilestoneIndex), true,
static_cast<uint8>(milestone.Flags));
}
TC_LOG_INFO("housing", "InitiativeManager: Milestone {} reached for initiative {} (progress={:.2f}, required={:.2f})",
milestone.MilestoneIndex, initiative.InitiativeID, initiative.Progress, milestone.ProgressRequired);
}
}
}
// ============================================================
// Weighted cycle selection (IDA-verified: server uses InitiativeCyclePriority.Weight)
// ============================================================
uint32 InitiativeManager::SelectWeightedCycle(uint32 initiativeID) const
{
// Collect all cycles for this initiative
std::vector<std::pair<uint32, int32>> candidateCycles; // cycleID, weight
for (InitiativeCycleEntry const* cycle : sInitiativeCycleStore)
{
if (!cycle || cycle->InitiativeID != static_cast<int32>(initiativeID))
continue;
// Look up priority weight for this cycle
int32 weight = 1; // default weight
auto prioItr = _cyclePriorities.find(cycle->ID);
if (prioItr != _cyclePriorities.end() && !prioItr->second.empty())
weight = std::max<int32>(1, prioItr->second[0].second);
candidateCycles.emplace_back(cycle->ID, weight);
}
if (candidateCycles.empty())
return GetActiveCycleForInitiative(initiativeID); // fallback to lowest CycleIndex
if (candidateCycles.size() == 1)
return candidateCycles[0].first;
// Weighted random selection
int32 totalWeight = 0;
for (auto const& [cid, w] : candidateCycles)
totalWeight += w;
int32 roll = irand(1, totalWeight);
int32 cumulative = 0;
for (auto const& [cid, w] : candidateCycles)
{
cumulative += w;
if (roll <= cumulative)
return cid;
}
return candidateCycles.back().first;
}
uint32 InitiativeManager::CalculateMaxPoints(uint32 initiativeID) const
{
// Max points = sum of all task TargetCounts (sniff-verified: ProgressRequired=1000.0f scale)
uint32 maxPoints = 0;
auto const& tasks = GetTasksForInitiative(initiativeID);
for (auto const& task : tasks)
maxPoints += static_cast<uint32>(std::max<int32>(1, task.TargetCount));
return maxPoints;
}
// ============================================================
// IDA-verified status/points update packet sending
// ============================================================
void InitiativeManager::SendInitiativeUpdateStatus(Neighborhood* neighborhood, NeighborhoodInitiativeUpdateStatus status) const
{
if (!neighborhood)
return;
WorldPackets::Housing::InitiativeUpdateStatus packet;
packet.Status = static_cast<uint8>(status);
WorldPacket const* data = packet.Write();
for (auto const& member : neighborhood->GetMembers())
{
if (Player* player = ObjectAccessor::FindPlayer(member.PlayerGuid))
{
if (player->GetSession())
player->GetSession()->SendPacket(data);
}
}
}
void InitiativeManager::SendInitiativePointsUpdate(Neighborhood* neighborhood, uint32 currentPoints, uint32 maxPoints) const
{
if (!neighborhood)
return;
WorldPackets::Housing::InitiativePointsUpdate packet;
packet.CurrentPoints = currentPoints;
packet.MaxPoints = maxPoints;
WorldPacket const* data = packet.Write();
for (auto const& member : neighborhood->GetMembers())
{
if (Player* player = ObjectAccessor::FindPlayer(member.PlayerGuid))
{
if (player->GetSession())
player->GetSession()->SendPacket(data);
}
}
}
void InitiativeManager::SendInitiativeMilestoneUpdate(Neighborhood* neighborhood, uint8 milestoneIndex, bool reached, uint8 flags) const
{
if (!neighborhood)
return;
WorldPackets::Housing::InitiativeMilestoneUpdate packet;
packet.MilestoneIndex = milestoneIndex;
packet.Reached = reached ? 1 : 0;
packet.Flags = flags;
WorldPacket const* data = packet.Write();
for (auto const& member : neighborhood->GetMembers())
{
if (Player* player = ObjectAccessor::FindPlayer(member.PlayerGuid))
{
if (player->GetSession())
player->GetSession()->SendPacket(data);
}
}
}
@@ -19,6 +19,7 @@
#define TRINITYCORE_INITIATIVE_MANAGER_H
#include "Define.h"
#include "HousingDefines.h"
#include "ObjectGuid.h"
#include <memory>
#include <set>
@@ -145,6 +146,11 @@ public:
// Auto-start initiatives for neighborhoods that don't have one
void CheckAndStartInitiatives();
// Send IDA-verified status/points update packets
void SendInitiativeUpdateStatus(Neighborhood* neighborhood, NeighborhoodInitiativeUpdateStatus status) const;
void SendInitiativePointsUpdate(Neighborhood* neighborhood, uint32 currentPoints, uint32 maxPoints) const;
void SendInitiativeMilestoneUpdate(Neighborhood* neighborhood, uint8 milestoneIndex, bool reached, uint8 flags) const;
private:
InitiativeManager() = default;
@@ -156,6 +162,8 @@ private:
void PersistContribution(uint64 initiativeDbId, uint64 playerGuid, uint32 taskId, uint32 amount);
void CheckMilestones(ActiveInitiative& initiative, Neighborhood* neighborhood);
void GrantMilestoneRewards(Player* player, uint32 milestoneID);
uint32 SelectWeightedCycle(uint32 initiativeID) const;
uint32 CalculateMaxPoints(uint32 initiativeID) const;
// Active initiatives: neighborhoodGuid -> list of active initiatives
std::unordered_map<uint64, std::vector<std::unique_ptr<ActiveInitiative>>> _activeInitiatives;
@@ -167,6 +175,8 @@ private:
std::unordered_map<uint32, std::vector<InitiativeMilestoneData>> _cycleMilestones;
// InitiativeID -> active cycle ID
std::unordered_map<uint32, uint32> _initiativeActiveCycle;
// CycleID -> list of priority entries (for weighted selection)
std::unordered_map<uint32, std::vector<std::pair<uint32, int32>>> _cyclePriorities; // cycleID -> [(initiativeID, weight)]
// Update timer
uint32 _updateTimer = 0;
@@ -2082,6 +2082,39 @@ namespace WorldPackets::Housing
return &_worldPacket;
}
WorldPacket const* InitiativeUpdateStatus::Write()
{
_worldPacket << uint8(Status);
return &_worldPacket;
}
WorldPacket const* InitiativePointsUpdate::Write()
{
_worldPacket << uint32(CurrentPoints);
_worldPacket << uint32(MaxPoints);
return &_worldPacket;
}
WorldPacket const* InitiativeMilestoneUpdate::Write()
{
_worldPacket << uint8(MilestoneIndex);
_worldPacket << uint8(Reached);
_worldPacket << uint8(Flags);
return &_worldPacket;
}
WorldPacket const* InitiativeChestResult::Write()
{
_worldPacket << uint32(Result);
return &_worldPacket;
}
WorldPacket const* InitiativeTrackedUpdated::Write()
{
_worldPacket << NeighborhoodGUID;
return &_worldPacket;
}
WorldPacket const* HousingPhotoSharingAuthorizationResult::Write()
{
_worldPacket << uint8(Result);
@@ -2152,6 +2152,57 @@ namespace WorldPackets::Housing
uint32 MilestoneIndex = 0;
};
// IDA-verified: SMSG_INITIATIVE_UPDATE_STATUS carries 1 byte (NeighborhoodInitiativeUpdateStatus)
// Sent when initiative state changes: Started, MilestoneCompleted, Completed, Failed
class InitiativeUpdateStatus final : public ServerPacket
{
public:
InitiativeUpdateStatus() : ServerPacket(SMSG_INITIATIVE_UPDATE_STATUS) {}
WorldPacket const* Write() override;
uint8 Status = 0; // NeighborhoodInitiativeUpdateStatus
};
// IDA-verified: SMSG_INITIATIVE_POINTS_UPDATE carries 2 uint32 (current, max)
// Sent after progress changes to update the client's progress bar
class InitiativePointsUpdate final : public ServerPacket
{
public:
InitiativePointsUpdate() : ServerPacket(SMSG_INITIATIVE_POINTS_UPDATE) {}
WorldPacket const* Write() override;
uint32 CurrentPoints = 0;
uint32 MaxPoints = 0;
};
// IDA-verified: SMSG_INITIATIVE_MILESTONE_UPDATE carries 3 bytes
// Sent when milestone state changes (milestoneIndex, reached, flags)
class InitiativeMilestoneUpdate final : public ServerPacket
{
public:
InitiativeMilestoneUpdate() : ServerPacket(SMSG_INITIATIVE_MILESTONE_UPDATE) {}
WorldPacket const* Write() override;
uint8 MilestoneIndex = 0;
uint8 Reached = 0;
uint8 Flags = 0;
};
// IDA-verified: SMSG_INITIATIVE_CHEST_RESULT carries 1 uint32 (NeighborhoodInitiativeChestResult)
class InitiativeChestResult final : public ServerPacket
{
public:
InitiativeChestResult() : ServerPacket(SMSG_INITIATIVE_CHEST_RESULT) {}
WorldPacket const* Write() override;
uint32 Result = 0; // NeighborhoodInitiativeChestResult
};
// IDA-verified: SMSG_INITIATIVE_TRACKED_UPDATED carries a packed GUID (8 bytes)
class InitiativeTrackedUpdated final : public ServerPacket
{
public:
InitiativeTrackedUpdated() : ServerPacket(SMSG_INITIATIVE_TRACKED_UPDATED) {}
WorldPacket const* Write() override;
ObjectGuid NeighborhoodGUID;
};
// ============================================================
// Photo Sharing SMSG Responses (0x42037x)
// ============================================================
@@ -1853,10 +1853,15 @@ void OpcodeTable::InitializeServerOpcodes()
DEFINE_SERVER_OPCODE_HANDLER(SMSG_HOUSING_UPDATE_HOUSE_INFO, STATUS_NEVER, CONNECTION_TYPE_REALM);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_INITIALIZE_FACTIONS, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_INITIAL_SETUP, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_INITIATIVE_CHEST_RESULT, STATUS_NEVER, CONNECTION_TYPE_REALM);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_INITIATIVE_COMPLETE, STATUS_NEVER, CONNECTION_TYPE_REALM);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_INITIATIVE_MILESTONE_UPDATE, STATUS_NEVER, CONNECTION_TYPE_REALM);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_INITIATIVE_POINTS_UPDATE, STATUS_NEVER, CONNECTION_TYPE_REALM);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_INITIATIVE_REWARD_AVAILABLE, STATUS_NEVER, CONNECTION_TYPE_REALM);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_INITIATIVE_SERVICE_STATUS, STATUS_NEVER, CONNECTION_TYPE_REALM);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_INITIATIVE_TASK_COMPLETE, STATUS_NEVER, CONNECTION_TYPE_REALM);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_INITIATIVE_TRACKED_UPDATED, STATUS_NEVER, CONNECTION_TYPE_REALM);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_INITIATIVE_UPDATE_STATUS, STATUS_NEVER, CONNECTION_TYPE_REALM);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_INIT_WORLD_STATES, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_INSPECT_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_INSTANCE_ABANDON_VOTE_COMPLETED, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE);
@@ -1776,10 +1776,15 @@ enum OpcodeServer : uint32
SMSG_HOUSING_SVC_REQUEST_PLAYER_RELOAD_DATA = 0x540020,
SMSG_INITIALIZE_FACTIONS = 0x4201CC,
SMSG_INITIAL_SETUP = 0x420014,
SMSG_INITIATIVE_CHEST_RESULT = 0x420369,
SMSG_INITIATIVE_COMPLETE = 0x420363,
SMSG_INITIATIVE_MILESTONE_UPDATE = 0x42036A,
SMSG_INITIATIVE_POINTS_UPDATE = 0x42036B,
SMSG_INITIATIVE_REWARD_AVAILABLE = 0x420368,
SMSG_INITIATIVE_SERVICE_STATUS = 0x420361,
SMSG_INITIATIVE_TASK_COMPLETE = 0x420362,
SMSG_INITIATIVE_TRACKED_UPDATED = 0x42036C,
SMSG_INITIATIVE_UPDATE_STATUS = 0x42036D,
SMSG_INIT_WORLD_STATES = 0x4201EE,
SMSG_INSPECT_RESULT = 0x4200D9,
SMSG_INSTANCE_ABANDON_VOTE_COMPLETED = 0x42009D,