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:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user