Core/ Archeology merge
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
--
|
||||
-- Archaeology: per-character active dig sites.
|
||||
-- Persists the ActivePlayer ResearchSites / ResearchSiteProgress update fields across relog/restart,
|
||||
-- plus the site's current hidden find position so a relog cannot relocate an in-progress find.
|
||||
-- Ported from evry/master-track/archaeology 94eadb810a (findX/findY added by cb60da9643).
|
||||
--
|
||||
DROP TABLE IF EXISTS `character_research_site`;
|
||||
CREATE TABLE `character_research_site` (
|
||||
`guid` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'Global Unique Identifier',
|
||||
`researchSiteId` smallint unsigned NOT NULL DEFAULT '0',
|
||||
`progress` int unsigned NOT NULL DEFAULT '0',
|
||||
`findX` float NOT NULL DEFAULT '0' COMMENT 'Current hidden find world X',
|
||||
`findY` float NOT NULL DEFAULT '0' COMMENT 'Current hidden find world Y',
|
||||
PRIMARY KEY (`guid`,`researchSiteId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Archaeology active dig sites per character';
|
||||
|
||||
DROP TABLE IF EXISTS `character_research_project`;
|
||||
CREATE TABLE `character_research_project` (
|
||||
`guid` bigint unsigned NOT NULL COMMENT 'Character GUID',
|
||||
`projectId` int unsigned NOT NULL COMMENT 'ResearchProject.db2 ID (current project for its branch)',
|
||||
PRIMARY KEY (`guid`,`projectId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Archaeology active research projects per character';
|
||||
|
||||
DROP TABLE IF EXISTS `character_research_history`;
|
||||
CREATE TABLE `character_research_history` (
|
||||
`guid` bigint unsigned NOT NULL COMMENT 'Character GUID',
|
||||
`projectId` int unsigned NOT NULL COMMENT 'ResearchProject.db2 ID',
|
||||
`firstCompleted` bigint NOT NULL DEFAULT '0' COMMENT 'Unix time of the first completion',
|
||||
`completionCount` int unsigned NOT NULL DEFAULT '1' COMMENT 'Times this project has been solved',
|
||||
PRIMARY KEY (`guid`,`projectId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Archaeology completed research projects per character';
|
||||
@@ -0,0 +1,72 @@
|
||||
--
|
||||
-- Archaeology research DB2 hotfix tables. feature/archaeology added the DB2 loaders +
|
||||
-- prepared statements (HotfixDatabase.cpp) but shipped no table SQL.
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `research_branch`;
|
||||
CREATE TABLE `research_branch` (
|
||||
`ID` int unsigned NOT NULL DEFAULT '0',
|
||||
`Name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
`ResearchFieldID` tinyint unsigned NOT NULL DEFAULT '0',
|
||||
`CurrencyID` smallint unsigned NOT NULL DEFAULT '0',
|
||||
`TextureFileID` int NOT NULL DEFAULT '0',
|
||||
`BigTextureFileID` int NOT NULL DEFAULT '0',
|
||||
`ItemID` int NOT NULL DEFAULT '0',
|
||||
`VerifiedBuild` int NOT NULL DEFAULT '0'
|
||||
, PRIMARY KEY (`ID`,`VerifiedBuild`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `research_project`;
|
||||
CREATE TABLE `research_project` (
|
||||
`ID` int unsigned NOT NULL DEFAULT '0',
|
||||
`Name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
`Description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
`Rarity` tinyint unsigned NOT NULL DEFAULT '0',
|
||||
`SpellID` int NOT NULL DEFAULT '0',
|
||||
`ResearchBranchID` smallint unsigned NOT NULL DEFAULT '0',
|
||||
`NumSockets` tinyint unsigned NOT NULL DEFAULT '0',
|
||||
`TextureFileID` int NOT NULL DEFAULT '0',
|
||||
`RequiredWeight` int unsigned NOT NULL DEFAULT '0',
|
||||
`VerifiedBuild` int NOT NULL DEFAULT '0'
|
||||
, PRIMARY KEY (`ID`,`VerifiedBuild`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `research_site`;
|
||||
CREATE TABLE `research_site` (
|
||||
`ID` int unsigned NOT NULL DEFAULT '0',
|
||||
`Name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
`MapID` smallint NOT NULL DEFAULT '0',
|
||||
`QuestPOIBlobID` int NOT NULL DEFAULT '0',
|
||||
`AreaPOIIconEnum` int unsigned NOT NULL DEFAULT '0',
|
||||
`VerifiedBuild` int NOT NULL DEFAULT '0'
|
||||
, PRIMARY KEY (`ID`,`VerifiedBuild`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `research_branch_locale`;
|
||||
CREATE TABLE `research_branch_locale` (
|
||||
`ID` int unsigned NOT NULL DEFAULT '0',
|
||||
`locale` varchar(4) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`Name_lang` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
`VerifiedBuild` int NOT NULL DEFAULT '0'
|
||||
, PRIMARY KEY (`ID`,`locale`,`VerifiedBuild`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `research_project_locale`;
|
||||
CREATE TABLE `research_project_locale` (
|
||||
`ID` int unsigned NOT NULL DEFAULT '0',
|
||||
`locale` varchar(4) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`Name_lang` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
`Description_lang` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
`VerifiedBuild` int NOT NULL DEFAULT '0'
|
||||
, PRIMARY KEY (`ID`,`locale`,`VerifiedBuild`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `research_site_locale`;
|
||||
CREATE TABLE `research_site_locale` (
|
||||
`ID` int unsigned NOT NULL DEFAULT '0',
|
||||
`locale` varchar(4) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`Name_lang` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
`VerifiedBuild` int NOT NULL DEFAULT '0'
|
||||
, PRIMARY KEY (`ID`,`locale`,`VerifiedBuild`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,142 @@
|
||||
--
|
||||
-- Archaeology: dig-site -> research branch mapping (Eastern Kingdoms + Kalimdor).
|
||||
-- Each dig site is themed to one race/branch. This is server-owned reference data that client DB2
|
||||
-- does not carry: sourced from the Archy addon dig-site DB (blobID = ResearchSite.QuestPOIBlobID),
|
||||
-- joined to ResearchBranch by fragment currency.
|
||||
--
|
||||
-- PROVISIONAL-FROM-FORK (evry/master-track/archaeology 0b474e639d): findCount = 6 for every site.
|
||||
-- Derived from a single Cataclysm-site survey observation, not a per-site retail table.
|
||||
--
|
||||
-- Ported from evry/master-track/archaeology 0b474e639d (amended by cb60da9643).
|
||||
--
|
||||
DROP TABLE IF EXISTS `archaeology_dig_site`;
|
||||
CREATE TABLE `archaeology_dig_site` (
|
||||
`researchSiteId` int unsigned NOT NULL DEFAULT '0' COMMENT 'ResearchSite.db2 ID',
|
||||
`researchBranchId` int unsigned NOT NULL DEFAULT '0' COMMENT 'ResearchBranch.db2 ID',
|
||||
`findCount` tinyint unsigned NOT NULL DEFAULT '6' COMMENT 'finds required to exhaust the site',
|
||||
PRIMARY KEY (`researchSiteId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Archaeology dig site to research branch (fork reference data)';
|
||||
DELETE FROM `archaeology_dig_site`;
|
||||
INSERT INTO `archaeology_dig_site` (`researchSiteId`,`researchBranchId`,`findCount`) VALUES
|
||||
(9,1,6),
|
||||
(10,1,6),
|
||||
(12,1,6),
|
||||
(13,3,6),
|
||||
(15,1,6),
|
||||
(18,8,6),
|
||||
(19,1,6),
|
||||
(20,1,6),
|
||||
(21,3,6),
|
||||
(22,1,6),
|
||||
(23,8,6),
|
||||
(24,8,6),
|
||||
(25,8,6),
|
||||
(26,8,6),
|
||||
(27,8,6),
|
||||
(144,1,6),
|
||||
(146,1,6),
|
||||
(150,1,6),
|
||||
(152,8,6),
|
||||
(154,3,6),
|
||||
(163,4,6),
|
||||
(165,3,6),
|
||||
(167,4,6),
|
||||
(169,4,6),
|
||||
(171,4,6),
|
||||
(173,4,6),
|
||||
(175,4,6),
|
||||
(177,4,6),
|
||||
(179,4,6),
|
||||
(181,3,6),
|
||||
(183,1,6),
|
||||
(185,4,6),
|
||||
(187,4,6),
|
||||
(189,3,6),
|
||||
(191,4,6),
|
||||
(193,4,6),
|
||||
(195,3,6),
|
||||
(197,4,6),
|
||||
(199,3,6),
|
||||
(201,4,6),
|
||||
(203,3,6),
|
||||
(205,3,6),
|
||||
(207,1,6),
|
||||
(209,1,6),
|
||||
(211,1,6),
|
||||
(213,1,6),
|
||||
(215,3,6),
|
||||
(217,8,6),
|
||||
(219,4,6),
|
||||
(221,3,6),
|
||||
(223,8,6),
|
||||
(225,8,6),
|
||||
(227,8,6),
|
||||
(229,8,6),
|
||||
(231,8,6),
|
||||
(233,8,6),
|
||||
(235,8,6),
|
||||
(237,3,6),
|
||||
(239,8,6),
|
||||
(241,8,6),
|
||||
(243,8,6),
|
||||
(245,8,6),
|
||||
(247,3,6),
|
||||
(249,3,6),
|
||||
(251,3,6),
|
||||
(259,3,6),
|
||||
(261,3,6),
|
||||
(279,4,6),
|
||||
(281,4,6),
|
||||
(283,4,6),
|
||||
(285,4,6),
|
||||
(287,4,6),
|
||||
(289,4,6),
|
||||
(291,4,6),
|
||||
(293,4,6),
|
||||
(295,4,6),
|
||||
(297,4,6),
|
||||
(299,4,6),
|
||||
(301,4,6),
|
||||
(303,4,6),
|
||||
(305,4,6),
|
||||
(307,4,6),
|
||||
(309,3,6),
|
||||
(313,4,6),
|
||||
(315,8,6),
|
||||
(317,8,6),
|
||||
(319,8,6),
|
||||
(321,8,6),
|
||||
(323,3,6),
|
||||
(325,3,6),
|
||||
(327,3,6),
|
||||
(329,3,6),
|
||||
(331,3,6),
|
||||
(333,3,6),
|
||||
(335,3,6),
|
||||
(337,4,6),
|
||||
(461,4,6),
|
||||
(463,4,6),
|
||||
(465,4,6),
|
||||
(467,4,6),
|
||||
(469,4,6),
|
||||
(477,1,6),
|
||||
(479,1,6),
|
||||
(481,1,6),
|
||||
(485,7,6),
|
||||
(487,7,6),
|
||||
(489,7,6),
|
||||
(491,7,6),
|
||||
(493,7,6),
|
||||
(495,7,6),
|
||||
(497,7,6),
|
||||
(499,7,6),
|
||||
(501,7,6),
|
||||
(570,7,6),
|
||||
(572,7,6),
|
||||
(574,7,6),
|
||||
(576,7,6),
|
||||
(578,7,6),
|
||||
(581,7,6),
|
||||
(583,7,6),
|
||||
(615,5,6),
|
||||
(617,5,6);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1008,9 +1008,15 @@ void CharacterDatabaseConnection::DoPrepareStatements()
|
||||
PrepareStatement(CHAR_DEL_CLUB_FINDER_APPLICATION, "DELETE FROM club_finder_application WHERE postingId = ? AND playerGuid = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(CHAR_UPD_CLUB_FINDER_POSTING_FLAGS, "UPDATE club_finder_posting SET displayFlags = ? WHERE postingId = ?", CONNECTION_ASYNC);
|
||||
|
||||
PrepareStatement(CHAR_SEL_CHARACTER_RESEARCH_SITE, "SELECT researchSiteId, progress FROM character_research_site WHERE guid = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(CHAR_SEL_CHARACTER_RESEARCH_SITE, "SELECT researchSiteId, progress, findX, findY FROM character_research_site WHERE guid = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(CHAR_DEL_CHARACTER_RESEARCH_SITE, "DELETE FROM character_research_site WHERE guid = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(CHAR_INS_CHARACTER_RESEARCH_SITE, "INSERT INTO character_research_site (guid, researchSiteId, progress) VALUES (?, ?, ?)", CONNECTION_ASYNC);
|
||||
PrepareStatement(CHAR_INS_CHARACTER_RESEARCH_SITE, "INSERT INTO character_research_site (guid, researchSiteId, progress, findX, findY) VALUES (?, ?, ?, ?, ?)", CONNECTION_ASYNC);
|
||||
PrepareStatement(CHAR_SEL_CHARACTER_RESEARCH_PROJECT, "SELECT projectId FROM character_research_project WHERE guid = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(CHAR_DEL_CHARACTER_RESEARCH_PROJECT, "DELETE FROM character_research_project WHERE guid = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(CHAR_INS_CHARACTER_RESEARCH_PROJECT, "INSERT INTO character_research_project (guid, projectId) VALUES (?, ?)", CONNECTION_ASYNC);
|
||||
PrepareStatement(CHAR_SEL_CHARACTER_RESEARCH_HISTORY, "SELECT projectId, firstCompleted, completionCount FROM character_research_history WHERE guid = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(CHAR_DEL_CHARACTER_RESEARCH_HISTORY, "DELETE FROM character_research_history WHERE guid = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(CHAR_INS_CHARACTER_RESEARCH_HISTORY, "INSERT INTO character_research_history (guid, projectId, firstCompleted, completionCount) VALUES (?, ?, ?, ?)", CONNECTION_ASYNC);
|
||||
|
||||
PrepareStatement(CHAR_SEL_WEEKLY_REWARD_ACTIVITY, "SELECT period, activityType, count, bestLevel, levels FROM character_weekly_reward_activity WHERE ownerGuid = ?", CONNECTION_SYNCH);
|
||||
PrepareStatement(CHAR_REP_WEEKLY_REWARD_ACTIVITY, "REPLACE INTO character_weekly_reward_activity (ownerGuid, activityType, period, count, bestLevel, levels) VALUES (?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC);
|
||||
|
||||
@@ -853,6 +853,12 @@ enum CharacterDatabaseStatements : uint32
|
||||
CHAR_SEL_CHARACTER_RESEARCH_SITE,
|
||||
CHAR_DEL_CHARACTER_RESEARCH_SITE,
|
||||
CHAR_INS_CHARACTER_RESEARCH_SITE,
|
||||
CHAR_SEL_CHARACTER_RESEARCH_PROJECT,
|
||||
CHAR_DEL_CHARACTER_RESEARCH_PROJECT,
|
||||
CHAR_INS_CHARACTER_RESEARCH_PROJECT,
|
||||
CHAR_SEL_CHARACTER_RESEARCH_HISTORY,
|
||||
CHAR_DEL_CHARACTER_RESEARCH_HISTORY,
|
||||
CHAR_INS_CHARACTER_RESEARCH_HISTORY,
|
||||
|
||||
CHAR_SEL_WEEKLY_REWARD_ACTIVITY,
|
||||
CHAR_REP_WEEKLY_REWARD_ACTIVITY,
|
||||
|
||||
@@ -19,8 +19,63 @@
|
||||
#include "Containers.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "DB2Stores.h"
|
||||
#include "GameObjectData.h"
|
||||
#include "GridDefines.h"
|
||||
#include "Log.h"
|
||||
#include "Map.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "Random.h"
|
||||
#include "SpellPackets.h"
|
||||
#include "Timer.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
// Sibling: WorldObject::MovePosition default maxHeightChange - cheap local-slope reject without PathGenerator.
|
||||
constexpr float FIND_TERRAIN_PROBE_DISTANCE = 2.0f;
|
||||
constexpr float FIND_TERRAIN_MAX_HEIGHT_CHANGE = 6.0f;
|
||||
|
||||
bool IsUsableFindTerrain(Map* map, PhaseShift const& phaseShift, float x, float y)
|
||||
{
|
||||
if (!map || !Trinity::IsValidMapCoord(x, y))
|
||||
return false;
|
||||
|
||||
float const z = map->GetHeight(phaseShift, x, y, MAX_HEIGHT, true, MAX_FALL_DISTANCE);
|
||||
if (z <= INVALID_HEIGHT || !Trinity::IsValidMapCoord(x, y, z))
|
||||
return false;
|
||||
|
||||
// Sibling: Map::IsInWater / TerrainInfo::IsInWater (IN_WATER | UNDER_WATER).
|
||||
if (map->IsInWater(phaseShift, x, y, z))
|
||||
return false;
|
||||
|
||||
static constexpr float probeOffsets[4][2] =
|
||||
{
|
||||
{ FIND_TERRAIN_PROBE_DISTANCE, 0.0f },
|
||||
{ -FIND_TERRAIN_PROBE_DISTANCE, 0.0f },
|
||||
{ 0.0f, FIND_TERRAIN_PROBE_DISTANCE },
|
||||
{ 0.0f, -FIND_TERRAIN_PROBE_DISTANCE }
|
||||
};
|
||||
|
||||
for (float const (&offset)[2] : probeOffsets)
|
||||
{
|
||||
float const nx = x + offset[0];
|
||||
float const ny = y + offset[1];
|
||||
if (!Trinity::IsValidMapCoord(nx, ny))
|
||||
continue;
|
||||
|
||||
float const nz = map->GetHeight(phaseShift, nx, ny, MAX_HEIGHT, true, MAX_FALL_DISTANCE);
|
||||
if (nz <= INVALID_HEIGHT)
|
||||
continue;
|
||||
|
||||
if (std::fabs(nz - z) > FIND_TERRAIN_MAX_HEIGHT_CHANGE)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ArchaeologyMgr::ArchaeologyMgr() = default;
|
||||
ArchaeologyMgr::~ArchaeologyMgr() = default;
|
||||
@@ -79,7 +134,7 @@ void ArchaeologyMgr::LoadDigSiteData()
|
||||
|
||||
if (!sResearchSiteStore.HasRecord(siteId))
|
||||
{
|
||||
TC_LOG_ERROR("sql.sql", "Table `archaeology_dig_site` has researchSiteId {} with no matching ResearchSite.db2 entry, skipped.", siteId);
|
||||
TC_LOG_DEBUG("sql.sql", "Table `archaeology_dig_site` has researchSiteId {} with no matching ResearchSite.db2 entry, skipped.", siteId);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -92,13 +147,165 @@ void ArchaeologyMgr::LoadDigSiteData()
|
||||
TC_LOG_INFO("server.loading", ">> Loaded {} archaeology dig-site branch mappings in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
|
||||
}
|
||||
|
||||
void ArchaeologyMgr::LoadResearchBranchData()
|
||||
{
|
||||
uint32 oldMSTime = getMSTime();
|
||||
|
||||
_findGameObjectsByBranch.clear();
|
||||
|
||||
QueryResult result = WorldDatabase.Query("SELECT researchBranchId, findGameObjectId FROM archaeology_research_branch");
|
||||
if (!result)
|
||||
{
|
||||
TC_LOG_INFO("server.loading", ">> Loaded 0 archaeology research-branch policies. DB table `archaeology_research_branch` is empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32 count = 0;
|
||||
do
|
||||
{
|
||||
Field* fields = result->Fetch();
|
||||
uint32 branchId = fields[0].GetUInt32();
|
||||
uint32 gameObjectId = fields[1].GetUInt32();
|
||||
|
||||
ResearchBranchEntry const* branch = sResearchBranchStore.LookupEntry(branchId);
|
||||
if (!branch || !branch->CurrencyID)
|
||||
{
|
||||
TC_LOG_DEBUG("sql.sql", "Table `archaeology_research_branch` has researchBranchId {} with no usable ResearchBranch.db2 entry, skipped.", branchId);
|
||||
continue;
|
||||
}
|
||||
|
||||
GameObjectTemplate const* gameObjectTemplate = sObjectMgr->GetGameObjectTemplate(gameObjectId);
|
||||
if (!gameObjectTemplate || gameObjectTemplate->type != GAMEOBJECT_TYPE_CHEST ||
|
||||
gameObjectTemplate->chest.open != 1859 ||
|
||||
!gameObjectTemplate->GetLootId())
|
||||
{
|
||||
TC_LOG_DEBUG("sql.sql", "Table `archaeology_research_branch` maps branch {} to invalid archaeology find GameObject {}, skipped.", branchId, gameObjectId);
|
||||
continue;
|
||||
}
|
||||
|
||||
_findGameObjectsByBranch.emplace(branchId, gameObjectId);
|
||||
++count;
|
||||
} while (result->NextRow());
|
||||
|
||||
TC_LOG_INFO("server.loading", ">> Loaded {} archaeology research-branch policies in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
|
||||
}
|
||||
|
||||
void ArchaeologyMgr::LoadDigSitePoints()
|
||||
{
|
||||
uint32 oldMSTime = getMSTime();
|
||||
|
||||
// 0 1 2
|
||||
QueryResult result = WorldDatabase.Query("SELECT researchSiteId, posX, posY FROM archaeology_dig_site_point ORDER BY researchSiteId, idx");
|
||||
if (!result)
|
||||
{
|
||||
TC_LOG_INFO("server.loading", ">> Loaded 0 archaeology dig-site polygons. DB table `archaeology_dig_site_point` is empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32 points = 0, sites = 0;
|
||||
do
|
||||
{
|
||||
Field* fields = result->Fetch();
|
||||
uint32 siteId = fields[0].GetUInt32();
|
||||
|
||||
auto itr = _digSiteInfo.find(siteId);
|
||||
if (itr == _digSiteInfo.end())
|
||||
{
|
||||
TC_LOG_DEBUG("sql.sql", "Table `archaeology_dig_site_point` has researchSiteId {} with no branch mapping in `archaeology_dig_site`, skipped.", siteId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (itr->second.Polygon.empty())
|
||||
++sites;
|
||||
|
||||
itr->second.Polygon.emplace_back(fields[1].GetFloat(), fields[2].GetFloat());
|
||||
++points;
|
||||
} while (result->NextRow());
|
||||
|
||||
TC_LOG_INFO("server.loading", ">> Loaded {} archaeology dig-site polygons ({} points) in {} ms", sites, points, GetMSTimeDiffToNow(oldMSTime));
|
||||
}
|
||||
|
||||
ArchaeologyDigSiteInfo const* ArchaeologyMgr::GetDigSiteInfo(uint32 researchSiteId) const
|
||||
{
|
||||
auto itr = _digSiteInfo.find(researchSiteId);
|
||||
return itr != _digSiteInfo.end() ? &itr->second : nullptr;
|
||||
}
|
||||
|
||||
std::vector<uint32> ArchaeologyMgr::RollResearchSitesForMap(uint32 mapId, uint32 count) const
|
||||
uint32 ArchaeologyMgr::GetFindGameObjectId(uint32 researchBranchId) const
|
||||
{
|
||||
auto itr = _findGameObjectsByBranch.find(researchBranchId);
|
||||
return itr != _findGameObjectsByBranch.end() ? itr->second : 0;
|
||||
}
|
||||
|
||||
bool ArchaeologyMgr::IsResearchBranchEnabled(uint32 researchBranchId) const
|
||||
{
|
||||
return GetFindGameObjectId(researchBranchId) != 0;
|
||||
}
|
||||
|
||||
bool ArchaeologyMgr::IsInsideDigSite(uint32 researchSiteId, float x, float y) const
|
||||
{
|
||||
ArchaeologyDigSiteInfo const* info = GetDigSiteInfo(researchSiteId);
|
||||
if (!info || info->Polygon.size() < 3)
|
||||
return false;
|
||||
|
||||
// Standard ray-casting point-in-polygon test on the world X/Y boundary.
|
||||
std::vector<std::pair<float, float>> const& poly = info->Polygon;
|
||||
bool inside = false;
|
||||
for (std::size_t i = 0, j = poly.size() - 1; i < poly.size(); j = i++)
|
||||
{
|
||||
float xi = poly[i].first, yi = poly[i].second;
|
||||
float xj = poly[j].first, yj = poly[j].second;
|
||||
if (((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi))
|
||||
inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
bool ArchaeologyMgr::GenerateFindLocation(uint32 researchSiteId, float& x, float& y, Map* map, PhaseShift const& phaseShift) const
|
||||
{
|
||||
ArchaeologyDigSiteInfo const* info = GetDigSiteInfo(researchSiteId);
|
||||
if (!info || info->Polygon.size() < 3 || !map)
|
||||
return false;
|
||||
|
||||
float minX = info->Polygon.front().first;
|
||||
float maxX = minX;
|
||||
float minY = info->Polygon.front().second;
|
||||
float maxY = minY;
|
||||
for (std::pair<float, float> const& p : info->Polygon)
|
||||
{
|
||||
minX = std::min(minX, p.first);
|
||||
maxX = std::max(maxX, p.first);
|
||||
minY = std::min(minY, p.second);
|
||||
maxY = std::max(maxY, p.second);
|
||||
}
|
||||
|
||||
// Rejection sampling over the polygon's bounding box is uniform over the polygon, works for
|
||||
// concave boundaries, and runs only when assigning the next hidden find. Retail carries no fixed
|
||||
// find coordinates in ResearchSite.db2; the generated point is persisted with character state.
|
||||
// The terrain rejectors discard unusable Map samples without skewing that distribution.
|
||||
for (uint32 attempt = 0; attempt < 1000; ++attempt)
|
||||
{
|
||||
x = frand(minX, maxX);
|
||||
y = frand(minY, maxY);
|
||||
if (!IsInsideDigSite(researchSiteId, x, y))
|
||||
continue;
|
||||
|
||||
if (!IsUsableFindTerrain(map, phaseShift, x, y))
|
||||
continue;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ArchaeologyMgr::IsSurveyableDigSite(uint32 researchSiteId) const
|
||||
{
|
||||
ArchaeologyDigSiteInfo const* info = GetDigSiteInfo(researchSiteId);
|
||||
return info && info->FindCount && info->Polygon.size() >= 3 && GetFindGameObjectId(info->BranchID);
|
||||
}
|
||||
|
||||
std::vector<uint32> ArchaeologyMgr::RollResearchSitesForMap(uint32 mapId, uint32 count, std::vector<uint32> const& exclude) const
|
||||
{
|
||||
std::vector<uint32> result;
|
||||
|
||||
@@ -107,6 +314,15 @@ std::vector<uint32> ArchaeologyMgr::RollResearchSitesForMap(uint32 mapId, uint32
|
||||
return result;
|
||||
|
||||
std::vector<ResearchSiteEntry const*> picks = *pool;
|
||||
// Only assign sites we can fully drive (branch mapping + boundary polygon), so every active site
|
||||
// the player receives is surveyable. Sites with no `archaeology_dig_site` row are skipped.
|
||||
picks.erase(std::remove_if(picks.begin(), picks.end(),
|
||||
[this, &exclude](ResearchSiteEntry const* site)
|
||||
{
|
||||
return !IsSurveyableDigSite(site->ID) ||
|
||||
std::find(exclude.begin(), exclude.end(), site->ID) != exclude.end();
|
||||
}), picks.end());
|
||||
|
||||
if (picks.size() > count)
|
||||
Trinity::Containers::RandomResize(picks, count);
|
||||
|
||||
@@ -116,3 +332,153 @@ std::vector<uint32> ArchaeologyMgr::RollResearchSitesForMap(uint32 mapId, uint32
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32 ArchaeologyMgr::RollReplacementSite(uint32 mapId, std::vector<uint32> const& exclude) const
|
||||
{
|
||||
std::vector<ResearchSiteEntry const*> const* pool = GetResearchSitesForMap(mapId);
|
||||
if (!pool || pool->empty())
|
||||
return 0;
|
||||
|
||||
// Candidate = surveyable (branch mapping + polygon) and not already one of the player's active
|
||||
// sites, so exhausting a site never re-rolls the same one or a duplicate.
|
||||
std::vector<uint32> candidates;
|
||||
for (ResearchSiteEntry const* site : *pool)
|
||||
if (IsSurveyableDigSite(site->ID) && std::find(exclude.begin(), exclude.end(), site->ID) == exclude.end())
|
||||
candidates.push_back(site->ID);
|
||||
|
||||
if (candidates.empty())
|
||||
return 0;
|
||||
|
||||
return Trinity::Containers::SelectRandomContainerElement(candidates);
|
||||
}
|
||||
|
||||
uint32 ArchaeologyMgr::RollResearchProject(uint32 branchId, std::unordered_set<uint32> const& completed) const
|
||||
{
|
||||
if (!IsResearchBranchEnabled(branchId))
|
||||
return 0;
|
||||
|
||||
// PROVISIONAL-FROM-FORK (evry/master-track/archaeology 882af5c594, originally 24a970f7c7):
|
||||
// split by rarity (0 = common, 1 = rare); prefer uncompleted within a tier; roll rare at
|
||||
// PROVISIONAL_RARE_CHANCE_PERCENT when both tiers have candidates. This is NOT a sniffed retail
|
||||
// rate - retail weighting, absolute fresh-before-repeat and pity are all unmodelled. Change the
|
||||
// constant (and/or the policy) once a sample replaces it.
|
||||
std::vector<uint32> commons, rares;
|
||||
for (ResearchProjectEntry const* project : sResearchProjectStore)
|
||||
{
|
||||
if (project->ResearchBranchID != branchId || project->SpellID <= 0)
|
||||
continue;
|
||||
|
||||
(project->Rarity == 0 ? commons : rares).push_back(project->ID);
|
||||
}
|
||||
|
||||
// Prefer projects the player has not completed yet; fall back to the full set once a rarity tier
|
||||
// is exhausted so a long-time archaeologist keeps getting projects (retail allows repeats).
|
||||
auto dropCompleted = [&completed](std::vector<uint32>& pool)
|
||||
{
|
||||
std::vector<uint32> fresh;
|
||||
for (uint32 id : pool)
|
||||
if (!completed.count(id))
|
||||
fresh.push_back(id);
|
||||
if (!fresh.empty())
|
||||
pool = std::move(fresh);
|
||||
};
|
||||
dropCompleted(commons);
|
||||
dropCompleted(rares);
|
||||
|
||||
constexpr uint32 PROVISIONAL_RARE_CHANCE_PERCENT = 20; // PROVISIONAL-FROM-FORK 882af5c594 - not retail-proven
|
||||
bool const pickRare = !rares.empty() && (commons.empty() || urand(0, 99) < PROVISIONAL_RARE_CHANCE_PERCENT);
|
||||
std::vector<uint32> const& pool = pickRare ? rares : (!commons.empty() ? commons : rares);
|
||||
if (pool.empty())
|
||||
return 0;
|
||||
|
||||
return Trinity::Containers::SelectRandomContainerElement(pool);
|
||||
}
|
||||
|
||||
ResearchProjectEntry const* ArchaeologyMgr::GetProjectBySpellId(uint32 spellId) const
|
||||
{
|
||||
if (!spellId)
|
||||
return nullptr;
|
||||
|
||||
// Linear scan of the (few hundred) projects. Only reached when a player casts an archaeology solve
|
||||
// spell, so the cost is negligible and avoids maintaining a separate spell->project index.
|
||||
for (ResearchProjectEntry const* project : sResearchProjectStore)
|
||||
if (project->SpellID > 0 && uint32(project->SpellID) == spellId)
|
||||
return project;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::optional<ArchaeologySolvePlan> ArchaeologyMgr::BuildSolvePlan(
|
||||
uint32 spellId, std::vector<WorldPackets::Spells::SpellWeight> const& weights) const
|
||||
{
|
||||
ResearchProjectEntry const* project = GetProjectBySpellId(spellId);
|
||||
if (!project || !IsResearchBranchEnabled(project->ResearchBranchID))
|
||||
return std::nullopt;
|
||||
|
||||
ResearchBranchEntry const* branch = sResearchBranchStore.LookupEntry(project->ResearchBranchID);
|
||||
if (!branch || !branch->CurrencyID)
|
||||
return std::nullopt;
|
||||
|
||||
CurrencyTypesEntry const* fragments = sCurrencyTypesStore.LookupEntry(branch->CurrencyID);
|
||||
if (!fragments || !fragments->SpellWeight)
|
||||
return std::nullopt;
|
||||
|
||||
uint64 fragmentCount = 0;
|
||||
uint64 keystoneCount = 0;
|
||||
for (WorldPackets::Spells::SpellWeight const& weight : weights)
|
||||
{
|
||||
if (weight.ID <= 0 || !weight.Quantity)
|
||||
return std::nullopt;
|
||||
|
||||
switch (weight.Type)
|
||||
{
|
||||
case 1:
|
||||
if (uint32(weight.ID) != branch->CurrencyID)
|
||||
return std::nullopt;
|
||||
fragmentCount += weight.Quantity;
|
||||
break;
|
||||
case 2:
|
||||
if (branch->ItemID <= 0 || weight.ID != branch->ItemID)
|
||||
return std::nullopt;
|
||||
keystoneCount += weight.Quantity;
|
||||
break;
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
if (keystoneCount > project->NumSockets || fragmentCount > project->RequiredWeight ||
|
||||
fragmentCount > uint64(std::numeric_limits<int32>::max()))
|
||||
return std::nullopt;
|
||||
|
||||
uint64 totalWeight = fragmentCount * fragments->SpellWeight;
|
||||
if (totalWeight > project->RequiredWeight)
|
||||
return std::nullopt;
|
||||
|
||||
if (keystoneCount)
|
||||
{
|
||||
ItemSparseEntry const* keystone = sItemSparseStore.LookupEntry(uint32(branch->ItemID));
|
||||
if (!keystone || !keystone->SpellWeight ||
|
||||
keystone->SpellWeightCategory != fragments->SpellCategory)
|
||||
return std::nullopt;
|
||||
|
||||
uint64 keystoneWeight = keystoneCount * keystone->SpellWeight;
|
||||
if (keystoneWeight > project->RequiredWeight - totalWeight)
|
||||
return std::nullopt;
|
||||
|
||||
totalWeight += keystoneWeight;
|
||||
}
|
||||
|
||||
if (totalWeight != project->RequiredWeight)
|
||||
return std::nullopt;
|
||||
|
||||
ArchaeologySolvePlan plan;
|
||||
plan.ProjectID = project->ID;
|
||||
plan.BranchID = project->ResearchBranchID;
|
||||
plan.FragmentCurrencyID = branch->CurrencyID;
|
||||
plan.FragmentCount = uint32(fragmentCount);
|
||||
plan.KeystoneItemID = branch->ItemID > 0 ? uint32(branch->ItemID) : 0;
|
||||
plan.KeystoneCount = uint32(keystoneCount);
|
||||
plan.RequiredWeight = project->RequiredWeight;
|
||||
return plan;
|
||||
}
|
||||
|
||||
@@ -19,24 +19,48 @@
|
||||
#define TRINITY_ARCHAEOLOGYMGR_H
|
||||
|
||||
#include "Define.h"
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
struct ResearchProjectEntry;
|
||||
struct ResearchSiteEntry;
|
||||
struct ArchaeologyMgrTestAccess;
|
||||
|
||||
class Map;
|
||||
class PhaseShift;
|
||||
|
||||
namespace WorldPackets::Spells
|
||||
{
|
||||
struct SpellWeight;
|
||||
}
|
||||
|
||||
struct ArchaeologySolvePlan
|
||||
{
|
||||
uint32 ProjectID = 0;
|
||||
uint32 BranchID = 0;
|
||||
uint32 FragmentCurrencyID = 0;
|
||||
uint32 FragmentCount = 0;
|
||||
uint32 KeystoneItemID = 0;
|
||||
uint32 KeystoneCount = 0;
|
||||
uint32 RequiredWeight = 0;
|
||||
};
|
||||
|
||||
// Per dig site: which research branch its fragments belong to, and how many finds exhaust it.
|
||||
// The site->branch theming is not carried by client DB2, so it is fork reference data loaded from
|
||||
// the world table `archaeology_dig_site` (seeded from the Archy addon join). See
|
||||
// docs/midnight-assessment/archaeology/archaeology-phase1-foundation-handoff.md.
|
||||
// The site->branch theming is not carried by client DB2, so it is server-owned reference data
|
||||
// loaded from the world table `archaeology_dig_site`.
|
||||
struct ArchaeologyDigSiteInfo
|
||||
{
|
||||
uint32 BranchID = 0;
|
||||
uint8 FindCount = 0;
|
||||
std::vector<std::pair<float, float>> Polygon; // dig-site boundary, world X/Y vertices in order
|
||||
};
|
||||
|
||||
// Server-side owner of Archaeology (secondary profession) research data loaded from the client
|
||||
// DB2 stores. Indexes the dig-site pools per continent so active sites can be assigned to players,
|
||||
// and the site->branch reference data. Dig-site polygons (QuestPOIBlob) follow with the survey slice.
|
||||
// the site->branch reference data, and the dig-site boundary polygons.
|
||||
class TC_GAME_API ArchaeologyMgr
|
||||
{
|
||||
private:
|
||||
@@ -58,19 +82,64 @@ public:
|
||||
// (needs sResearchSiteStore) and once the world DB is available.
|
||||
void LoadDigSiteData();
|
||||
|
||||
// Load server-owned branch policy (the branch-specific retail find GameObject).
|
||||
void LoadResearchBranchData();
|
||||
|
||||
// Load dig-site boundary polygons from `archaeology_dig_site_point`. Call after LoadDigSiteData.
|
||||
void LoadDigSitePoints();
|
||||
|
||||
// True if the world position (x, y) is inside the dig site's boundary polygon.
|
||||
bool IsInsideDigSite(uint32 researchSiteId, float x, float y) const;
|
||||
|
||||
// Generate a uniformly distributed hidden-find location inside the dig-site polygon,
|
||||
// rejecting Map samples with invalid ground, liquid, or locally extreme slope.
|
||||
// Returns false if the site has no usable polygon or no sample passes rejectors.
|
||||
bool GenerateFindLocation(uint32 researchSiteId, float& x, float& y, Map* map, PhaseShift const& phaseShift) const;
|
||||
|
||||
// True if the server has every policy needed to drive this site through Survey and loot.
|
||||
bool IsSurveyableDigSite(uint32 researchSiteId) const;
|
||||
|
||||
// Dig-site pool for a continent/map, or nullptr if the map has none.
|
||||
std::vector<ResearchSiteEntry const*> const* GetResearchSitesForMap(uint32 mapId) const;
|
||||
|
||||
// Randomly pick up to `count` distinct dig-site IDs from a map's pool (fewer if the pool is
|
||||
// smaller). Empty if the map has no dig sites.
|
||||
std::vector<uint32> RollResearchSitesForMap(uint32 mapId, uint32 count) const;
|
||||
// smaller), excluding IDs already active for the player. Empty if the map has no dig sites.
|
||||
std::vector<uint32> RollResearchSitesForMap(uint32 mapId, uint32 count, std::vector<uint32> const& exclude = {}) const;
|
||||
|
||||
// Pick one random branch-mapped dig site on a map that is not in `exclude` (used to replace an
|
||||
// exhausted site). Returns 0 if none are available.
|
||||
uint32 RollReplacementSite(uint32 mapId, std::vector<uint32> const& exclude) const;
|
||||
|
||||
// Pick a random research project for a branch: provisional common/rare split +
|
||||
// prefer-uncompleted + a provisional rare roll (see PROVISIONAL_RARE_CHANCE_PERCENT).
|
||||
// Returns the ResearchProject.db2 ID, or 0 if the branch has none.
|
||||
uint32 RollResearchProject(uint32 branchId, std::unordered_set<uint32> const& completed) const;
|
||||
|
||||
// The research project whose solve spell is `spellId` (the spell a player casts to complete it),
|
||||
// or nullptr if none. Called only for the archaeology solve spells the script is bound to.
|
||||
ResearchProjectEntry const* GetProjectBySpellId(uint32 spellId) const;
|
||||
|
||||
// Build the immutable resource plan represented by a research-project cast. Every accepted
|
||||
// quantity and compatibility rule comes from the cast and the loaded Research/item/currency
|
||||
// DB2 rows; player ownership is checked separately immediately before consumption.
|
||||
std::optional<ArchaeologySolvePlan> BuildSolvePlan(uint32 spellId, std::vector<WorldPackets::Spells::SpellWeight> const& weights) const;
|
||||
|
||||
// Branch/find-count for a dig site, or nullptr if the site has no mapping.
|
||||
ArchaeologyDigSiteInfo const* GetDigSiteInfo(uint32 researchSiteId) const;
|
||||
|
||||
// Branch-specific lootable find GameObject, or 0 if the branch is not enabled.
|
||||
uint32 GetFindGameObjectId(uint32 researchBranchId) const;
|
||||
|
||||
// True when the server has loaded the branch policy required to drive its complete
|
||||
// site/find/project loop.
|
||||
bool IsResearchBranchEnabled(uint32 researchBranchId) const;
|
||||
|
||||
private:
|
||||
friend struct ArchaeologyMgrTestAccess;
|
||||
|
||||
std::unordered_map<uint32 /*mapId*/, std::vector<ResearchSiteEntry const*>> _researchSitesByMap;
|
||||
std::unordered_map<uint32 /*researchSiteId*/, ArchaeologyDigSiteInfo> _digSiteInfo;
|
||||
std::unordered_map<uint32 /*researchBranchId*/, uint32 /*findGameObjectId*/> _findGameObjectsByBranch;
|
||||
};
|
||||
|
||||
#define sArchaeologyMgr ArchaeologyMgr::instance()
|
||||
|
||||
@@ -6629,7 +6629,6 @@ struct BountySetEntry
|
||||
int32 LockedQuestID;
|
||||
};
|
||||
|
||||
|
||||
struct ResearchBranchEntry
|
||||
{
|
||||
uint32 ID;
|
||||
|
||||
@@ -547,10 +547,10 @@ enum class CriteriaType : int16
|
||||
KillCreature = 0, // Kill NPC "{Creature}"
|
||||
WinBattleground = 1, // Win battleground "{Map}"
|
||||
CompleteResearchProject = 2, /*NYI*/ // Complete research project "{ResearchProject}"
|
||||
CompleteAnyResearchProject = 3, /*NYI*/ // Complete any research project
|
||||
FindResearchObject = 4, /*NYI*/ // Find research object "{GameObjects}"
|
||||
CompleteAnyResearchProject = 3, // Complete any research project
|
||||
FindResearchObject = 4, // Find research object "{GameObjects}"
|
||||
ReachLevel = 5, // Reach level
|
||||
ExhaustAnyResearchSite = 6, /*NYI*/ // Exhaust any research site
|
||||
ExhaustAnyResearchSite = 6, // Exhaust any research site
|
||||
SkillRaised = 7, // Skill "{SkillLine}" raised
|
||||
EarnAchievement = 8, // Earn achievement "{Achievement}"
|
||||
CompleteQuestsCount = 9, // Count of complete quests (quest count)
|
||||
|
||||
@@ -1451,7 +1451,7 @@ TempSummon* WorldObject::SummonPersonalClone(Position const& pos, TempSummonType
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
GameObject* WorldObject::SummonGameObject(uint32 entry, Position const& pos, QuaternionData const& rot, Seconds respawnTime, GOSummonType summonType)
|
||||
GameObject* WorldObject::SummonGameObject(uint32 entry, Position const& pos, QuaternionData const& rot, Seconds respawnTime, GOSummonType summonType, ObjectGuid privateObjectOwner)
|
||||
{
|
||||
if (!IsInWorld())
|
||||
return nullptr;
|
||||
@@ -1469,7 +1469,10 @@ GameObject* WorldObject::SummonGameObject(uint32 entry, Position const& pos, Qua
|
||||
return nullptr;
|
||||
|
||||
PhasingHandler::InheritPhaseShift(go, this);
|
||||
|
||||
//WowCommunity
|
||||
if (!privateObjectOwner.IsEmpty())
|
||||
go->SetPrivateObjectOwner(privateObjectOwner);
|
||||
//WowCommunity
|
||||
go->SetRespawnTime(respawnTime.count());
|
||||
if (GetTypeId() == TYPEID_PLAYER || (GetTypeId() == TYPEID_UNIT && summonType == GO_SUMMON_TIMED_OR_CORPSE_DESPAWN)) //not sure how to handle this
|
||||
ToUnit()->AddGameObject(go);
|
||||
@@ -1480,7 +1483,7 @@ GameObject* WorldObject::SummonGameObject(uint32 entry, Position const& pos, Qua
|
||||
return go;
|
||||
}
|
||||
|
||||
GameObject* WorldObject::SummonGameObject(uint32 entry, float x, float y, float z, float ang, QuaternionData const& rot, Seconds respawnTime, GOSummonType summonType)
|
||||
GameObject* WorldObject::SummonGameObject(uint32 entry, float x, float y, float z, float ang, QuaternionData const& rot, Seconds respawnTime, GOSummonType summonType, ObjectGuid privateObjectOwner)
|
||||
{
|
||||
if (!x && !y && !z)
|
||||
{
|
||||
@@ -1489,7 +1492,7 @@ GameObject* WorldObject::SummonGameObject(uint32 entry, float x, float y, float
|
||||
}
|
||||
|
||||
Position pos(x, y, z, ang);
|
||||
return SummonGameObject(entry, pos, rot, respawnTime, summonType);
|
||||
return SummonGameObject(entry, pos, rot, respawnTime, summonType, privateObjectOwner);
|
||||
}
|
||||
|
||||
Creature* WorldObject::SummonTrigger(float x, float y, float z, float ang, Milliseconds despawnTime, CreatureAI* (*GetAI)(Creature*))
|
||||
|
||||
@@ -422,8 +422,8 @@ class TC_GAME_API WorldObject : public Object, public WorldLocation
|
||||
TempSummon* SummonCreature(uint32 entry, Position const& pos, TempSummonType despawnType = TEMPSUMMON_MANUAL_DESPAWN, Milliseconds despawnTime = 0s, uint32 vehId = 0, uint32 spellId = 0, ObjectGuid privateObjectOwner = ObjectGuid::Empty);
|
||||
TempSummon* SummonCreature(uint32 entry, float x, float y, float z, float o = 0, TempSummonType despawnType = TEMPSUMMON_MANUAL_DESPAWN, Milliseconds despawnTime = 0s, ObjectGuid privateObjectOwner = ObjectGuid::Empty);
|
||||
TempSummon* SummonPersonalClone(Position const& pos, TempSummonType despawnType = TEMPSUMMON_MANUAL_DESPAWN, Milliseconds despawnTime = 0s, uint32 vehId = 0, uint32 spellId = 0, Player* privateObjectOwner = nullptr);
|
||||
GameObject* SummonGameObject(uint32 entry, Position const& pos, QuaternionData const& rot, Seconds respawnTime, GOSummonType summonType = GO_SUMMON_TIMED_OR_CORPSE_DESPAWN);
|
||||
GameObject* SummonGameObject(uint32 entry, float x, float y, float z, float ang, QuaternionData const& rot, Seconds respawnTime, GOSummonType summonType = GO_SUMMON_TIMED_OR_CORPSE_DESPAWN);
|
||||
GameObject* SummonGameObject(uint32 entry, Position const& pos, QuaternionData const& rot, Seconds respawnTime, GOSummonType summonType = GO_SUMMON_TIMED_OR_CORPSE_DESPAWN, ObjectGuid privateObjectOwner = ObjectGuid::Empty);
|
||||
GameObject* SummonGameObject(uint32 entry, float x, float y, float z, float ang, QuaternionData const& rot, Seconds respawnTime, GOSummonType summonType = GO_SUMMON_TIMED_OR_CORPSE_DESPAWN, ObjectGuid privateObjectOwner = ObjectGuid::Empty);
|
||||
Creature* SummonTrigger(float x, float y, float z, float ang, Milliseconds despawnTime, CreatureAI* (*GetAI)(Creature*) = nullptr);
|
||||
void SummonCreatureGroup(uint8 group, std::list<TempSummon*>* list = nullptr);
|
||||
|
||||
|
||||
@@ -158,15 +158,19 @@
|
||||
#include "DelvesDefines.h"
|
||||
#include "DelvesRewards.h"
|
||||
#include "GarrisonPackets.h"
|
||||
#include "GridDefines.h"
|
||||
#include "HousingDynamicEntity.h"
|
||||
#include "MythicPlusPacketsCommon.h"
|
||||
#include "ArchaeologyMgr.h"
|
||||
#include "ArchaeologyPackets.h"
|
||||
#include "ElapsedTimerMgr.h"
|
||||
#include "PreyMgr.h"
|
||||
#include "OmniumFolioMgr.h"
|
||||
#include "CovenantPackets.h"
|
||||
#include "WeeklyRewardsMgr.h"
|
||||
#include <LFGPackets.h>
|
||||
#include "QuaternionData.h"
|
||||
#include "LFGPackets.h"
|
||||
#include <cmath>
|
||||
//WowCommunity
|
||||
|
||||
// corpse reclaim times
|
||||
@@ -176,6 +180,7 @@
|
||||
enum PlayerSpells
|
||||
{
|
||||
SPELL_EXPERIENCE_ELIMINATED = 206662,
|
||||
SPELL_ARCHAEOLOGY_STANDING_ON_IT = 210837,
|
||||
};
|
||||
|
||||
static uint32 corpseReclaimDelay[MAX_DEATH_COUNT] = { 30, 60, 120 };
|
||||
@@ -4185,6 +4190,20 @@ void Player::DeleteFromDB(ObjectGuid playerguid, uint32 accountId, bool updateRe
|
||||
stmt->setUInt64(0, guid);
|
||||
trans->Append(stmt);
|
||||
|
||||
//WowCommunity
|
||||
stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_RESEARCH_SITE);
|
||||
stmt->setUInt64(0, guid);
|
||||
trans->Append(stmt);
|
||||
|
||||
stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_RESEARCH_PROJECT);
|
||||
stmt->setUInt64(0, guid);
|
||||
trans->Append(stmt);
|
||||
//WowCommunity
|
||||
|
||||
stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_RESEARCH_HISTORY);
|
||||
stmt->setUInt64(0, guid);
|
||||
trans->Append(stmt);
|
||||
|
||||
stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_STATS);
|
||||
stmt->setUInt64(0, guid);
|
||||
trans->Append(stmt);
|
||||
@@ -4249,9 +4268,6 @@ void Player::DeleteFromDB(ObjectGuid playerguid, uint32 accountId, bool updateRe
|
||||
stmt->setUInt64(0, guid);
|
||||
trans->Append(stmt);
|
||||
|
||||
stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_RESEARCH_SITE);
|
||||
stmt->setUInt64(0, guid);
|
||||
trans->Append(stmt);
|
||||
|
||||
sCharacterCache->DeleteCharacterCacheEntry(playerguid, name);
|
||||
break;
|
||||
@@ -6254,6 +6270,9 @@ bool Player::UpdatePosition(float x, float y, float z, float orientation, bool t
|
||||
SetGroupUpdateFlag(GROUP_UPDATE_FLAG_POSITION);
|
||||
|
||||
CheckAreaExplore();
|
||||
//WowCommunity
|
||||
_UpdateArchaeologySurveyIndicator();
|
||||
//WowCommunity
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -19059,7 +19078,10 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
|
||||
|
||||
//WowCommunity
|
||||
_LoadResearchSites(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_RESEARCH_SITES)); // Archaeology: restore persisted dig sites
|
||||
InitializeResearchSites(); // Archaeology: seed active dig sites if none persisted and skills are known (Phase 1 slice)
|
||||
InitializeResearchSites(); // Archaeology: seed active dig sites if none persisted and the profession is known
|
||||
_LoadResearchHistory(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_RESEARCH_HISTORY)); // Archaeology: restore completed projects (before project rolls so they avoid repeats)
|
||||
_LoadResearchProjects(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_RESEARCH_PROJECTS)); // Archaeology: restore active research projects
|
||||
InitializeResearchProjects(); // Archaeology: backfill a project for branches with fragments but none active
|
||||
//WowCommunity
|
||||
|
||||
SetNumRespecs(fields.numRespecs);
|
||||
@@ -21278,6 +21300,8 @@ void Player::SaveToDB(LoginDatabaseTransaction loginTransaction, CharacterDataba
|
||||
_SaveAuras(trans);
|
||||
_SaveSkills(trans);
|
||||
_SaveResearchSites(trans);
|
||||
_SaveResearchProjects(trans);
|
||||
_SaveResearchHistory(trans);
|
||||
_SaveStoredAuraTeleportLocations(trans);
|
||||
m_achievementMgr->SaveToDB(trans);
|
||||
m_reputationMgr->SaveToDB(trans);
|
||||
@@ -21998,14 +22022,688 @@ void Player::_SaveResearchSites(CharacterDatabaseTransaction trans)
|
||||
uint32 const count = m_activePlayerData->ResearchSites[0].size();
|
||||
for (uint32 i = 0; i < count; ++i)
|
||||
{
|
||||
uint32 const siteId = m_activePlayerData->ResearchSites[0][i];
|
||||
float findX = 0.0f, findY = 0.0f;
|
||||
_EnsureResearchSiteFindLocation(siteId, findX, findY);
|
||||
|
||||
stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_RESEARCH_SITE);
|
||||
stmt->setUInt64(0, GetGUID().GetCounter());
|
||||
stmt->setUInt16(1, m_activePlayerData->ResearchSites[0][i]);
|
||||
stmt->setUInt16(1, siteId);
|
||||
stmt->setUInt32(2, m_activePlayerData->ResearchSiteProgress[0][i]);
|
||||
stmt->setFloat(3, findX);
|
||||
stmt->setFloat(4, findY);
|
||||
trans->Append(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::_SaveResearchProjects(CharacterDatabaseTransaction trans)
|
||||
{
|
||||
// Rewrite the character's active research projects from the Research update field (delete-all +
|
||||
// reinsert; one entry per active branch).
|
||||
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_RESEARCH_PROJECT);
|
||||
stmt->setUInt64(0, GetGUID().GetCounter());
|
||||
trans->Append(stmt);
|
||||
|
||||
uint32 const count = m_activePlayerData->Research[0].size();
|
||||
for (uint32 i = 0; i < count; ++i)
|
||||
{
|
||||
int16 projectId = m_activePlayerData->Research[0][i].ResearchProjectID;
|
||||
if (!projectId)
|
||||
continue;
|
||||
|
||||
ResearchProjectEntry const* project = sResearchProjectStore.LookupEntry(uint32(projectId));
|
||||
if (!project || !sArchaeologyMgr->IsResearchBranchEnabled(project->ResearchBranchID))
|
||||
continue;
|
||||
|
||||
stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_RESEARCH_PROJECT);
|
||||
stmt->setUInt64(0, GetGUID().GetCounter());
|
||||
stmt->setUInt32(1, uint32(projectId));
|
||||
trans->Append(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::_SaveResearchHistory(CharacterDatabaseTransaction trans)
|
||||
{
|
||||
// Rewrite the character's completed research projects from ResearchHistory (delete-all + reinsert).
|
||||
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_RESEARCH_HISTORY);
|
||||
stmt->setUInt64(0, GetGUID().GetCounter());
|
||||
trans->Append(stmt);
|
||||
|
||||
auto const& completed = m_activePlayerData->ResearchHistory->CompletedProjects;
|
||||
for (uint32 i = 0; i < completed.size(); ++i)
|
||||
{
|
||||
stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_RESEARCH_HISTORY);
|
||||
stmt->setUInt64(0, GetGUID().GetCounter());
|
||||
stmt->setUInt32(1, completed[i].ProjectID);
|
||||
stmt->setInt64(2, completed[i].FirstCompleted);
|
||||
stmt->setUInt32(3, completed[i].CompletionCount);
|
||||
trans->Append(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Player::HandleArchaeologySurvey()
|
||||
{
|
||||
// A successful Survey reveals a private, branch-specific lootable find at the hidden position and
|
||||
// advances site progress immediately. Dig/open-lock and fragment award then use the normal
|
||||
// GameObject chest path. Retail also spawns an approximate red/yellow/green direction tool on
|
||||
// misses and applies Standing On It while the player is over the hidden point.
|
||||
//
|
||||
// PROVISIONAL-FROM-FORK (evry/master-track/archaeology 9d7a0c6254 / 107b2cea57): the 8/40/80
|
||||
// distance bands, the 8/20/40 degree facing cones and the 2-4 yd tool spawn ring come from the
|
||||
// fork's retail observation, not from a captured server rule.
|
||||
constexpr uint32 SPELL_ARCHAEOLOGY_SURVEY = 80451;
|
||||
constexpr uint32 GO_SURVEY_TOOL_GREEN = 204272;
|
||||
constexpr uint32 GO_SURVEY_TOOL_YELLOW = 206589;
|
||||
constexpr uint32 GO_SURVEY_TOOL_RED = 206590;
|
||||
constexpr float SURVEY_FIND_DISTANCE = 8.0f;
|
||||
constexpr float SURVEY_GREEN_DISTANCE = 40.0f;
|
||||
constexpr float SURVEY_YELLOW_DISTANCE = 80.0f;
|
||||
// Max |tool facing -> find| lean per band, in radians.
|
||||
constexpr float SURVEY_FACING_CONE_GREEN = float(8.0 * M_PI / 180.0);
|
||||
constexpr float SURVEY_FACING_CONE_YELLOW = float(20.0 * M_PI / 180.0);
|
||||
constexpr float SURVEY_FACING_CONE_RED = float(40.0 * M_PI / 180.0);
|
||||
constexpr float SURVEY_TOOL_SPAWN_MIN = 2.0f;
|
||||
constexpr float SURVEY_TOOL_SPAWN_MAX = 4.0f;
|
||||
constexpr Seconds SURVEY_TOOL_DURATION = 5s;
|
||||
constexpr Seconds ARCHAEOLOGY_FIND_DURATION = 2min;
|
||||
|
||||
if (!HasSkill(SKILL_ARCHAEOLOGY))
|
||||
return;
|
||||
|
||||
// Survey tools occupy the reference implementation's second GameObject slot. Recasting removes
|
||||
// the previous tool before creating its replacement, so stale guidance cannot make a
|
||||
// later cast appear inert.
|
||||
if (GameObject* previousTool = GetMap()->GetGameObject(m_ObjectSlot[1]))
|
||||
{
|
||||
uint32 const entry = previousTool->GetEntry();
|
||||
if (entry == GO_SURVEY_TOOL_GREEN || entry == GO_SURVEY_TOOL_YELLOW || entry == GO_SURVEY_TOOL_RED)
|
||||
{
|
||||
if (previousTool->GetSpellId() == SPELL_ARCHAEOLOGY_SURVEY)
|
||||
previousTool->SetSpellId(0);
|
||||
|
||||
RemoveGameObject(previousTool, true);
|
||||
m_ObjectSlot[1] = ObjectGuid::Empty;
|
||||
}
|
||||
}
|
||||
|
||||
// Only one revealed, unconsumed find may exist for a player. Clear an expired/despawned token
|
||||
// lazily; owned GameObjects are also removed by normal player cleanup.
|
||||
if (_pendingArchaeologyFind)
|
||||
{
|
||||
if (GameObject* pendingFind = GetMap()->GetGameObject(_pendingArchaeologyFind->GameObjectGuid))
|
||||
if (pendingFind->isSpawned())
|
||||
return;
|
||||
|
||||
_pendingArchaeologyFind.reset();
|
||||
}
|
||||
|
||||
uint32 const mapId = GetMapId();
|
||||
float const px = GetPositionX();
|
||||
float const py = GetPositionY();
|
||||
|
||||
// Find which of the player's active dig sites (on this map) they are standing in.
|
||||
uint32 siteId = 0;
|
||||
uint32 siteIndex = 0;
|
||||
ArchaeologyDigSiteInfo const* info = nullptr;
|
||||
uint32 const siteCount = m_activePlayerData->ResearchSites[0].size();
|
||||
for (uint32 i = 0; i < siteCount; ++i)
|
||||
{
|
||||
uint32 candidate = m_activePlayerData->ResearchSites[0][i];
|
||||
ResearchSiteEntry const* site = sResearchSiteStore.LookupEntry(candidate);
|
||||
if (!site || uint32(site->MapID) != mapId)
|
||||
continue;
|
||||
|
||||
if (sArchaeologyMgr->IsInsideDigSite(candidate, px, py))
|
||||
{
|
||||
siteId = candidate;
|
||||
siteIndex = i;
|
||||
info = sArchaeologyMgr->GetDigSiteInfo(candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!siteId || !info)
|
||||
return; // not standing in one of the player's active dig sites
|
||||
|
||||
uint32 const progressSize = m_activePlayerData->ResearchSiteProgress[0].size();
|
||||
uint32 progress = siteIndex < progressSize ? m_activePlayerData->ResearchSiteProgress[0][siteIndex] : 0;
|
||||
if (progress >= info->FindCount)
|
||||
{
|
||||
// Site already fully surveyed (e.g. persisted full from before exhaust/replace existed):
|
||||
// cycle it now instead of no-oping, so the player is never stuck on a dead site.
|
||||
ReplaceResearchSite(siteIndex, mapId);
|
||||
return;
|
||||
}
|
||||
|
||||
float fx, fy;
|
||||
if (!_EnsureResearchSiteFindLocation(siteId, fx, fy))
|
||||
return;
|
||||
|
||||
float const dist = GetExactDist2d(fx, fy);
|
||||
// Reveal when strictly inside the band; dist == 8.0f is still green guidance.
|
||||
bool found = dist < SURVEY_FIND_DISTANCE;
|
||||
|
||||
if (found)
|
||||
{
|
||||
uint32 const findGameObjectId = sArchaeologyMgr->GetFindGameObjectId(info->BranchID);
|
||||
if (!findGameObjectId)
|
||||
found = false;
|
||||
else
|
||||
{
|
||||
// Sibling: ArchaeologyMgr::IsUsableFindTerrain / Creature spawn - resolve Z from
|
||||
// MAX_HEIGHT, not player Z. UpdateGroundPositionZ searches downward from the seed; on
|
||||
// inclines the dig XY ground can sit above the player and the GO clips underground.
|
||||
float const fz = GetMap()->GetHeight(GetPhaseShift(), fx, fy, MAX_HEIGHT, true, MAX_FALL_DISTANCE);
|
||||
if (fz <= INVALID_HEIGHT || !Trinity::IsValidMapCoord(fx, fy, fz))
|
||||
found = false;
|
||||
else
|
||||
{
|
||||
float const facing = GetOrientation();
|
||||
if (GameObject* find = SummonGameObject(findGameObjectId, Position(fx, fy, fz, facing),
|
||||
QuaternionData::fromEulerAnglesZYX(facing, 0.0f, 0.0f), ARCHAEOLOGY_FIND_DURATION,
|
||||
GO_SUMMON_TIMED_OR_CORPSE_DESPAWN, GetGUID()))
|
||||
{
|
||||
_pendingArchaeologyFind = PendingArchaeologyFind
|
||||
{
|
||||
.GameObjectGuid = find->GetGUID(),
|
||||
.ResearchSiteId = siteId,
|
||||
.ResearchBranchId = info->BranchID
|
||||
};
|
||||
|
||||
++progress;
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::ResearchSiteProgress, 0).ModifyValue(siteIndex), progress);
|
||||
UpdateCriteria(CriteriaType::FindResearchObject, findGameObjectId);
|
||||
RemoveAurasDueToSpell(SPELL_ARCHAEOLOGY_STANDING_ON_IT);
|
||||
|
||||
// Progress advances at reveal on retail. Generate and retain the next point now so
|
||||
// relog/restart cannot relocate an in-progress site's hidden find.
|
||||
if (progress < info->FindCount)
|
||||
{
|
||||
_researchSiteFindLocations.erase(siteId);
|
||||
float nextX, nextY;
|
||||
_EnsureResearchSiteFindLocation(siteId, nextX, nextY);
|
||||
}
|
||||
}
|
||||
else
|
||||
found = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
uint32 const toolEntry = dist < SURVEY_GREEN_DISTANCE ? GO_SURVEY_TOOL_GREEN
|
||||
: dist < SURVEY_YELLOW_DISTANCE ? GO_SURVEY_TOOL_YELLOW
|
||||
: GO_SURVEY_TOOL_RED;
|
||||
float const facingCone = dist < SURVEY_GREEN_DISTANCE ? SURVEY_FACING_CONE_GREEN
|
||||
: dist < SURVEY_YELLOW_DISTANCE ? SURVEY_FACING_CONE_YELLOW
|
||||
: SURVEY_FACING_CONE_RED;
|
||||
|
||||
// Retail spawns the theodolite beside the player (small ring), not under feet.
|
||||
float const spawnAngle = frand(0.0f, float(2 * M_PI));
|
||||
float const spawnDist = frand(SURVEY_TOOL_SPAWN_MIN, SURVEY_TOOL_SPAWN_MAX);
|
||||
float const tx = px + spawnDist * std::cos(spawnAngle);
|
||||
float const ty = py + spawnDist * std::sin(spawnAngle);
|
||||
float const tz = GetMap()->GetHeight(GetPhaseShift(), tx, ty, MAX_HEIGHT, true, MAX_FALL_DISTANCE);
|
||||
if (tz > INVALID_HEIGHT && Trinity::IsValidMapCoord(tx, ty, tz))
|
||||
{
|
||||
// Facing is tool->find with a band-dependent cone (red noisier than yellow/green).
|
||||
// PROVISIONAL-FROM-FORK 107b2cea57.
|
||||
Position const toolPos(tx, ty, tz);
|
||||
float const facing = Position::NormalizeOrientation(
|
||||
toolPos.GetAbsoluteAngle(fx, fy) + frand(-facingCone, facingCone));
|
||||
if (GameObject* tool = SummonGameObject(toolEntry, Position(tx, ty, tz, facing),
|
||||
QuaternionData::fromEulerAnglesZYX(facing, 0.0f, 0.0f), SURVEY_TOOL_DURATION))
|
||||
{
|
||||
tool->SetSpellId(SPELL_ARCHAEOLOGY_SURVEY);
|
||||
m_ObjectSlot[1] = tool->GetGUID();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WorldPackets::Archaeology::SurveyCast survey;
|
||||
survey.TotalFinds = progress;
|
||||
survey.NumFindsCompleted = info->FindCount;
|
||||
survey.ResearchBranchID = info->BranchID;
|
||||
survey.SuccessfulFind = found;
|
||||
SendDirectMessage(survey.Write());
|
||||
|
||||
// Exhaust and replace once every find is collected (retail single-slot replacement).
|
||||
if (found && progress >= info->FindCount)
|
||||
{
|
||||
ReplaceResearchSite(siteIndex, mapId);
|
||||
UpdateCriteria(CriteriaType::ExhaustAnyResearchSite);
|
||||
}
|
||||
}
|
||||
|
||||
bool Player::CanUseArchaeologyFind(GameObject const* find) const
|
||||
{
|
||||
if (!find || !_pendingArchaeologyFind || !HasSkill(SKILL_ARCHAEOLOGY))
|
||||
return false;
|
||||
|
||||
return _pendingArchaeologyFind->GameObjectGuid == find->GetGUID() &&
|
||||
find->GetOwnerGUID() == GetGUID() &&
|
||||
find->GetPrivateObjectOwner() == GetGUID() &&
|
||||
find->GetEntry() == sArchaeologyMgr->GetFindGameObjectId(_pendingArchaeologyFind->ResearchBranchId);
|
||||
}
|
||||
|
||||
void Player::OnArchaeologyFindLooted(GameObject* find)
|
||||
{
|
||||
if (!CanUseArchaeologyFind(find))
|
||||
return;
|
||||
|
||||
uint32 const branchId = _pendingArchaeologyFind->ResearchBranchId;
|
||||
_pendingArchaeologyFind.reset();
|
||||
|
||||
// PROVISIONAL-FROM-FORK (evry/master-track/archaeology 24a970f7c7): the first successful
|
||||
// unique-find acquisition grants +1 Archaeology (reveal / reopen do not). Guaranteed for the
|
||||
// observed skill range; near-cap chance curves are not modelled.
|
||||
UpdateSkillPro(SKILL_ARCHAEOLOGY, 1000, 1);
|
||||
|
||||
// The normal chest loot path has already granted the branch currency (+ optional provisional
|
||||
// keystone). Surface the branch's current project immediately; login remains the disconnect fallback.
|
||||
EnsureResearchProject(branchId);
|
||||
}
|
||||
|
||||
bool Player::_EnsureResearchSiteFindLocation(uint32 researchSiteId, float& x, float& y)
|
||||
{
|
||||
auto itr = _researchSiteFindLocations.find(researchSiteId);
|
||||
if (itr != _researchSiteFindLocations.end() &&
|
||||
sArchaeologyMgr->IsInsideDigSite(researchSiteId, itr->second.first, itr->second.second))
|
||||
{
|
||||
x = itr->second.first;
|
||||
y = itr->second.second;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!sArchaeologyMgr->GenerateFindLocation(researchSiteId, x, y, GetMap(), GetPhaseShift()))
|
||||
return false;
|
||||
|
||||
_researchSiteFindLocations[researchSiteId] = { x, y };
|
||||
return true;
|
||||
}
|
||||
|
||||
void Player::_UpdateArchaeologySurveyIndicator()
|
||||
{
|
||||
// Must match HandleArchaeologySurvey's reveal radius.
|
||||
constexpr float SURVEY_FIND_DISTANCE = 8.0f;
|
||||
|
||||
bool standingOnFind = false;
|
||||
if (HasSkill(SKILL_ARCHAEOLOGY))
|
||||
{
|
||||
if (_pendingArchaeologyFind)
|
||||
{
|
||||
if (GameObject* find = GetMap()->GetGameObject(_pendingArchaeologyFind->GameObjectGuid))
|
||||
{
|
||||
if (find->isSpawned())
|
||||
{
|
||||
if (HasAura(SPELL_ARCHAEOLOGY_STANDING_ON_IT))
|
||||
RemoveAurasDueToSpell(SPELL_ARCHAEOLOGY_STANDING_ON_IT);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_pendingArchaeologyFind.reset();
|
||||
}
|
||||
|
||||
uint32 const mapId = GetMapId();
|
||||
uint32 const siteCount = m_activePlayerData->ResearchSites[0].size();
|
||||
for (uint32 i = 0; i < siteCount; ++i)
|
||||
{
|
||||
uint32 const siteId = m_activePlayerData->ResearchSites[0][i];
|
||||
ResearchSiteEntry const* site = sResearchSiteStore.LookupEntry(siteId);
|
||||
if (!site || site->MapID < 0 || uint32(site->MapID) != mapId ||
|
||||
!sArchaeologyMgr->IsInsideDigSite(siteId, GetPositionX(), GetPositionY()))
|
||||
continue;
|
||||
|
||||
ArchaeologyDigSiteInfo const* info = sArchaeologyMgr->GetDigSiteInfo(siteId);
|
||||
uint32 const progressSize = m_activePlayerData->ResearchSiteProgress[0].size();
|
||||
uint32 const progress = i < progressSize ? m_activePlayerData->ResearchSiteProgress[0][i] : 0;
|
||||
if (!info || progress >= info->FindCount)
|
||||
break;
|
||||
|
||||
float fx, fy;
|
||||
standingOnFind = _EnsureResearchSiteFindLocation(siteId, fx, fy) &&
|
||||
GetExactDist2d(fx, fy) < SURVEY_FIND_DISTANCE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (standingOnFind)
|
||||
{
|
||||
if (!HasAura(SPELL_ARCHAEOLOGY_STANDING_ON_IT))
|
||||
CastSpell(this, SPELL_ARCHAEOLOGY_STANDING_ON_IT, true);
|
||||
}
|
||||
else if (HasAura(SPELL_ARCHAEOLOGY_STANDING_ON_IT))
|
||||
RemoveAurasDueToSpell(SPELL_ARCHAEOLOGY_STANDING_ON_IT);
|
||||
}
|
||||
|
||||
void Player::ReplaceResearchSite(uint32 siteIndex, uint32 mapId)
|
||||
{
|
||||
// Swap one active dig-site slot for a fresh surveyable site on the same continent (progress
|
||||
// reset). The client picks up the new site from the ResearchSites update field. If the continent
|
||||
// has no other surveyable site the slot is left as-is.
|
||||
uint32 const siteCount = m_activePlayerData->ResearchSites[0].size();
|
||||
if (siteIndex >= siteCount)
|
||||
return;
|
||||
|
||||
std::vector<uint32> activeSites;
|
||||
activeSites.reserve(siteCount);
|
||||
for (uint32 i = 0; i < siteCount; ++i)
|
||||
activeSites.push_back(m_activePlayerData->ResearchSites[0][i]);
|
||||
|
||||
uint32 const replacement = sArchaeologyMgr->RollReplacementSite(mapId, activeSites);
|
||||
if (!replacement)
|
||||
return;
|
||||
|
||||
_researchSiteFindLocations.erase(m_activePlayerData->ResearchSites[0][siteIndex]);
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::ResearchSites, 0).ModifyValue(siteIndex), uint16(replacement));
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::ResearchSiteProgress, 0).ModifyValue(siteIndex), 0u);
|
||||
|
||||
float x, y;
|
||||
_EnsureResearchSiteFindLocation(replacement, x, y);
|
||||
}
|
||||
|
||||
void Player::_LoadResearchSites(PreparedQueryResult result)
|
||||
{
|
||||
// Restore persisted active dig sites into the ResearchSites / ResearchSiteProgress update fields.
|
||||
// SELECT researchSiteId, progress, findX, findY FROM character_research_site WHERE guid = ?
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
do
|
||||
{
|
||||
Field* fields = result->Fetch();
|
||||
uint16 siteId = fields[0].GetUInt16();
|
||||
uint32 progress = fields[1].GetUInt32();
|
||||
float findX = fields[2].GetFloat();
|
||||
float findY = fields[3].GetFloat();
|
||||
|
||||
// A persisted row can name any ResearchSite.db2 entry on the continent, including
|
||||
// non-Archaeology overlays such as warfront phases. Do not expose a site the server cannot
|
||||
// drive; InitializeResearchSites replaces the missing slot below.
|
||||
if (!sArchaeologyMgr->IsSurveyableDigSite(siteId))
|
||||
{
|
||||
TC_LOG_WARN("entities.player.loading", "Player::_LoadResearchSites: player ({}, name: '{}') has unsupported research site {}. Replacing it with a surveyable site.",
|
||||
GetGUID().ToString(), GetName(), siteId);
|
||||
continue;
|
||||
}
|
||||
|
||||
AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::ResearchSites, 0).ModifyValue()) = siteId;
|
||||
AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::ResearchSiteProgress, 0).ModifyValue()) = progress;
|
||||
|
||||
if (sArchaeologyMgr->IsInsideDigSite(siteId, findX, findY))
|
||||
_researchSiteFindLocations[siteId] = { findX, findY };
|
||||
else
|
||||
_EnsureResearchSiteFindLocation(siteId, findX, findY);
|
||||
} while (result->NextRow());
|
||||
}
|
||||
|
||||
void Player::InitializeResearchSites()
|
||||
{
|
||||
// Introduction continents: Eastern Kingdoms (0), Kalimdor (1), Outland (530), Northrend (571),
|
||||
// Pandaria (870). Content stops at Pandaria - there is no Draenor+ dig-site data, so those maps
|
||||
// are deliberately absent rather than seeded with invented rows.
|
||||
//
|
||||
// PROVISIONAL-FROM-FORK (evry/master-track/archaeology 80890c6a9f, extended by eb4525d6bf and
|
||||
// b59c8db8ff): four active sites per continent, and the only eligibility test is "knows the
|
||||
// profession and the site is surveyable". Retail gates dig sites on more than that.
|
||||
if (!HasSkill(SKILL_ARCHAEOLOGY))
|
||||
return;
|
||||
|
||||
std::vector<uint32> activeSites;
|
||||
activeSites.reserve(m_activePlayerData->ResearchSites[0].size() + 16);
|
||||
for (uint16 siteId : m_activePlayerData->ResearchSites[0])
|
||||
activeSites.push_back(siteId);
|
||||
|
||||
for (uint32 mapId : { 0u, 1u, 530u, 571u, 870u })
|
||||
{
|
||||
uint32 activeCount = 0;
|
||||
for (uint32 siteId : activeSites)
|
||||
if (ResearchSiteEntry const* site = sResearchSiteStore.LookupEntry(siteId))
|
||||
if (uint32(site->MapID) == mapId)
|
||||
++activeCount;
|
||||
|
||||
if (activeCount >= 4)
|
||||
continue;
|
||||
|
||||
for (uint32 siteId : sArchaeologyMgr->RollResearchSitesForMap(mapId, 4 - activeCount, activeSites))
|
||||
{
|
||||
AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::ResearchSites, 0).ModifyValue()) = uint16(siteId);
|
||||
AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::ResearchSiteProgress, 0).ModifyValue()) = 0u;
|
||||
activeSites.push_back(siteId);
|
||||
|
||||
float x, y;
|
||||
_EnsureResearchSiteFindLocation(siteId, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int32 Player::GetCurrentResearchProject(uint32 branchId) const
|
||||
{
|
||||
uint32 const count = m_activePlayerData->Research[0].size();
|
||||
for (uint32 i = 0; i < count; ++i)
|
||||
{
|
||||
int16 projectId = m_activePlayerData->Research[0][i].ResearchProjectID;
|
||||
if (!projectId)
|
||||
continue;
|
||||
|
||||
if (ResearchProjectEntry const* project = sResearchProjectStore.LookupEntry(uint32(projectId)))
|
||||
if (project->ResearchBranchID == branchId)
|
||||
return projectId;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::unordered_set<uint32> Player::GetCompletedResearchProjects() const
|
||||
{
|
||||
std::unordered_set<uint32> completed;
|
||||
for (UF::CompletedProject const& project : m_activePlayerData->ResearchHistory->CompletedProjects)
|
||||
completed.insert(project.ProjectID);
|
||||
return completed;
|
||||
}
|
||||
|
||||
uint32 Player::EnsureResearchProject(uint32 branchId)
|
||||
{
|
||||
if (!sArchaeologyMgr->IsResearchBranchEnabled(branchId))
|
||||
return 0;
|
||||
|
||||
if (int32 existing = GetCurrentResearchProject(branchId))
|
||||
return uint32(existing);
|
||||
|
||||
uint32 projectId = sArchaeologyMgr->RollResearchProject(branchId, GetCompletedResearchProjects());
|
||||
if (!projectId)
|
||||
return 0;
|
||||
|
||||
UF::Research& research = AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::Research, 0).ModifyValue());
|
||||
research.ResearchProjectID = int16(projectId);
|
||||
return projectId;
|
||||
}
|
||||
|
||||
void Player::InitializeResearchProjects()
|
||||
{
|
||||
// Backfill a current project for any branch the player already has fragments in but no active
|
||||
// project (e.g. characters that earned fragments before projects were implemented). Fresh fragment
|
||||
// gains assign on the fly in HandleArchaeologySurvey, so this only matters on the first login after
|
||||
// the feature lands. Currencies are already loaded by this point in LoadFromDB.
|
||||
if (!HasSkill(SKILL_ARCHAEOLOGY))
|
||||
return;
|
||||
|
||||
for (ResearchBranchEntry const* branch : sResearchBranchStore)
|
||||
{
|
||||
if (!branch->CurrencyID || GetCurrencyQuantity(branch->CurrencyID) == 0)
|
||||
continue;
|
||||
|
||||
EnsureResearchProject(branch->ID);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::_LoadResearchProjects(PreparedQueryResult result)
|
||||
{
|
||||
// Restore the character's active research projects into the Research update field.
|
||||
// SELECT projectId FROM character_research_project WHERE guid = ?
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
do
|
||||
{
|
||||
Field* fields = result->Fetch();
|
||||
uint32 projectId = fields[0].GetUInt32();
|
||||
ResearchProjectEntry const* project = sResearchProjectStore.LookupEntry(projectId);
|
||||
if (!project || !sArchaeologyMgr->IsResearchBranchEnabled(project->ResearchBranchID))
|
||||
continue;
|
||||
|
||||
UF::Research& research = AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::Research, 0).ModifyValue());
|
||||
research.ResearchProjectID = int16(projectId);
|
||||
} while (result->NextRow());
|
||||
}
|
||||
|
||||
bool Player::CanCastResearchProjectSpell(uint32 spellId) const
|
||||
{
|
||||
if (!HasSkill(SKILL_ARCHAEOLOGY))
|
||||
return false;
|
||||
|
||||
ResearchProjectEntry const* project = sArchaeologyMgr->GetProjectBySpellId(spellId);
|
||||
if (!project || !sArchaeologyMgr->IsResearchBranchEnabled(project->ResearchBranchID))
|
||||
return false;
|
||||
|
||||
// Only the branch's current project may be solved.
|
||||
if (GetCurrentResearchProject(project->ResearchBranchID) != int32(project->ID))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Player::CanSolveResearchProject(ArchaeologySolvePlan const& plan) const
|
||||
{
|
||||
if (!HasSkill(SKILL_ARCHAEOLOGY) || !sArchaeologyMgr->IsResearchBranchEnabled(plan.BranchID))
|
||||
return false;
|
||||
|
||||
ResearchProjectEntry const* project = sResearchProjectStore.LookupEntry(plan.ProjectID);
|
||||
if (!project || project->ResearchBranchID != plan.BranchID ||
|
||||
project->RequiredWeight != plan.RequiredWeight ||
|
||||
GetCurrentResearchProject(plan.BranchID) != int32(plan.ProjectID))
|
||||
return false;
|
||||
|
||||
if (plan.FragmentCount && !HasCurrency(plan.FragmentCurrencyID, plan.FragmentCount))
|
||||
return false;
|
||||
|
||||
if (plan.KeystoneCount && !HasItemCount(plan.KeystoneItemID, plan.KeystoneCount))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Player::ConsumeResearchProjectSolveResources(ArchaeologySolvePlan const& plan)
|
||||
{
|
||||
if (!CanSolveResearchProject(plan))
|
||||
return false;
|
||||
|
||||
// The final cast check and this commit run synchronously on the player's world thread. Validate
|
||||
// every resource first, then consume the exact normalized plan before the reward spell effect.
|
||||
if (plan.KeystoneCount &&
|
||||
DestroyItemCount(plan.KeystoneItemID, plan.KeystoneCount, true) != plan.KeystoneCount)
|
||||
return false;
|
||||
|
||||
if (plan.FragmentCount)
|
||||
RemoveCurrency(plan.FragmentCurrencyID, plan.FragmentCount, CurrencyDestroyReason::Spell);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Player::CompleteResearchProjectSolve(ArchaeologySolvePlan const& plan)
|
||||
{
|
||||
// The script calls this only after its exact resource plan committed. Keep an expected-project
|
||||
// guard so a completed cast cannot finalize a different or already-advanced project.
|
||||
if (GetCurrentResearchProject(plan.BranchID) != int32(plan.ProjectID))
|
||||
return;
|
||||
|
||||
ResearchProjectEntry const* project = sResearchProjectStore.LookupEntry(plan.ProjectID);
|
||||
if (!project)
|
||||
return;
|
||||
|
||||
RecordCompletedProject(plan.ProjectID);
|
||||
UpdateCriteria(CriteriaType::CompleteAnyResearchProject, project->Rarity, plan.BranchID);
|
||||
AdvanceResearchProject(plan.BranchID, plan.ProjectID);
|
||||
|
||||
// Solve skill-ups follow SkillLineAbility.NumSkillUps for Archaeology (commons 5 / rares 15).
|
||||
// Spells with no Archaeology SkillLineAbility row grant nothing. Generic UpdateCraftSkill skips
|
||||
// these rows because their SkillupSkillLineID is 0.
|
||||
if (project->SpellID > 0)
|
||||
{
|
||||
SkillLineAbilityMapBounds const bounds = sSpellMgr->GetSkillLineAbilityMapBounds(uint32(project->SpellID));
|
||||
for (SkillLineAbilityMap::const_iterator itr = bounds.first; itr != bounds.second; ++itr)
|
||||
{
|
||||
SkillLineAbilityEntry const* ability = itr->second;
|
||||
if (!ability || ability->SkillLine != SKILL_ARCHAEOLOGY || ability->NumSkillUps <= 0)
|
||||
continue;
|
||||
|
||||
UpdateSkillPro(SKILL_ARCHAEOLOGY, 1000, uint32(ability->NumSkillUps));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Player::RecordCompletedProject(uint32 projectId)
|
||||
{
|
||||
auto history = m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::ResearchHistory);
|
||||
|
||||
// Bump the completion count if this project has been solved before.
|
||||
auto const& completed = m_activePlayerData->ResearchHistory->CompletedProjects;
|
||||
for (uint32 i = 0; i < completed.size(); ++i)
|
||||
{
|
||||
if (uint32(completed[i].ProjectID) == projectId)
|
||||
{
|
||||
auto entry = history.ModifyValue(&UF::ResearchHistory::CompletedProjects, i);
|
||||
SetUpdateFieldValue(entry.ModifyValue(&UF::CompletedProject::CompletionCount), uint32(completed[i].CompletionCount) + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto entry = AddDynamicUpdateFieldValue(history.ModifyValue(&UF::ResearchHistory::CompletedProjects));
|
||||
entry.ModifyValue(&UF::CompletedProject::ProjectID).SetValue(projectId);
|
||||
entry.ModifyValue(&UF::CompletedProject::FirstCompleted).SetValue(int64(GameTime::GetGameTime()));
|
||||
entry.ModifyValue(&UF::CompletedProject::CompletionCount).SetValue(1u);
|
||||
}
|
||||
|
||||
void Player::AdvanceResearchProject(uint32 branchId, uint32 completedProjectId)
|
||||
{
|
||||
// Drop the completed project from the active list, then roll the branch's next project.
|
||||
uint32 const count = m_activePlayerData->Research[0].size();
|
||||
for (uint32 i = 0; i < count; ++i)
|
||||
{
|
||||
if (uint32(m_activePlayerData->Research[0][i].ResearchProjectID) == completedProjectId)
|
||||
{
|
||||
RemoveDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::Research, 0).ModifyValue(), i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
EnsureResearchProject(branchId);
|
||||
}
|
||||
|
||||
void Player::_LoadResearchHistory(PreparedQueryResult result)
|
||||
{
|
||||
// Restore completed research projects into the ResearchHistory update field.
|
||||
// SELECT projectId, firstCompleted, completionCount FROM character_research_history WHERE guid = ?
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
auto history = m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::ResearchHistory);
|
||||
do
|
||||
{
|
||||
Field* fields = result->Fetch();
|
||||
uint32 projectId = fields[0].GetUInt32();
|
||||
if (!sResearchProjectStore.HasRecord(projectId))
|
||||
continue;
|
||||
|
||||
auto entry = AddDynamicUpdateFieldValue(history.ModifyValue(&UF::ResearchHistory::CompletedProjects));
|
||||
entry.ModifyValue(&UF::CompletedProject::ProjectID).SetValue(projectId);
|
||||
entry.ModifyValue(&UF::CompletedProject::FirstCompleted).SetValue(fields[1].GetInt64());
|
||||
entry.ModifyValue(&UF::CompletedProject::CompletionCount).SetValue(fields[2].GetUInt32());
|
||||
} while (result->NextRow());
|
||||
}
|
||||
|
||||
void Player::_SaveSkills(CharacterDatabaseTransaction trans)
|
||||
{
|
||||
CharacterDatabasePreparedStatement* stmt;
|
||||
@@ -27902,47 +28600,6 @@ void Player::StoreLootItem(ObjectGuid lootWorldObjectGuid, uint8 lootSlot, Loot*
|
||||
sLootItemStorage->RemoveStoredLootItemForContainer(lootWorldObjectGuid.GetCounter(), item->type, item->itemid, item->count, item->LootListId);
|
||||
}
|
||||
|
||||
void Player::_LoadResearchSites(PreparedQueryResult result)
|
||||
{
|
||||
// Restore persisted active dig sites into the ResearchSites / ResearchSiteProgress update fields.
|
||||
// SELECT researchSiteId, progress FROM character_research_site WHERE guid = ?
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
do
|
||||
{
|
||||
Field* fields = result->Fetch();
|
||||
uint16 siteId = fields[0].GetUInt16();
|
||||
uint32 progress = fields[1].GetUInt32();
|
||||
|
||||
AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::ResearchSites, 0).ModifyValue()) = siteId;
|
||||
AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::ResearchSiteProgress, 0).ModifyValue()) = progress;
|
||||
} while (result->NextRow());
|
||||
}
|
||||
|
||||
void Player::InitializeResearchSites()
|
||||
{
|
||||
// Archaeology (from the ground up) - Phase 1 Cataclysm vertical slice: seed active dig sites for
|
||||
// Eastern Kingdoms (0) and Kalimdor (1) only. Later phases extend to the other continents retail
|
||||
// assigns (Draenor/Legion/Zandalar/Kul Tiras). Per-continent active count 4 (retail 68453 sniff
|
||||
// observed 4-5 per continent).
|
||||
if (!HasSkill(SKILL_ARCHAEOLOGY))
|
||||
return;
|
||||
|
||||
// Only seed when the character has no active sites yet; persistence lands in a later sub-slice.
|
||||
if (!m_activePlayerData->ResearchSites[0].empty())
|
||||
return;
|
||||
|
||||
for (uint32 mapId : { 0u, 1u })
|
||||
{
|
||||
for (uint32 siteId : sArchaeologyMgr->RollResearchSitesForMap(mapId, 4))
|
||||
{
|
||||
AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::ResearchSites, 0).ModifyValue()) = uint16(siteId);
|
||||
AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::ResearchSiteProgress, 0).ModifyValue()) = 0u;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Player::_LoadSkills(PreparedQueryResult result)
|
||||
{
|
||||
// 0 1 2 3
|
||||
@@ -32371,6 +33028,15 @@ void Player::ExecutePendingSpellCastRequest()
|
||||
triggerFlag = TRIGGERED_FULL_MASK;
|
||||
}
|
||||
|
||||
//WowCommunity
|
||||
// The research UI casts a project's own SpellID, which is never learned into the spellbook.
|
||||
// Fail closed unless this exact solve script is enabled and the player's current state permits it;
|
||||
// otherwise the spell's CREATE_ITEM effect could run without the bookkeeping script.
|
||||
if (plrCaster->CanCastResearchProjectSpell(spellInfo->Id) &&
|
||||
sObjectMgr->HasEnabledSpellScript(spellInfo->Id, "spell_archaeology_solve"))
|
||||
allow = true;
|
||||
//WowCommunity
|
||||
|
||||
if (!allow)
|
||||
{
|
||||
CancelPendingCastRequest();
|
||||
@@ -32425,6 +33091,10 @@ void Player::ExecutePendingSpellCastRequest()
|
||||
|
||||
spell->m_fromClient = true;
|
||||
std::ranges::copy(_pendingSpellCastRequest->CastRequest.Misc, std::ranges::begin(spell->m_misc.Raw.Data));
|
||||
//WowCommunity
|
||||
if (!_pendingSpellCastRequest->CastRequest.Weight.empty())
|
||||
spell->m_customArg = std::move(_pendingSpellCastRequest->CastRequest.Weight);
|
||||
//WowCommunity
|
||||
spell->prepare(targets);
|
||||
|
||||
_pendingSpellCastRequest = nullptr;
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "DB2Structure.h"
|
||||
class MythicPlusData;
|
||||
struct SoulbindEntry;
|
||||
struct ArchaeologySolvePlan;
|
||||
//WowCommunity
|
||||
struct AccessRequirement;
|
||||
struct AchievementEntry;
|
||||
@@ -1074,6 +1075,8 @@ enum PlayerLoginQueryIndex
|
||||
PLAYER_LOGIN_QUERY_LOAD_ARENA_STATS,
|
||||
PLAYER_LOGIN_QUERY_LOAD_CHROMIE_TIME,
|
||||
PLAYER_LOGIN_QUERY_LOAD_RESEARCH_SITES,
|
||||
PLAYER_LOGIN_QUERY_LOAD_RESEARCH_PROJECTS,
|
||||
PLAYER_LOGIN_QUERY_LOAD_RESEARCH_HISTORY,
|
||||
PLAYER_LOGIN_QUERY_LOAD_CONTENT_TRACKING,
|
||||
MAX_PLAYER_LOGIN_QUERY
|
||||
};
|
||||
@@ -2609,8 +2612,48 @@ class TC_GAME_API Player final : public Unit, public GridObject<Player>
|
||||
|
||||
// Archaeology: seed active dig sites into the ResearchSites update fields on login when the
|
||||
// player knows the profession and has none yet (Phase 1 Cataclysm vertical slice).
|
||||
// Archaeology: seed active dig sites into the ResearchSites update fields on login when the
|
||||
// player knows the profession and has none yet.
|
||||
void InitializeResearchSites();
|
||||
|
||||
// Archaeology: resolve a Survey (spell 80451) cast - if standing in an active dig site, test
|
||||
// the hidden find, reveal its private lootable GameObject + advance progress on success, and
|
||||
// reply with the survey result packet.
|
||||
void HandleArchaeologySurvey();
|
||||
|
||||
// Archaeology find GameObject guard/callback used by go_archaeology_find.
|
||||
bool CanUseArchaeologyFind(GameObject const* find) const;
|
||||
void OnArchaeologyFindLooted(GameObject* find);
|
||||
|
||||
// Archaeology: swap one active dig-site slot for a fresh surveyable site on the same continent
|
||||
// (progress reset), used when a site is exhausted or found already complete.
|
||||
void ReplaceResearchSite(uint32 siteIndex, uint32 mapId);
|
||||
|
||||
// Archaeology: on login, assign a current research project for each branch the player already
|
||||
// has fragments in but no active project (new fragment gains assign on the fly).
|
||||
void InitializeResearchProjects();
|
||||
|
||||
// Archaeology: the active research project for a branch (ResearchProject.db2 ID), or 0 if none.
|
||||
int32 GetCurrentResearchProject(uint32 branchId) const;
|
||||
|
||||
// Archaeology: ensure a branch has a current project, assigning a fresh one if it has none.
|
||||
// Returns the project ID (existing or new), or 0 if the branch has no eligible projects.
|
||||
uint32 EnsureResearchProject(uint32 branchId);
|
||||
|
||||
// Archaeology: the set of completed research project IDs (from ResearchHistory), used to bias
|
||||
// new project rolls away from repeats.
|
||||
std::unordered_set<uint32> GetCompletedResearchProjects() const;
|
||||
|
||||
// Archaeology: authorize the current project's solve spell through the client-cast known-spell
|
||||
// gate. Resource validation remains in the solve script, which owns the preserved cast weights.
|
||||
bool CanCastResearchProjectSpell(uint32 spellId) const;
|
||||
|
||||
// Archaeology: revalidate mutable player state against one immutable DB2-backed solve plan,
|
||||
// consume its exact accepted resources before the reward effect, then finalize bookkeeping.
|
||||
bool CanSolveResearchProject(ArchaeologySolvePlan const& plan) const;
|
||||
bool ConsumeResearchProjectSolveResources(ArchaeologySolvePlan const& plan);
|
||||
void CompleteResearchProjectSolve(ArchaeologySolvePlan const& plan);
|
||||
|
||||
/*********************************************************/
|
||||
/*** PVP SYSTEM ***/
|
||||
/*********************************************************/
|
||||
@@ -3523,6 +3566,12 @@ class TC_GAME_API Player final : public Unit, public GridObject<Player>
|
||||
void _LoadGroup(PreparedQueryResult result);
|
||||
void _LoadSkills(PreparedQueryResult result);
|
||||
void _LoadResearchSites(PreparedQueryResult result);
|
||||
bool _EnsureResearchSiteFindLocation(uint32 researchSiteId, float& x, float& y);
|
||||
void _UpdateArchaeologySurveyIndicator();
|
||||
void _LoadResearchProjects(PreparedQueryResult result);
|
||||
void _LoadResearchHistory(PreparedQueryResult result);
|
||||
void RecordCompletedProject(uint32 projectId);
|
||||
void AdvanceResearchProject(uint32 branchId, uint32 completedProjectId);
|
||||
void _LoadSpells(PreparedQueryResult result, PreparedQueryResult favoritesResult);
|
||||
void _LoadStoredAuraTeleportLocations(PreparedQueryResult result);
|
||||
bool _LoadHomeBind(PreparedQueryResult result);
|
||||
@@ -3584,6 +3633,8 @@ class TC_GAME_API Player final : public Unit, public GridObject<Player>
|
||||
void _SaveSeasonalQuestStatus(CharacterDatabaseTransaction trans);
|
||||
void _SaveSkills(CharacterDatabaseTransaction trans);
|
||||
void _SaveResearchSites(CharacterDatabaseTransaction trans);
|
||||
void _SaveResearchProjects(CharacterDatabaseTransaction trans);
|
||||
void _SaveResearchHistory(CharacterDatabaseTransaction trans);
|
||||
void _SaveSpells(CharacterDatabaseTransaction trans);
|
||||
void _SaveStoredAuraTeleportLocations(CharacterDatabaseTransaction trans);
|
||||
void _SaveEquipmentSets(CharacterDatabaseTransaction trans);
|
||||
@@ -3862,6 +3913,14 @@ class TC_GAME_API Player final : public Unit, public GridObject<Player>
|
||||
|
||||
//WowCommunity
|
||||
std::unique_ptr<MythicPlusData> _mythicPlusData;
|
||||
struct PendingArchaeologyFind
|
||||
{
|
||||
ObjectGuid GameObjectGuid;
|
||||
uint32 ResearchSiteId = 0;
|
||||
uint32 ResearchBranchId = 0;
|
||||
};
|
||||
Optional<PendingArchaeologyFind> _pendingArchaeologyFind;
|
||||
std::unordered_map<uint32 /*researchSiteId*/, std::pair<float, float>> _researchSiteFindLocations;
|
||||
//WowCommunity
|
||||
bool _advancedCombatLoggingEnabled;
|
||||
|
||||
|
||||
@@ -8014,9 +8014,9 @@ namespace
|
||||
{ 285, "Masculla" }, { 286, "Arranca" }, { 287, "Chupa" }, { 288, "Golpea" },
|
||||
|
||||
{ 289, "tumbas" }, { 290, "huesos" }, { 291, "craneos" }, { 292, "sesos" },
|
||||
{ 293, "cadáveres" }, { 294, "costillas" }, { 295, "criptas" }, { 296, "carroña" },
|
||||
{ 297, "entrañas" }, { 298, "miembros" }, { 299, "pellejos" }, { 300, "gargantas" },
|
||||
{ 301, "tendones" }, { 302, "tuétanos" }, { 303, "mortales" }, { 304, "sepulcros" },
|
||||
{ 293, "cadaveres" }, { 294, "costillas" }, { 295, "criptas" }, { 296, "carroza" },
|
||||
{ 297, "entranas" }, { 298, "miembros" }, { 299, "pellejos" }, { 300, "gargantas" },
|
||||
{ 301, "tendones" }, { 302, "tutanos" }, { 303, "mortales" }, { 304, "sepulcros" },
|
||||
{ 305, "visceras" }, { 306, "espinas" }, { 307, "feretros" }, { 308, "carnes" },
|
||||
{ 309, "ojos" }, { 310, "almas" }, { 311, "ratas" }, { 312, "sangres" },
|
||||
{ 313, "cabezas" }
|
||||
@@ -12402,3 +12402,12 @@ void ObjectMgr::LoadQuestGarrisonFollowers()
|
||||
|
||||
TC_LOG_INFO("server.loading", ">> Loaded {} quest garrison follower rewards in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
|
||||
}
|
||||
|
||||
bool ObjectMgr::HasEnabledSpellScript(uint32 spellId, std::string_view scriptName)
|
||||
{
|
||||
auto [begin, end] = GetSpellScriptsBounds(spellId);
|
||||
return std::any_of(begin, end, [this, scriptName](SpellScriptsContainer::value_type const& script)
|
||||
{
|
||||
return script.second.second && GetScriptName(script.second.first) == scriptName;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1197,7 +1197,9 @@ class TC_GAME_API ObjectMgr
|
||||
uint32 GetAreaTriggerScriptId(uint32 trigger_id) const;
|
||||
uint32 GetEventScriptId(uint32 eventId) const;
|
||||
SpellScriptsBounds GetSpellScriptsBounds(uint32 spellId);
|
||||
|
||||
//WowCommunity
|
||||
bool HasEnabledSpellScript(uint32 spellId, std::string_view scriptName);
|
||||
//WowCommunity
|
||||
RepRewardRate const* GetRepRewardRate(uint32 factionId) const
|
||||
{
|
||||
RepRewardRateContainer::const_iterator itr = _repRewardRateStore.find(factionId);
|
||||
|
||||
@@ -419,6 +419,14 @@ bool LoginQueryHolder::Initialize()
|
||||
stmt->setUInt64(0, lowGuid);
|
||||
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_RESEARCH_SITES, stmt);
|
||||
|
||||
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_RESEARCH_PROJECT);
|
||||
stmt->setUInt64(0, lowGuid);
|
||||
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_RESEARCH_PROJECTS, stmt);
|
||||
|
||||
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_RESEARCH_HISTORY);
|
||||
stmt->setUInt64(0, lowGuid);
|
||||
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_RESEARCH_HISTORY, stmt);
|
||||
|
||||
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CONTENT_TRACKING);
|
||||
stmt->setUInt64(0, lowGuid);
|
||||
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_CONTENT_TRACKING, stmt);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "AdventureMapPackets.h"
|
||||
#include "AreaTriggerPackets.h"
|
||||
#include "ArtifactPackets.h"
|
||||
#include "ArchaeologyPackets.h"
|
||||
#include "AuctionHousePackets.h"
|
||||
#include "AuthenticationPackets.h"
|
||||
#include "AzeritePackets.h"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ArchaeologyPackets.h"
|
||||
|
||||
WorldPacket const* WorldPackets::Archaeology::SurveyCast::Write()
|
||||
{
|
||||
_worldPacket << uint32(TotalFinds);
|
||||
_worldPacket << uint32(NumFindsCompleted);
|
||||
_worldPacket << uint32(ResearchBranchID);
|
||||
_worldPacket.WriteBit(SuccessfulFind);
|
||||
_worldPacket.FlushBits();
|
||||
|
||||
return &_worldPacket;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TRINITYCORE_ARCHAEOLOGY_PACKETS_H
|
||||
#define TRINITYCORE_ARCHAEOLOGY_PACKETS_H
|
||||
|
||||
#include "Packet.h"
|
||||
|
||||
namespace WorldPackets
|
||||
{
|
||||
namespace Archaeology
|
||||
{
|
||||
// Result of an Archaeology Survey cast: how many finds the current dig site has yielded and
|
||||
// needs, which research branch it feeds, and whether this cast revealed a find.
|
||||
class SurveyCast final : public ServerPacket
|
||||
{
|
||||
public:
|
||||
explicit SurveyCast() : ServerPacket(SMSG_ARCHAEOLOGY_SURVERY_CAST, 4 + 4 + 4 + 1) { }
|
||||
|
||||
WorldPacket const* Write() override;
|
||||
|
||||
uint32 TotalFinds = 0;
|
||||
uint32 NumFindsCompleted = 0;
|
||||
uint32 ResearchBranchID = 0;
|
||||
bool SuccessfulFind = false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endif // TRINITYCORE_ARCHAEOLOGY_PACKETS_H
|
||||
@@ -1228,7 +1228,7 @@ void OpcodeTable::InitializeServerOpcodes()
|
||||
DEFINE_SERVER_OPCODE_HANDLER(SMSG_ALL_ACHIEVEMENT_DATA, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
|
||||
DEFINE_SERVER_OPCODE_HANDLER(SMSG_ALL_GUILD_ACHIEVEMENTS, STATUS_NEVER, CONNECTION_TYPE_REALM);
|
||||
DEFINE_SERVER_OPCODE_HANDLER(SMSG_APPLY_MOUNT_EQUIPMENT_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
|
||||
DEFINE_SERVER_OPCODE_HANDLER(SMSG_ARCHAEOLOGY_SURVERY_CAST, STATUS_UNHANDLED, CONNECTION_TYPE_REALM);
|
||||
DEFINE_SERVER_OPCODE_HANDLER(SMSG_ARCHAEOLOGY_SURVERY_CAST, STATUS_NEVER, CONNECTION_TYPE_REALM);
|
||||
DEFINE_SERVER_OPCODE_HANDLER(SMSG_AREA_POI_UPDATE_RESPONSE, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
|
||||
DEFINE_SERVER_OPCODE_HANDLER(SMSG_AREA_SPIRIT_HEALER_TIME, STATUS_NEVER, CONNECTION_TYPE_REALM);
|
||||
DEFINE_SERVER_OPCODE_HANDLER(SMSG_AREA_TRIGGER_DENIED, STATUS_NEVER, CONNECTION_TYPE_REALM);
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
#define SpellCastRequest_h__
|
||||
|
||||
#include "SpellPackets.h"
|
||||
//WowCommunity
|
||||
#include <utility>
|
||||
//WowCommunity
|
||||
|
||||
struct SpellCastRequestItemData
|
||||
{
|
||||
@@ -33,7 +36,7 @@ struct SpellCastRequestItemData
|
||||
struct SpellCastRequest
|
||||
{
|
||||
SpellCastRequest(WorldPackets::Spells::SpellCastRequest&& castRequest, ObjectGuid castingUnitGUID, Optional<SpellCastRequestItemData> itemData = {}) :
|
||||
CastRequest(castRequest), CastingUnitGUID(castingUnitGUID), ItemData(itemData) { }
|
||||
CastRequest(std::move(castRequest)), CastingUnitGUID(castingUnitGUID), ItemData(itemData) {}
|
||||
|
||||
WorldPackets::Spells::SpellCastRequest CastRequest;
|
||||
ObjectGuid CastingUnitGUID;
|
||||
|
||||
@@ -2017,8 +2017,10 @@ bool World::SetInitialWorldSettings()
|
||||
sItemUpgradeMgr.Initialize();
|
||||
|
||||
TC_LOG_INFO("server.loading", "Loading Archaeology research data...");
|
||||
sArchaeologyMgr->LoadResearchSites();
|
||||
sArchaeologyMgr->LoadDigSiteData();
|
||||
sArchaeologyMgr->LoadResearchSites(); // must be after DB2 stores load
|
||||
sArchaeologyMgr->LoadDigSiteData(); // must be after LoadResearchSites
|
||||
sArchaeologyMgr->LoadResearchBranchData(); // must be after gameobject templates load
|
||||
sArchaeologyMgr->LoadDigSitePoints(); // must be after LoadDigSiteData
|
||||
|
||||
TC_LOG_INFO("server.loading", "Loading Treasure Pickers...");
|
||||
sObjectMgr->LoadTreasurePickerTemplates(); // must be after LoadItemTemplates()
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ArchaeologyMgr.h"
|
||||
#include "GameObject.h"
|
||||
#include "GameObjectAI.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "Player.h"
|
||||
#include "ScriptMgr.h"
|
||||
#include "SpellPackets.h"
|
||||
#include "SpellScript.h"
|
||||
#include <any>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
struct go_archaeology_find : public GameObjectAI
|
||||
{
|
||||
go_archaeology_find(GameObject* gameObject) : GameObjectAI(gameObject) { }
|
||||
|
||||
bool OnGossipHello(Player* player) override
|
||||
{
|
||||
// Private-object visibility is the first boundary; retain an authoritative owner/token check
|
||||
// in case a stale or forced-visible object is used.
|
||||
return !player->CanUseArchaeologyFind(me);
|
||||
}
|
||||
|
||||
void OnLootStateChanged(uint32 state, Unit* /*unit*/) override
|
||||
{
|
||||
if (state != GO_JUST_DEACTIVATED)
|
||||
return;
|
||||
|
||||
if (Player* owner = ObjectAccessor::GetPlayer(*me, me->GetOwnerGUID()))
|
||||
owner->OnArchaeologyFindLooted(me);
|
||||
}
|
||||
};
|
||||
|
||||
// 80451 - Survey
|
||||
// Archaeology survey: the profession loop lives in Player::HandleArchaeologySurvey; this hook just
|
||||
// runs it after the survey cast completes for a player caster.
|
||||
class spell_archaeology_survey : public SpellScript
|
||||
{
|
||||
void HandleAfterCast()
|
||||
{
|
||||
if (Player* player = GetCaster()->ToPlayer())
|
||||
player->HandleArchaeologySurvey();
|
||||
}
|
||||
|
||||
void Register() override
|
||||
{
|
||||
AfterCast += SpellCastFn(spell_archaeology_survey::HandleAfterCast);
|
||||
}
|
||||
};
|
||||
|
||||
// Research project solve spells (one per ResearchProject.db2 entry, bound via spell_script_names)
|
||||
// Solving = casting the project's own SpellID. The spell's own effect creates the reward item; this
|
||||
// script validates one DB2-backed resource plan, commits exactly that plan before effects, then
|
||||
// records the completion and rolls the branch's next project once.
|
||||
class spell_archaeology_solve : public SpellScript
|
||||
{
|
||||
std::optional<ArchaeologySolvePlan> _solvePlan;
|
||||
bool _resourcesConsumed = false;
|
||||
bool _completed = false;
|
||||
|
||||
SpellCastResult CheckCast()
|
||||
{
|
||||
Player* player = GetCaster()->ToPlayer();
|
||||
if (!player)
|
||||
return SPELL_FAILED_DONT_REPORT;
|
||||
|
||||
if (!_solvePlan)
|
||||
{
|
||||
std::vector<WorldPackets::Spells::SpellWeight> const* weights =
|
||||
std::any_cast<std::vector<WorldPackets::Spells::SpellWeight>>(&GetSpell()->m_customArg);
|
||||
if (!weights)
|
||||
return SPELL_FAILED_DONT_REPORT;
|
||||
|
||||
_solvePlan = sArchaeologyMgr->BuildSolvePlan(GetSpellInfo()->Id, *weights);
|
||||
if (!_solvePlan)
|
||||
return SPELL_FAILED_DONT_REPORT;
|
||||
}
|
||||
|
||||
return player->CanSolveResearchProject(*_solvePlan) ? SPELL_CAST_OK : SPELL_FAILED_DONT_REPORT;
|
||||
}
|
||||
|
||||
void HandleOnCast()
|
||||
{
|
||||
if (_solvePlan)
|
||||
if (Player* player = GetCaster()->ToPlayer())
|
||||
_resourcesConsumed = player->ConsumeResearchProjectSolveResources(*_solvePlan);
|
||||
}
|
||||
|
||||
void GuardEffects(SpellEffIndex effectIndex)
|
||||
{
|
||||
if (!_resourcesConsumed)
|
||||
PreventHitDefaultEffect(effectIndex);
|
||||
}
|
||||
|
||||
void HandleAfterCast()
|
||||
{
|
||||
if (!_resourcesConsumed || _completed || !_solvePlan)
|
||||
return;
|
||||
|
||||
if (Player* player = GetCaster()->ToPlayer())
|
||||
{
|
||||
player->CompleteResearchProjectSolve(*_solvePlan);
|
||||
_completed = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Register() override
|
||||
{
|
||||
OnCheckCast += SpellCheckCastFn(spell_archaeology_solve::CheckCast);
|
||||
OnCast += SpellCastFn(spell_archaeology_solve::HandleOnCast);
|
||||
OnEffectHitTarget += SpellEffectFn(spell_archaeology_solve::GuardEffects, EFFECT_ALL, SPELL_EFFECT_ANY);
|
||||
AfterCast += SpellCastFn(spell_archaeology_solve::HandleAfterCast);
|
||||
}
|
||||
};
|
||||
|
||||
void AddSC_archaeology_spell_scripts()
|
||||
{
|
||||
RegisterGameObjectAI(go_archaeology_find);
|
||||
RegisterSpellScript(spell_archaeology_survey);
|
||||
RegisterSpellScript(spell_archaeology_solve);
|
||||
}
|
||||
@@ -36,6 +36,7 @@ void AddSC_azerite_item_spell_scripts();
|
||||
void AddSC_housing_spell_scripts();
|
||||
void AddSC_dragonriding_spell_scripts();
|
||||
void AddSC_spell_delve_dread_pit();
|
||||
void AddSC_archaeology_spell_scripts();
|
||||
|
||||
// The name of this function should match:
|
||||
// void Add${NameOfDirectory}Scripts()
|
||||
@@ -61,4 +62,5 @@ void AddSpellsScripts()
|
||||
AddSC_housing_spell_scripts();
|
||||
AddSC_dragonriding_spell_scripts();
|
||||
AddSC_spell_delve_dread_pit();
|
||||
AddSC_archaeology_spell_scripts();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user