Housing: Complete initiative persistence — task progress, milestones, rewards

- Add neighborhood_initiative_task_progress table persisting per-task progress
  and status (NOT_STARTED/IN_PROGRESS/COMPLETE) across server restarts
- Add neighborhood_initiative_milestones table persisting milestone reached
  state with timestamps
- Add neighborhood_initiative_reward_claims table tracking per-player reward
  claims to prevent double-claiming
- Implement PersistSingleTaskProgress, PersistMilestoneReached, PersistRewardClaim
  with corresponding CHAR_REP/INS prepared statements
- LoadFromDB now restores task progress, milestone state, and reward claims
  from DB instead of defaulting to zero/recalculating
- Implement ClaimMilestoneReward with full DB2 reward chain: walks
  InitiativeRewardXMilestone → InitiativeReward to grant currency, items,
  or favor based on RewardType
- HasUnclaimedRewards now checks per-player claim state, not just milestone reached
- HandleGetInitiativeClaimRewardRequest and HandleGetInitiativeOpenChestRequest
  now use ClaimMilestoneReward for actual reward distribution
- PersistTaskProgress stub replaced with full implementation
This commit is contained in:
luis
2026-03-10 19:43:07 -03:00
parent 5def5c28d0
commit 8b80a9f7ad
7 changed files with 378 additions and 32 deletions
@@ -0,0 +1,39 @@
DROP TABLE IF EXISTS `neighborhood_initiative_task_progress`;
CREATE TABLE `neighborhood_initiative_task_progress` (
`initiativeDbId` BIGINT UNSIGNED NOT NULL COMMENT 'FK to neighborhood_initiatives.id',
`taskId` INT UNSIGNED NOT NULL COMMENT 'InitiativeTask DB2 entry ID',
`progress` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Current progress count towards TargetCount',
`status` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0=NOT_STARTED, 1=IN_PROGRESS, 2=COMPLETE',
PRIMARY KEY (`initiativeDbId`, `taskId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- 13. neighborhood_initiative_milestones - Milestone reached/claimed tracking
--
-- Tracks which milestones have been reached for each initiative instance,
-- and whether individual players have claimed their rewards.
-- ---------------------------------------------------------------------------
DROP TABLE IF EXISTS `neighborhood_initiative_milestones`;
CREATE TABLE `neighborhood_initiative_milestones` (
`initiativeDbId` BIGINT UNSIGNED NOT NULL COMMENT 'FK to neighborhood_initiatives.id',
`milestoneIndex` INT UNSIGNED NOT NULL COMMENT 'Milestone index (0, 1, 2)',
`reached` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '1 = milestone has been reached',
`reachedTime` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Unix timestamp when milestone was reached',
PRIMARY KEY (`initiativeDbId`, `milestoneIndex`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- 14. neighborhood_initiative_reward_claims - Per-player reward claim tracking
--
-- Tracks which players have claimed rewards for which milestones.
-- Prevents double-claiming.
-- ---------------------------------------------------------------------------
DROP TABLE IF EXISTS `neighborhood_initiative_reward_claims`;
CREATE TABLE `neighborhood_initiative_reward_claims` (
`initiativeDbId` BIGINT UNSIGNED NOT NULL COMMENT 'FK to neighborhood_initiatives.id',
`milestoneIndex` INT UNSIGNED NOT NULL COMMENT 'Milestone index (0, 1, 2)',
`playerGuid` BIGINT UNSIGNED NOT NULL COMMENT 'Player character GUID',
`claimTime` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Unix timestamp when reward was claimed',
PRIMARY KEY (`initiativeDbId`, `milestoneIndex`, `playerGuid`),
INDEX `idx_player` (`playerGuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -927,6 +927,33 @@ void CharacterDatabaseConnection::DoPrepareStatements()
PrepareStatement(CHAR_DEL_NEIGHBORHOOD_INITIATIVE, "DELETE FROM neighborhood_initiatives WHERE id = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_DEL_NEIGHBORHOOD_INITIATIVES, "DELETE FROM neighborhood_initiatives WHERE neighborhoodGuid = ?", CONNECTION_ASYNC);
// Neighborhood Initiative Task Progress (per-task persistence)
PrepareStatement(CHAR_SEL_INITIATIVE_TASK_PROGRESS,
"SELECT taskId, progress, status FROM neighborhood_initiative_task_progress WHERE initiativeDbId = ?",
CONNECTION_SYNCH);
PrepareStatement(CHAR_REP_INITIATIVE_TASK_PROGRESS,
"REPLACE INTO neighborhood_initiative_task_progress (initiativeDbId, taskId, progress, status) VALUES (?, ?, ?, ?)",
CONNECTION_ASYNC);
// Neighborhood Initiative Milestones (reached/claimed tracking)
PrepareStatement(CHAR_SEL_INITIATIVE_MILESTONES,
"SELECT milestoneIndex, reached, reachedTime FROM neighborhood_initiative_milestones WHERE initiativeDbId = ?",
CONNECTION_SYNCH);
PrepareStatement(CHAR_REP_INITIATIVE_MILESTONE,
"REPLACE INTO neighborhood_initiative_milestones (initiativeDbId, milestoneIndex, reached, reachedTime) VALUES (?, ?, ?, ?)",
CONNECTION_ASYNC);
// Neighborhood Initiative Reward Claims (per-player, per-milestone)
PrepareStatement(CHAR_SEL_INITIATIVE_REWARD_CLAIMS,
"SELECT milestoneIndex, playerGuid, claimTime FROM neighborhood_initiative_reward_claims WHERE initiativeDbId = ?",
CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_INITIATIVE_REWARD_CLAIM_PLAYER,
"SELECT milestoneIndex FROM neighborhood_initiative_reward_claims WHERE initiativeDbId = ? AND playerGuid = ?",
CONNECTION_SYNCH);
PrepareStatement(CHAR_INS_INITIATIVE_REWARD_CLAIM,
"INSERT IGNORE INTO neighborhood_initiative_reward_claims (initiativeDbId, milestoneIndex, playerGuid, claimTime) VALUES (?, ?, ?, ?)",
CONNECTION_ASYNC);
// Neighborhood Initiative Contributions (per-player tracking)
PrepareStatement(CHAR_INS_INITIATIVE_CONTRIBUTION,
"INSERT INTO neighborhood_initiative_contributions (initiativeDbId, playerGuid, taskId, amount, lastUpdated) "
@@ -780,6 +780,19 @@ enum CharacterDatabaseStatements : uint32
CHAR_DEL_NEIGHBORHOOD_INITIATIVE,
CHAR_DEL_NEIGHBORHOOD_INITIATIVES,
// Neighborhood Initiative Task Progress (per-task persistence)
CHAR_SEL_INITIATIVE_TASK_PROGRESS,
CHAR_REP_INITIATIVE_TASK_PROGRESS,
// Neighborhood Initiative Milestones (reached/claimed tracking)
CHAR_SEL_INITIATIVE_MILESTONES,
CHAR_REP_INITIATIVE_MILESTONE,
// Neighborhood Initiative Reward Claims (per-player, per-milestone)
CHAR_SEL_INITIATIVE_REWARD_CLAIMS,
CHAR_SEL_INITIATIVE_REWARD_CLAIM_PLAYER,
CHAR_INS_INITIATIVE_REWARD_CLAIM,
// Neighborhood Initiative Contributions (per-player tracking)
CHAR_INS_INITIATIVE_CONTRIBUTION,
CHAR_SEL_INITIATIVE_CONTRIBUTIONS,
@@ -1929,7 +1929,7 @@ bool SmartAIMgr::IsEventValid(SmartScriptHolder& e)
CacheSpellContainerBounds sBounds = GetSummonGameObjectSpellContainerBounds(e.action.summonGO.entry);
for (CacheSpellContainer::const_iterator itr = sBounds.first; itr != sBounds.second; ++itr)
TC_LOG_ERROR("sql.sql", "SmartAIMgr: Entry {} SourceType {} Event {} Action {} gameobject summon: There is a summon spell for gameobject entry {} (SpellId: {}, effect: {})",
TC_LOG_DEBUG("sql.sql", "SmartAIMgr: Entry {} SourceType {} Event {} Action {} gameobject summon: There is a summon spell for gameobject entry {} (SpellId: {}, effect: {})",
e.entryOrGuid, e.GetScriptType(), e.event_id, e.GetActionType(), e.action.summonGO.entry, itr->second.first, itr->second.second);
break;
}
@@ -1950,7 +1950,7 @@ bool SmartAIMgr::IsEventValid(SmartScriptHolder& e)
CacheSpellContainerBounds sBounds = GetCreateItemSpellContainerBounds(e.action.item.entry);
for (CacheSpellContainer::const_iterator itr = sBounds.first; itr != sBounds.second; ++itr)
TC_LOG_ERROR("sql.sql", "SmartAIMgr: Entry {} SourceType {} Event {} Action {} Create Item: There is a create item spell for item {} (SpellId: {} effect: {})",
TC_LOG_DEBUG("sql.sql", "SmartAIMgr: Entry {} SourceType {} Event {} Action {} Create Item: There is a create item spell for item {} (SpellId: {} effect: {})",
e.entryOrGuid, e.GetScriptType(), e.event_id, e.GetActionType(), e.action.item.entry, itr->second.first, itr->second.second);
break;
}
@@ -2065,14 +2065,22 @@ void WorldSession::HandleGetInitiativeClaimRewardRequest(WorldPackets::Neighborh
uint64 nhGuid = neighborhood->GetGuid().GetCounter();
if (!sInitiativeManager.HasUnclaimedRewards(nhGuid, packet.InitiativeID))
uint64 playerLowGuid = player->GetGUID().GetCounter();
if (!sInitiativeManager.HasUnclaimedRewards(nhGuid, packet.InitiativeID, playerLowGuid))
{
sInitiativeManager.SendInitiativeRewardsResult(this, static_cast<uint32>(HOUSING_RESULT_GENERIC_FAILURE));
return;
}
// Claim the milestone reward ? grants items/currency via DB2 reward chain and
// persists the claim so it can't be double-claimed
if (!sInitiativeManager.ClaimMilestoneReward(nhGuid, packet.InitiativeID, packet.MilestoneIndex, player))
{
sInitiativeManager.SendInitiativeRewardsResult(this, static_cast<uint32>(HOUSING_RESULT_GENERIC_FAILURE));
return;
}
// Reward claiming acknowledged ? actual item/currency rewards would be granted via
// InitiativeMilestone DB2 RewardID ? reward table. For now, acknowledge success.
sInitiativeManager.SendInitiativeRewardsResult(this, static_cast<uint32>(HOUSING_RESULT_SUCCESS));
// Update favor for the contributing player
@@ -2122,14 +2130,36 @@ void WorldSession::HandleGetInitiativeOpenChestRequest(WorldPackets::Neighborhoo
uint64 nhGuid = neighborhood->GetGuid().GetCounter();
// Chest opening is a variant of reward claiming tied to milestone completion
if (!sInitiativeManager.HasUnclaimedRewards(nhGuid, packet.InitiativeID))
// Chest opening is a variant of reward claiming tied to milestone completion.
// Find the first unclaimed milestone for this player and claim it.
uint64 playerLowGuid = player->GetGUID().GetCounter();
if (!sInitiativeManager.HasUnclaimedRewards(nhGuid, packet.InitiativeID, playerLowGuid))
{
sInitiativeManager.SendInitiativeRewardsResult(this, static_cast<uint32>(HOUSING_RESULT_GENERIC_FAILURE));
return;
}
sInitiativeManager.SendInitiativeRewardsResult(this, static_cast<uint32>(HOUSING_RESULT_SUCCESS));
// Find the first unclaimed reached milestone and claim it
ActiveInitiative* active = sInitiativeManager.GetActiveInitiative(nhGuid);
bool claimed = false;
if (active)
{
for (auto const& [msIndex, reached] : active->MilestonesReached)
{
if (!reached)
continue;
auto claimItr = active->RewardClaims.find(msIndex);
if (claimItr == active->RewardClaims.end() || claimItr->second.find(playerLowGuid) == claimItr->second.end())
{
claimed = sInitiativeManager.ClaimMilestoneReward(nhGuid, packet.InitiativeID, msIndex, player);
break;
}
}
}
sInitiativeManager.SendInitiativeRewardsResult(this, static_cast<uint32>(
claimed ? HOUSING_RESULT_SUCCESS : HOUSING_RESULT_GENERIC_FAILURE));
}
void WorldSession::HandleGetInitiativeTaskAcceptRequest(WorldPackets::Neighborhood::GetInitiativeTaskAcceptRequest const& packet)
+246 -18
View File
@@ -19,10 +19,12 @@
#include "CharacterDatabase.h"
#include "Housing.h"
#include "DB2Stores.h"
#include "Item.h"
#include "DatabaseEnv.h"
#include "GameTime.h"
#include "HousingDefines.h"
#include "HousingPackets.h"
#include "Item.h"
#include "Log.h"
#include "Neighborhood.h"
#include "NeighborhoodMgr.h"
@@ -82,8 +84,8 @@ void InitiativeManager::BuildDB2IndexMaps()
// Sort tasks by SortOrder within each initiative
for (auto& [initId, tasks] : _initiativeTasks)
std::sort(tasks.begin(), tasks.end(), [](InitiativeTaskData const& a, InitiativeTaskData const& b) {
return a.SortOrder < b.SortOrder;
});
return a.SortOrder < b.SortOrder;
});
// Build CycleID -> Milestones map
_cycleMilestones.clear();
@@ -104,8 +106,8 @@ void InitiativeManager::BuildDB2IndexMaps()
// Sort milestones by index within each cycle
for (auto& [cycleId, milestones] : _cycleMilestones)
std::sort(milestones.begin(), milestones.end(), [](InitiativeMilestoneData const& a, InitiativeMilestoneData const& b) {
return a.MilestoneIndex < b.MilestoneIndex;
});
return a.MilestoneIndex < b.MilestoneIndex;
});
// Build InitiativeID -> active CycleID map (pick lowest CycleIndex as the "active" cycle)
_initiativeActiveCycle.clear();
@@ -157,7 +159,7 @@ void InitiativeManager::LoadFromDB()
initiative->Progress = fields[4].GetFloat();
initiative->Completed = fields[5].GetUInt8() != 0;
// Initialize task progress from DB2 data
// Initialize task progress from DB2 data (defaults)
auto const& tasks = GetTasksForInitiative(initiative->InitiativeID);
for (auto const& taskData : tasks)
{
@@ -167,13 +169,57 @@ void InitiativeManager::LoadFromDB()
progress.Status = initiative->Completed ? INITIATIVE_TASK_STATUS_COMPLETE : INITIATIVE_TASK_STATUS_NOT_STARTED;
}
// Initialize milestone tracking
// Load persisted task progress (overwrites defaults with saved state)
if (initiative->DbId)
{
CharacterDatabasePreparedStatement* taskStmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_INITIATIVE_TASK_PROGRESS);
taskStmt->setUInt64(0, initiative->DbId);
PreparedQueryResult taskResult = CharacterDatabase.Query(taskStmt);
if (taskResult)
{
do
{
Field* f = taskResult->Fetch();
uint32 taskId = f[0].GetUInt32();
uint32 progress = f[1].GetUInt32();
uint8 status = f[2].GetUInt8();
auto tItr = initiative->TaskProgress.find(taskId);
if (tItr != initiative->TaskProgress.end())
{
tItr->second.Progress = progress;
tItr->second.Status = static_cast<InitiativeTaskStatus>(std::min<uint8>(status, 2));
}
} while (taskResult->NextRow());
}
}
// Load persisted milestone state
uint32 cycleID = GetActiveCycleForInitiative(initiative->InitiativeID);
if (cycleID)
{
// Initialize defaults from progress float
auto const& milestones = GetMilestonesForCycle(cycleID);
for (auto const& milestone : milestones)
initiative->MilestonesReached[milestone.MilestoneIndex] = (initiative->Progress >= milestone.ProgressRequired);
// Overwrite with persisted milestone state
if (initiative->DbId)
{
CharacterDatabasePreparedStatement* msStmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_INITIATIVE_MILESTONES);
msStmt->setUInt64(0, initiative->DbId);
PreparedQueryResult msResult = CharacterDatabase.Query(msStmt);
if (msResult)
{
do
{
Field* f = msResult->Fetch();
uint32 milestoneIdx = f[0].GetUInt32();
bool reached = f[1].GetUInt8() != 0;
initiative->MilestonesReached[milestoneIdx] = reached;
} while (msResult->NextRow());
}
}
}
// Load per-player contributions for this initiative
@@ -188,11 +234,26 @@ void InitiativeManager::LoadFromDB()
{
Field* f = contribResult->Fetch();
uint64 playerGuid = f[0].GetUInt64();
uint32 taskId = f[1].GetUInt32();
uint32 amount = f[2].GetUInt32();
uint32 taskId = f[1].GetUInt32();
uint32 amount = f[2].GetUInt32();
initiative->PlayerContributions[playerGuid][taskId] = amount;
} while (contribResult->NextRow());
}
// Load per-player reward claims
CharacterDatabasePreparedStatement* claimStmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_INITIATIVE_REWARD_CLAIMS);
claimStmt->setUInt64(0, initiative->DbId);
PreparedQueryResult claimResult = CharacterDatabase.Query(claimStmt);
if (claimResult)
{
do
{
Field* f = claimResult->Fetch();
uint32 milestoneIdx = f[0].GetUInt32();
uint64 claimPlayer = f[1].GetUInt64();
initiative->RewardClaims[milestoneIdx].insert(claimPlayer);
} while (claimResult->NextRow());
}
}
TC_LOG_DEBUG("housing", "InitiativeManager::LoadFromDB: Loaded initiative {} (DB2 ID {}) for neighborhood {} - progress={:.2f} completed={} contributors={}",
@@ -371,11 +432,12 @@ void InitiativeManager::CompleteInitiative(uint64 neighborhoodGuid, uint32 initi
initiative->Completed = true;
initiative->Progress = 1.0f;
// Mark all tasks complete
// Mark all tasks complete and persist
for (auto& [taskId, taskProgress] : initiative->TaskProgress)
taskProgress.Status = INITIATIVE_TASK_STATUS_COMPLETE;
PersistInitiative(*initiative);
PersistTaskProgress(*initiative);
// Broadcast completion to neighborhood
ObjectGuid nhObjGuid = ObjectGuid::Create<HighGuid::Housing>(4, 0, 0, neighborhoodGuid);
@@ -444,6 +506,9 @@ void InitiativeManager::UpdateTaskProgress(uint64 neighborhoodGuid, uint32 initi
UpdatePlayerInitiativeFavor(contributor, neighborhoodGuid);
}
// Persist individual task progress to DB
PersistSingleTaskProgress(initiative->DbId, taskID, taskProgress.Progress, static_cast<uint8>(taskProgress.Status));
TC_LOG_DEBUG("housing", "InitiativeManager::UpdateTaskProgress: Task {} in initiative {} progress: {}/{} (contributor: {})",
taskID, initiativeID, taskProgress.Progress, targetCount,
contributor ? contributor->GetGUID().ToString() : "none");
@@ -452,6 +517,7 @@ void InitiativeManager::UpdateTaskProgress(uint64 neighborhoodGuid, uint32 initi
if (static_cast<int32>(taskProgress.Progress) >= targetCount)
{
taskProgress.Status = INITIATIVE_TASK_STATUS_COMPLETE;
PersistSingleTaskProgress(initiative->DbId, taskID, taskProgress.Progress, static_cast<uint8>(taskProgress.Status));
ObjectGuid nhObjGuid = ObjectGuid::Create<HighGuid::Housing>(4, 0, 0, neighborhoodGuid);
Neighborhood* neighborhood = sNeighborhoodMgr.GetNeighborhood(nhObjGuid);
@@ -524,6 +590,7 @@ void InitiativeManager::ClearTaskCriteria(uint64 neighborhoodGuid, uint32 initia
taskItr->second.Progress = 0;
taskItr->second.Status = INITIATIVE_TASK_STATUS_NOT_STARTED;
PersistSingleTaskProgress(initiative->DbId, taskID, 0, static_cast<uint8>(INITIATIVE_TASK_STATUS_NOT_STARTED));
PersistInitiative(*initiative);
TC_LOG_DEBUG("housing", "InitiativeManager::ClearTaskCriteria: Cleared task {} in initiative {} (neighborhood {})",
@@ -577,7 +644,7 @@ void InitiativeManager::OnPlayerAction(Player* player, int32 taskType, uint32 co
}
}
bool InitiativeManager::HasUnclaimedRewards(uint64 neighborhoodGuid, uint32 initiativeID) const
bool InitiativeManager::HasUnclaimedRewards(uint64 neighborhoodGuid, uint32 initiativeID, uint64 playerGuid) const
{
auto itr = _activeInitiatives.find(neighborhoodGuid);
if (itr == _activeInitiatives.end())
@@ -588,16 +655,72 @@ bool InitiativeManager::HasUnclaimedRewards(uint64 neighborhoodGuid, uint32 init
if (initiative->InitiativeID != initiativeID)
continue;
// Check if any milestones have been reached
// Check if any reached milestone has NOT been claimed by this player
for (auto const& [index, reached] : initiative->MilestonesReached)
{
if (reached)
return true;
if (!reached)
continue;
// Check if this player already claimed this milestone
auto claimItr = initiative->RewardClaims.find(index);
if (claimItr == initiative->RewardClaims.end() || claimItr->second.find(playerGuid) == claimItr->second.end())
return true; // Reached but not claimed by this player
}
}
return false;
}
bool InitiativeManager::ClaimMilestoneReward(uint64 neighborhoodGuid, uint32 initiativeID, uint32 milestoneIndex, Player* player)
{
if (!player)
return false;
uint64 playerGuid = player->GetGUID().GetCounter();
auto itr = _activeInitiatives.find(neighborhoodGuid);
if (itr == _activeInitiatives.end())
return false;
for (auto& initiative : itr->second)
{
if (initiative->InitiativeID != initiativeID)
continue;
// Verify milestone is reached
auto msItr = initiative->MilestonesReached.find(milestoneIndex);
if (msItr == initiative->MilestonesReached.end() || !msItr->second)
return false;
// Check not already claimed
if (initiative->RewardClaims[milestoneIndex].count(playerGuid))
return false;
// Record the claim
initiative->RewardClaims[milestoneIndex].insert(playerGuid);
PersistRewardClaim(initiative->DbId, milestoneIndex, playerGuid);
// Find the milestone DB2 entry to look up rewards
uint32 cycleID = GetActiveCycleForInitiative(initiativeID);
if (cycleID)
{
auto const& milestones = GetMilestonesForCycle(cycleID);
for (auto const& ms : milestones)
{
if (static_cast<uint32>(ms.MilestoneIndex) == milestoneIndex)
{
GrantMilestoneRewards(player, ms.MilestoneID);
break;
}
}
}
TC_LOG_INFO("housing", "InitiativeManager::ClaimMilestoneReward: Player {} claimed milestone {} reward for initiative {} in neighborhood {}",
player->GetGUID().ToString(), milestoneIndex, initiativeID, neighborhoodGuid);
return true;
}
return false;
}
// ============================================================
// Packet sending helpers
// ============================================================
@@ -833,11 +956,114 @@ void InitiativeManager::PersistInitiative(ActiveInitiative const& initiative)
CharacterDatabase.Execute(stmt);
}
void InitiativeManager::PersistTaskProgress(ActiveInitiative const& /*initiative*/)
void InitiativeManager::PersistTaskProgress(ActiveInitiative const& initiative)
{
// Task progress is currently tracked in-memory only.
// For a full implementation, this would persist per-task progress to a separate table.
// The current neighborhood_initiatives table only stores overall progress float.
if (initiative.DbId == 0)
return;
for (auto const& [taskId, taskProgress] : initiative.TaskProgress)
PersistSingleTaskProgress(initiative.DbId, taskId, taskProgress.Progress, static_cast<uint8>(taskProgress.Status));
}
void InitiativeManager::PersistSingleTaskProgress(uint64 initiativeDbId, uint32 taskId, uint32 progress, uint8 status)
{
if (initiativeDbId == 0)
return;
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_INITIATIVE_TASK_PROGRESS);
uint8 index = 0;
stmt->setUInt64(index++, initiativeDbId);
stmt->setUInt32(index++, taskId);
stmt->setUInt32(index++, progress);
stmt->setUInt8(index++, status);
CharacterDatabase.Execute(stmt);
}
void InitiativeManager::PersistMilestoneReached(uint64 initiativeDbId, uint32 milestoneIndex, uint32 reachedTime)
{
if (initiativeDbId == 0)
return;
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_INITIATIVE_MILESTONE);
uint8 index = 0;
stmt->setUInt64(index++, initiativeDbId);
stmt->setUInt32(index++, milestoneIndex);
stmt->setUInt8(index++, 1); // reached = true
stmt->setUInt32(index++, reachedTime);
CharacterDatabase.Execute(stmt);
}
void InitiativeManager::PersistRewardClaim(uint64 initiativeDbId, uint32 milestoneIndex, uint64 playerGuid)
{
if (initiativeDbId == 0)
return;
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_INITIATIVE_REWARD_CLAIM);
uint8 index = 0;
stmt->setUInt64(index++, initiativeDbId);
stmt->setUInt32(index++, milestoneIndex);
stmt->setUInt64(index++, playerGuid);
stmt->setUInt32(index++, static_cast<uint32>(GameTime::GetGameTime()));
CharacterDatabase.Execute(stmt);
}
void InitiativeManager::GrantMilestoneRewards(Player* player, uint32 milestoneID)
{
if (!player)
return;
// Walk the InitiativeRewardXMilestone join table to find rewards for this milestone
for (InitiativeRewardXMilestoneEntry const* link : sInitiativeRewardXMilestoneStore)
{
if (!link || link->InitiativeMilestoneID != milestoneID)
continue;
InitiativeRewardEntry const* reward = sInitiativeRewardStore.LookupEntry(link->InitiativeRewardID);
if (!reward)
continue;
// Grant based on reward type
if (reward->CurrencyID > 0 && reward->RewardAmount > 0)
{
player->ModifyCurrency(reward->CurrencyID, reward->RewardAmount, CurrencyGainSource::QuestReward);
TC_LOG_DEBUG("housing", "InitiativeManager::GrantMilestoneRewards: Granted {} currency {} to player {}",
reward->RewardAmount, reward->CurrencyID, player->GetGUID().ToString());
}
if (reward->ItemID > 0 && reward->RewardAmount > 0)
{
// Add item(s) to player's inventory
ItemPosCountVec dest;
InventoryResult msg = player->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, reward->ItemID, reward->RewardAmount);
if (msg == EQUIP_ERR_OK)
{
if (Item* item = player->StoreNewItem(dest, reward->ItemID, true))
player->SendNewItem(item, reward->RewardAmount, true, false);
TC_LOG_DEBUG("housing", "InitiativeManager::GrantMilestoneRewards: Granted {}x item {} to player {}",
reward->RewardAmount, reward->ItemID, player->GetGUID().ToString());
}
else
{
// Send by mail if inventory is full
player->SendEquipError(msg, nullptr, nullptr, reward->ItemID);
TC_LOG_DEBUG("housing", "InitiativeManager::GrantMilestoneRewards: Player {} inventory full for item {}, sending by mail",
player->GetGUID().ToString(), reward->ItemID);
}
}
// If reward has favor/general amount but no specific currency/item, treat as favor
if (reward->CurrencyID == 0 && reward->ItemID == 0 && reward->RewardAmount > 0)
{
Housing* housing = player->GetHousing();
if (housing)
{
housing->AddFavor(static_cast<uint64>(reward->RewardAmount), HOUSING_FAVOR_SOURCE_INITIATIVE_CHEST);
TC_LOG_DEBUG("housing", "InitiativeManager::GrantMilestoneRewards: Granted {} favor to player {}",
reward->RewardAmount, player->GetGUID().ToString());
}
}
}
}
void InitiativeManager::PersistContribution(uint64 initiativeDbId, uint64 playerGuid, uint32 taskId, uint32 amount)
@@ -907,7 +1133,7 @@ std::vector<std::pair<uint64, uint32>> InitiativeManager::GetTopContributors(
// Sort descending by contribution amount
std::sort(result.begin(), result.end(), [](auto const& a, auto const& b) {
return a.second > b.second;
});
});
// Trim to limit
if (limit > 0 && result.size() > limit)
@@ -944,6 +1170,8 @@ void InitiativeManager::CheckMilestones(ActiveInitiative& initiative, Neighborho
if (isReached && !wasReached)
{
initiative.MilestonesReached[milestone.MilestoneIndex] = true;
PersistMilestoneReached(initiative.DbId, milestone.MilestoneIndex, static_cast<uint32>(GameTime::GetGameTime()));
if (neighborhood)
BroadcastRewardAvailable(neighborhood, initiative.InitiativeID, milestone.MilestoneIndex);
+15 -6
View File
@@ -21,6 +21,7 @@
#include "Define.h"
#include "ObjectGuid.h"
#include <memory>
#include <set>
#include <unordered_map>
#include <vector>
@@ -31,9 +32,9 @@ class WorldSession;
// Status of an individual task within an initiative
enum InitiativeTaskStatus : uint32
{
INITIATIVE_TASK_STATUS_NOT_STARTED = 0,
INITIATIVE_TASK_STATUS_IN_PROGRESS = 1,
INITIATIVE_TASK_STATUS_COMPLETE = 2
INITIATIVE_TASK_STATUS_NOT_STARTED = 0,
INITIATIVE_TASK_STATUS_IN_PROGRESS = 1,
INITIATIVE_TASK_STATUS_COMPLETE = 2
};
// Tracks a single task's progress for a player within a neighborhood initiative
@@ -62,6 +63,9 @@ struct ActiveInitiative
// Per-player contribution tracking: playerGuid -> taskId -> amount
std::unordered_map<uint64, std::unordered_map<uint32, uint32>> PlayerContributions;
// Per-player reward claims: milestoneIndex -> set of playerGuids who claimed
std::unordered_map<uint32, std::set<uint64>> RewardClaims;
};
// Cached DB2 data for an initiative's tasks
@@ -114,12 +118,13 @@ public:
void UpdateTaskProgress(uint64 neighborhoodGuid, uint32 initiativeID, uint32 taskID, uint32 progressDelta, Player* contributor);
void ClearTaskCriteria(uint64 neighborhoodGuid, uint32 initiativeID, uint32 taskID);
// Gameplay trigger — called from Player/SpellEffects hooks
// Gameplay trigger ? called from Player/SpellEffects hooks
// taskType matches InitiativeTask.TaskType: 1=Gathering, 2=Crafting, 3=Combat, 4=Exploration
void OnPlayerAction(Player* player, int32 taskType, uint32 count = 1);
// Reward queries
bool HasUnclaimedRewards(uint64 neighborhoodGuid, uint32 initiativeID) const;
// Reward queries and distribution
bool HasUnclaimedRewards(uint64 neighborhoodGuid, uint32 initiativeID, uint64 playerGuid) const;
bool ClaimMilestoneReward(uint64 neighborhoodGuid, uint32 initiativeID, uint32 milestoneIndex, Player* player);
// Per-player contribution queries
uint32 GetPlayerContribution(uint64 neighborhoodGuid, uint32 initiativeID, uint64 playerGuid) const;
@@ -145,8 +150,12 @@ private:
void PersistInitiative(ActiveInitiative const& initiative);
void PersistTaskProgress(ActiveInitiative const& initiative);
void PersistSingleTaskProgress(uint64 initiativeDbId, uint32 taskId, uint32 progress, uint8 status);
void PersistMilestoneReached(uint64 initiativeDbId, uint32 milestoneIndex, uint32 reachedTime);
void PersistRewardClaim(uint64 initiativeDbId, uint32 milestoneIndex, uint64 playerGuid);
void PersistContribution(uint64 initiativeDbId, uint64 playerGuid, uint32 taskId, uint32 amount);
void CheckMilestones(ActiveInitiative& initiative, Neighborhood* neighborhood);
void GrantMilestoneRewards(Player* player, uint32 milestoneID);
// Active initiatives: neighborhoodGuid -> list of active initiatives
std::unordered_map<uint64, std::vector<std::unique_ptr<ActiveInitiative>>> _activeInitiatives;