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_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_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_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_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);
|
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_SEL_CHARACTER_RESEARCH_SITE,
|
||||||
CHAR_DEL_CHARACTER_RESEARCH_SITE,
|
CHAR_DEL_CHARACTER_RESEARCH_SITE,
|
||||||
CHAR_INS_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_SEL_WEEKLY_REWARD_ACTIVITY,
|
||||||
CHAR_REP_WEEKLY_REWARD_ACTIVITY,
|
CHAR_REP_WEEKLY_REWARD_ACTIVITY,
|
||||||
|
|||||||
@@ -19,8 +19,63 @@
|
|||||||
#include "Containers.h"
|
#include "Containers.h"
|
||||||
#include "DatabaseEnv.h"
|
#include "DatabaseEnv.h"
|
||||||
#include "DB2Stores.h"
|
#include "DB2Stores.h"
|
||||||
|
#include "GameObjectData.h"
|
||||||
|
#include "GridDefines.h"
|
||||||
#include "Log.h"
|
#include "Log.h"
|
||||||
|
#include "Map.h"
|
||||||
|
#include "ObjectMgr.h"
|
||||||
|
#include "Random.h"
|
||||||
|
#include "SpellPackets.h"
|
||||||
#include "Timer.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;
|
||||||
ArchaeologyMgr::~ArchaeologyMgr() = default;
|
ArchaeologyMgr::~ArchaeologyMgr() = default;
|
||||||
@@ -79,7 +134,7 @@ void ArchaeologyMgr::LoadDigSiteData()
|
|||||||
|
|
||||||
if (!sResearchSiteStore.HasRecord(siteId))
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,13 +147,165 @@ void ArchaeologyMgr::LoadDigSiteData()
|
|||||||
TC_LOG_INFO("server.loading", ">> Loaded {} archaeology dig-site branch mappings in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
|
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
|
ArchaeologyDigSiteInfo const* ArchaeologyMgr::GetDigSiteInfo(uint32 researchSiteId) const
|
||||||
{
|
{
|
||||||
auto itr = _digSiteInfo.find(researchSiteId);
|
auto itr = _digSiteInfo.find(researchSiteId);
|
||||||
return itr != _digSiteInfo.end() ? &itr->second : nullptr;
|
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;
|
std::vector<uint32> result;
|
||||||
|
|
||||||
@@ -107,6 +314,15 @@ std::vector<uint32> ArchaeologyMgr::RollResearchSitesForMap(uint32 mapId, uint32
|
|||||||
return result;
|
return result;
|
||||||
|
|
||||||
std::vector<ResearchSiteEntry const*> picks = *pool;
|
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)
|
if (picks.size() > count)
|
||||||
Trinity::Containers::RandomResize(picks, count);
|
Trinity::Containers::RandomResize(picks, count);
|
||||||
|
|
||||||
@@ -116,3 +332,153 @@ std::vector<uint32> ArchaeologyMgr::RollResearchSitesForMap(uint32 mapId, uint32
|
|||||||
|
|
||||||
return result;
|
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
|
#define TRINITY_ARCHAEOLOGYMGR_H
|
||||||
|
|
||||||
#include "Define.h"
|
#include "Define.h"
|
||||||
|
#include <optional>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
struct ResearchProjectEntry;
|
||||||
struct ResearchSiteEntry;
|
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.
|
// 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 site->branch theming is not carried by client DB2, so it is server-owned reference data
|
||||||
// the world table `archaeology_dig_site` (seeded from the Archy addon join). See
|
// loaded from the world table `archaeology_dig_site`.
|
||||||
// docs/midnight-assessment/archaeology/archaeology-phase1-foundation-handoff.md.
|
|
||||||
struct ArchaeologyDigSiteInfo
|
struct ArchaeologyDigSiteInfo
|
||||||
{
|
{
|
||||||
uint32 BranchID = 0;
|
uint32 BranchID = 0;
|
||||||
uint8 FindCount = 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
|
// 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,
|
// 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
|
class TC_GAME_API ArchaeologyMgr
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
@@ -58,19 +82,64 @@ public:
|
|||||||
// (needs sResearchSiteStore) and once the world DB is available.
|
// (needs sResearchSiteStore) and once the world DB is available.
|
||||||
void LoadDigSiteData();
|
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.
|
// Dig-site pool for a continent/map, or nullptr if the map has none.
|
||||||
std::vector<ResearchSiteEntry const*> const* GetResearchSitesForMap(uint32 mapId) const;
|
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
|
// 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.
|
// smaller), excluding IDs already active for the player. Empty if the map has no dig sites.
|
||||||
std::vector<uint32> RollResearchSitesForMap(uint32 mapId, uint32 count) const;
|
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.
|
// Branch/find-count for a dig site, or nullptr if the site has no mapping.
|
||||||
ArchaeologyDigSiteInfo const* GetDigSiteInfo(uint32 researchSiteId) const;
|
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:
|
private:
|
||||||
|
friend struct ArchaeologyMgrTestAccess;
|
||||||
|
|
||||||
std::unordered_map<uint32 /*mapId*/, std::vector<ResearchSiteEntry const*>> _researchSitesByMap;
|
std::unordered_map<uint32 /*mapId*/, std::vector<ResearchSiteEntry const*>> _researchSitesByMap;
|
||||||
std::unordered_map<uint32 /*researchSiteId*/, ArchaeologyDigSiteInfo> _digSiteInfo;
|
std::unordered_map<uint32 /*researchSiteId*/, ArchaeologyDigSiteInfo> _digSiteInfo;
|
||||||
|
std::unordered_map<uint32 /*researchBranchId*/, uint32 /*findGameObjectId*/> _findGameObjectsByBranch;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define sArchaeologyMgr ArchaeologyMgr::instance()
|
#define sArchaeologyMgr ArchaeologyMgr::instance()
|
||||||
|
|||||||
@@ -6629,7 +6629,6 @@ struct BountySetEntry
|
|||||||
int32 LockedQuestID;
|
int32 LockedQuestID;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
struct ResearchBranchEntry
|
struct ResearchBranchEntry
|
||||||
{
|
{
|
||||||
uint32 ID;
|
uint32 ID;
|
||||||
|
|||||||
@@ -547,10 +547,10 @@ enum class CriteriaType : int16
|
|||||||
KillCreature = 0, // Kill NPC "{Creature}"
|
KillCreature = 0, // Kill NPC "{Creature}"
|
||||||
WinBattleground = 1, // Win battleground "{Map}"
|
WinBattleground = 1, // Win battleground "{Map}"
|
||||||
CompleteResearchProject = 2, /*NYI*/ // Complete research project "{ResearchProject}"
|
CompleteResearchProject = 2, /*NYI*/ // Complete research project "{ResearchProject}"
|
||||||
CompleteAnyResearchProject = 3, /*NYI*/ // Complete any research project
|
CompleteAnyResearchProject = 3, // Complete any research project
|
||||||
FindResearchObject = 4, /*NYI*/ // Find research object "{GameObjects}"
|
FindResearchObject = 4, // Find research object "{GameObjects}"
|
||||||
ReachLevel = 5, // Reach level
|
ReachLevel = 5, // Reach level
|
||||||
ExhaustAnyResearchSite = 6, /*NYI*/ // Exhaust any research site
|
ExhaustAnyResearchSite = 6, // Exhaust any research site
|
||||||
SkillRaised = 7, // Skill "{SkillLine}" raised
|
SkillRaised = 7, // Skill "{SkillLine}" raised
|
||||||
EarnAchievement = 8, // Earn achievement "{Achievement}"
|
EarnAchievement = 8, // Earn achievement "{Achievement}"
|
||||||
CompleteQuestsCount = 9, // Count of complete quests (quest count)
|
CompleteQuestsCount = 9, // Count of complete quests (quest count)
|
||||||
|
|||||||
@@ -1451,7 +1451,7 @@ TempSummon* WorldObject::SummonPersonalClone(Position const& pos, TempSummonType
|
|||||||
return nullptr;
|
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())
|
if (!IsInWorld())
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -1469,7 +1469,10 @@ GameObject* WorldObject::SummonGameObject(uint32 entry, Position const& pos, Qua
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
PhasingHandler::InheritPhaseShift(go, this);
|
PhasingHandler::InheritPhaseShift(go, this);
|
||||||
|
//WowCommunity
|
||||||
|
if (!privateObjectOwner.IsEmpty())
|
||||||
|
go->SetPrivateObjectOwner(privateObjectOwner);
|
||||||
|
//WowCommunity
|
||||||
go->SetRespawnTime(respawnTime.count());
|
go->SetRespawnTime(respawnTime.count());
|
||||||
if (GetTypeId() == TYPEID_PLAYER || (GetTypeId() == TYPEID_UNIT && summonType == GO_SUMMON_TIMED_OR_CORPSE_DESPAWN)) //not sure how to handle this
|
if (GetTypeId() == TYPEID_PLAYER || (GetTypeId() == TYPEID_UNIT && summonType == GO_SUMMON_TIMED_OR_CORPSE_DESPAWN)) //not sure how to handle this
|
||||||
ToUnit()->AddGameObject(go);
|
ToUnit()->AddGameObject(go);
|
||||||
@@ -1480,7 +1483,7 @@ GameObject* WorldObject::SummonGameObject(uint32 entry, Position const& pos, Qua
|
|||||||
return go;
|
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)
|
if (!x && !y && !z)
|
||||||
{
|
{
|
||||||
@@ -1489,7 +1492,7 @@ GameObject* WorldObject::SummonGameObject(uint32 entry, float x, float y, float
|
|||||||
}
|
}
|
||||||
|
|
||||||
Position pos(x, y, z, ang);
|
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*))
|
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, 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* 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);
|
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, 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);
|
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);
|
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);
|
void SummonCreatureGroup(uint8 group, std::list<TempSummon*>* list = nullptr);
|
||||||
|
|
||||||
|
|||||||
@@ -158,15 +158,19 @@
|
|||||||
#include "DelvesDefines.h"
|
#include "DelvesDefines.h"
|
||||||
#include "DelvesRewards.h"
|
#include "DelvesRewards.h"
|
||||||
#include "GarrisonPackets.h"
|
#include "GarrisonPackets.h"
|
||||||
|
#include "GridDefines.h"
|
||||||
#include "HousingDynamicEntity.h"
|
#include "HousingDynamicEntity.h"
|
||||||
#include "MythicPlusPacketsCommon.h"
|
#include "MythicPlusPacketsCommon.h"
|
||||||
#include "ArchaeologyMgr.h"
|
#include "ArchaeologyMgr.h"
|
||||||
|
#include "ArchaeologyPackets.h"
|
||||||
#include "ElapsedTimerMgr.h"
|
#include "ElapsedTimerMgr.h"
|
||||||
#include "PreyMgr.h"
|
#include "PreyMgr.h"
|
||||||
#include "OmniumFolioMgr.h"
|
#include "OmniumFolioMgr.h"
|
||||||
#include "CovenantPackets.h"
|
#include "CovenantPackets.h"
|
||||||
#include "WeeklyRewardsMgr.h"
|
#include "WeeklyRewardsMgr.h"
|
||||||
#include <LFGPackets.h>
|
#include "QuaternionData.h"
|
||||||
|
#include "LFGPackets.h"
|
||||||
|
#include <cmath>
|
||||||
//WowCommunity
|
//WowCommunity
|
||||||
|
|
||||||
// corpse reclaim times
|
// corpse reclaim times
|
||||||
@@ -176,6 +180,7 @@
|
|||||||
enum PlayerSpells
|
enum PlayerSpells
|
||||||
{
|
{
|
||||||
SPELL_EXPERIENCE_ELIMINATED = 206662,
|
SPELL_EXPERIENCE_ELIMINATED = 206662,
|
||||||
|
SPELL_ARCHAEOLOGY_STANDING_ON_IT = 210837,
|
||||||
};
|
};
|
||||||
|
|
||||||
static uint32 corpseReclaimDelay[MAX_DEATH_COUNT] = { 30, 60, 120 };
|
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);
|
stmt->setUInt64(0, guid);
|
||||||
trans->Append(stmt);
|
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 = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_STATS);
|
||||||
stmt->setUInt64(0, guid);
|
stmt->setUInt64(0, guid);
|
||||||
trans->Append(stmt);
|
trans->Append(stmt);
|
||||||
@@ -4249,9 +4268,6 @@ void Player::DeleteFromDB(ObjectGuid playerguid, uint32 accountId, bool updateRe
|
|||||||
stmt->setUInt64(0, guid);
|
stmt->setUInt64(0, guid);
|
||||||
trans->Append(stmt);
|
trans->Append(stmt);
|
||||||
|
|
||||||
stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_RESEARCH_SITE);
|
|
||||||
stmt->setUInt64(0, guid);
|
|
||||||
trans->Append(stmt);
|
|
||||||
|
|
||||||
sCharacterCache->DeleteCharacterCacheEntry(playerguid, name);
|
sCharacterCache->DeleteCharacterCacheEntry(playerguid, name);
|
||||||
break;
|
break;
|
||||||
@@ -6254,6 +6270,9 @@ bool Player::UpdatePosition(float x, float y, float z, float orientation, bool t
|
|||||||
SetGroupUpdateFlag(GROUP_UPDATE_FLAG_POSITION);
|
SetGroupUpdateFlag(GROUP_UPDATE_FLAG_POSITION);
|
||||||
|
|
||||||
CheckAreaExplore();
|
CheckAreaExplore();
|
||||||
|
//WowCommunity
|
||||||
|
_UpdateArchaeologySurveyIndicator();
|
||||||
|
//WowCommunity
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -19059,7 +19078,10 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
|
|||||||
|
|
||||||
//WowCommunity
|
//WowCommunity
|
||||||
_LoadResearchSites(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_RESEARCH_SITES)); // Archaeology: restore persisted dig sites
|
_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
|
//WowCommunity
|
||||||
|
|
||||||
SetNumRespecs(fields.numRespecs);
|
SetNumRespecs(fields.numRespecs);
|
||||||
@@ -21278,6 +21300,8 @@ void Player::SaveToDB(LoginDatabaseTransaction loginTransaction, CharacterDataba
|
|||||||
_SaveAuras(trans);
|
_SaveAuras(trans);
|
||||||
_SaveSkills(trans);
|
_SaveSkills(trans);
|
||||||
_SaveResearchSites(trans);
|
_SaveResearchSites(trans);
|
||||||
|
_SaveResearchProjects(trans);
|
||||||
|
_SaveResearchHistory(trans);
|
||||||
_SaveStoredAuraTeleportLocations(trans);
|
_SaveStoredAuraTeleportLocations(trans);
|
||||||
m_achievementMgr->SaveToDB(trans);
|
m_achievementMgr->SaveToDB(trans);
|
||||||
m_reputationMgr->SaveToDB(trans);
|
m_reputationMgr->SaveToDB(trans);
|
||||||
@@ -21998,14 +22022,688 @@ void Player::_SaveResearchSites(CharacterDatabaseTransaction trans)
|
|||||||
uint32 const count = m_activePlayerData->ResearchSites[0].size();
|
uint32 const count = m_activePlayerData->ResearchSites[0].size();
|
||||||
for (uint32 i = 0; i < count; ++i)
|
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 = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_RESEARCH_SITE);
|
||||||
stmt->setUInt64(0, GetGUID().GetCounter());
|
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->setUInt32(2, m_activePlayerData->ResearchSiteProgress[0][i]);
|
||||||
|
stmt->setFloat(3, findX);
|
||||||
|
stmt->setFloat(4, findY);
|
||||||
trans->Append(stmt);
|
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)
|
void Player::_SaveSkills(CharacterDatabaseTransaction trans)
|
||||||
{
|
{
|
||||||
CharacterDatabasePreparedStatement* stmt;
|
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);
|
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)
|
void Player::_LoadSkills(PreparedQueryResult result)
|
||||||
{
|
{
|
||||||
// 0 1 2 3
|
// 0 1 2 3
|
||||||
@@ -32371,6 +33028,15 @@ void Player::ExecutePendingSpellCastRequest()
|
|||||||
triggerFlag = TRIGGERED_FULL_MASK;
|
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)
|
if (!allow)
|
||||||
{
|
{
|
||||||
CancelPendingCastRequest();
|
CancelPendingCastRequest();
|
||||||
@@ -32425,6 +33091,10 @@ void Player::ExecutePendingSpellCastRequest()
|
|||||||
|
|
||||||
spell->m_fromClient = true;
|
spell->m_fromClient = true;
|
||||||
std::ranges::copy(_pendingSpellCastRequest->CastRequest.Misc, std::ranges::begin(spell->m_misc.Raw.Data));
|
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);
|
spell->prepare(targets);
|
||||||
|
|
||||||
_pendingSpellCastRequest = nullptr;
|
_pendingSpellCastRequest = nullptr;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
#include "DB2Structure.h"
|
#include "DB2Structure.h"
|
||||||
class MythicPlusData;
|
class MythicPlusData;
|
||||||
struct SoulbindEntry;
|
struct SoulbindEntry;
|
||||||
|
struct ArchaeologySolvePlan;
|
||||||
//WowCommunity
|
//WowCommunity
|
||||||
struct AccessRequirement;
|
struct AccessRequirement;
|
||||||
struct AchievementEntry;
|
struct AchievementEntry;
|
||||||
@@ -1074,6 +1075,8 @@ enum PlayerLoginQueryIndex
|
|||||||
PLAYER_LOGIN_QUERY_LOAD_ARENA_STATS,
|
PLAYER_LOGIN_QUERY_LOAD_ARENA_STATS,
|
||||||
PLAYER_LOGIN_QUERY_LOAD_CHROMIE_TIME,
|
PLAYER_LOGIN_QUERY_LOAD_CHROMIE_TIME,
|
||||||
PLAYER_LOGIN_QUERY_LOAD_RESEARCH_SITES,
|
PLAYER_LOGIN_QUERY_LOAD_RESEARCH_SITES,
|
||||||
|
PLAYER_LOGIN_QUERY_LOAD_RESEARCH_PROJECTS,
|
||||||
|
PLAYER_LOGIN_QUERY_LOAD_RESEARCH_HISTORY,
|
||||||
PLAYER_LOGIN_QUERY_LOAD_CONTENT_TRACKING,
|
PLAYER_LOGIN_QUERY_LOAD_CONTENT_TRACKING,
|
||||||
MAX_PLAYER_LOGIN_QUERY
|
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
|
// 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).
|
// 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();
|
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 ***/
|
/*** PVP SYSTEM ***/
|
||||||
/*********************************************************/
|
/*********************************************************/
|
||||||
@@ -3523,6 +3566,12 @@ class TC_GAME_API Player final : public Unit, public GridObject<Player>
|
|||||||
void _LoadGroup(PreparedQueryResult result);
|
void _LoadGroup(PreparedQueryResult result);
|
||||||
void _LoadSkills(PreparedQueryResult result);
|
void _LoadSkills(PreparedQueryResult result);
|
||||||
void _LoadResearchSites(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 _LoadSpells(PreparedQueryResult result, PreparedQueryResult favoritesResult);
|
||||||
void _LoadStoredAuraTeleportLocations(PreparedQueryResult result);
|
void _LoadStoredAuraTeleportLocations(PreparedQueryResult result);
|
||||||
bool _LoadHomeBind(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 _SaveSeasonalQuestStatus(CharacterDatabaseTransaction trans);
|
||||||
void _SaveSkills(CharacterDatabaseTransaction trans);
|
void _SaveSkills(CharacterDatabaseTransaction trans);
|
||||||
void _SaveResearchSites(CharacterDatabaseTransaction trans);
|
void _SaveResearchSites(CharacterDatabaseTransaction trans);
|
||||||
|
void _SaveResearchProjects(CharacterDatabaseTransaction trans);
|
||||||
|
void _SaveResearchHistory(CharacterDatabaseTransaction trans);
|
||||||
void _SaveSpells(CharacterDatabaseTransaction trans);
|
void _SaveSpells(CharacterDatabaseTransaction trans);
|
||||||
void _SaveStoredAuraTeleportLocations(CharacterDatabaseTransaction trans);
|
void _SaveStoredAuraTeleportLocations(CharacterDatabaseTransaction trans);
|
||||||
void _SaveEquipmentSets(CharacterDatabaseTransaction trans);
|
void _SaveEquipmentSets(CharacterDatabaseTransaction trans);
|
||||||
@@ -3862,6 +3913,14 @@ class TC_GAME_API Player final : public Unit, public GridObject<Player>
|
|||||||
|
|
||||||
//WowCommunity
|
//WowCommunity
|
||||||
std::unique_ptr<MythicPlusData> _mythicPlusData;
|
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
|
//WowCommunity
|
||||||
bool _advancedCombatLoggingEnabled;
|
bool _advancedCombatLoggingEnabled;
|
||||||
|
|
||||||
|
|||||||
@@ -8014,9 +8014,9 @@ namespace
|
|||||||
{ 285, "Masculla" }, { 286, "Arranca" }, { 287, "Chupa" }, { 288, "Golpea" },
|
{ 285, "Masculla" }, { 286, "Arranca" }, { 287, "Chupa" }, { 288, "Golpea" },
|
||||||
|
|
||||||
{ 289, "tumbas" }, { 290, "huesos" }, { 291, "craneos" }, { 292, "sesos" },
|
{ 289, "tumbas" }, { 290, "huesos" }, { 291, "craneos" }, { 292, "sesos" },
|
||||||
{ 293, "cadáveres" }, { 294, "costillas" }, { 295, "criptas" }, { 296, "carroña" },
|
{ 293, "cadaveres" }, { 294, "costillas" }, { 295, "criptas" }, { 296, "carroza" },
|
||||||
{ 297, "entrañas" }, { 298, "miembros" }, { 299, "pellejos" }, { 300, "gargantas" },
|
{ 297, "entranas" }, { 298, "miembros" }, { 299, "pellejos" }, { 300, "gargantas" },
|
||||||
{ 301, "tendones" }, { 302, "tuétanos" }, { 303, "mortales" }, { 304, "sepulcros" },
|
{ 301, "tendones" }, { 302, "tutanos" }, { 303, "mortales" }, { 304, "sepulcros" },
|
||||||
{ 305, "visceras" }, { 306, "espinas" }, { 307, "feretros" }, { 308, "carnes" },
|
{ 305, "visceras" }, { 306, "espinas" }, { 307, "feretros" }, { 308, "carnes" },
|
||||||
{ 309, "ojos" }, { 310, "almas" }, { 311, "ratas" }, { 312, "sangres" },
|
{ 309, "ojos" }, { 310, "almas" }, { 311, "ratas" }, { 312, "sangres" },
|
||||||
{ 313, "cabezas" }
|
{ 313, "cabezas" }
|
||||||
@@ -12402,3 +12402,12 @@ void ObjectMgr::LoadQuestGarrisonFollowers()
|
|||||||
|
|
||||||
TC_LOG_INFO("server.loading", ">> Loaded {} quest garrison follower rewards in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
|
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 GetAreaTriggerScriptId(uint32 trigger_id) const;
|
||||||
uint32 GetEventScriptId(uint32 eventId) const;
|
uint32 GetEventScriptId(uint32 eventId) const;
|
||||||
SpellScriptsBounds GetSpellScriptsBounds(uint32 spellId);
|
SpellScriptsBounds GetSpellScriptsBounds(uint32 spellId);
|
||||||
|
//WowCommunity
|
||||||
|
bool HasEnabledSpellScript(uint32 spellId, std::string_view scriptName);
|
||||||
|
//WowCommunity
|
||||||
RepRewardRate const* GetRepRewardRate(uint32 factionId) const
|
RepRewardRate const* GetRepRewardRate(uint32 factionId) const
|
||||||
{
|
{
|
||||||
RepRewardRateContainer::const_iterator itr = _repRewardRateStore.find(factionId);
|
RepRewardRateContainer::const_iterator itr = _repRewardRateStore.find(factionId);
|
||||||
|
|||||||
@@ -419,6 +419,14 @@ bool LoginQueryHolder::Initialize()
|
|||||||
stmt->setUInt64(0, lowGuid);
|
stmt->setUInt64(0, lowGuid);
|
||||||
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_RESEARCH_SITES, stmt);
|
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 = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CONTENT_TRACKING);
|
||||||
stmt->setUInt64(0, lowGuid);
|
stmt->setUInt64(0, lowGuid);
|
||||||
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_CONTENT_TRACKING, stmt);
|
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_CONTENT_TRACKING, stmt);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include "AdventureMapPackets.h"
|
#include "AdventureMapPackets.h"
|
||||||
#include "AreaTriggerPackets.h"
|
#include "AreaTriggerPackets.h"
|
||||||
#include "ArtifactPackets.h"
|
#include "ArtifactPackets.h"
|
||||||
|
#include "ArchaeologyPackets.h"
|
||||||
#include "AuctionHousePackets.h"
|
#include "AuctionHousePackets.h"
|
||||||
#include "AuthenticationPackets.h"
|
#include "AuthenticationPackets.h"
|
||||||
#include "AzeritePackets.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_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_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_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_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_SPIRIT_HEALER_TIME, STATUS_NEVER, CONNECTION_TYPE_REALM);
|
||||||
DEFINE_SERVER_OPCODE_HANDLER(SMSG_AREA_TRIGGER_DENIED, 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__
|
#define SpellCastRequest_h__
|
||||||
|
|
||||||
#include "SpellPackets.h"
|
#include "SpellPackets.h"
|
||||||
|
//WowCommunity
|
||||||
|
#include <utility>
|
||||||
|
//WowCommunity
|
||||||
|
|
||||||
struct SpellCastRequestItemData
|
struct SpellCastRequestItemData
|
||||||
{
|
{
|
||||||
@@ -33,7 +36,7 @@ struct SpellCastRequestItemData
|
|||||||
struct SpellCastRequest
|
struct SpellCastRequest
|
||||||
{
|
{
|
||||||
SpellCastRequest(WorldPackets::Spells::SpellCastRequest&& castRequest, ObjectGuid castingUnitGUID, Optional<SpellCastRequestItemData> itemData = {}) :
|
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;
|
WorldPackets::Spells::SpellCastRequest CastRequest;
|
||||||
ObjectGuid CastingUnitGUID;
|
ObjectGuid CastingUnitGUID;
|
||||||
|
|||||||
@@ -2017,8 +2017,10 @@ bool World::SetInitialWorldSettings()
|
|||||||
sItemUpgradeMgr.Initialize();
|
sItemUpgradeMgr.Initialize();
|
||||||
|
|
||||||
TC_LOG_INFO("server.loading", "Loading Archaeology research data...");
|
TC_LOG_INFO("server.loading", "Loading Archaeology research data...");
|
||||||
sArchaeologyMgr->LoadResearchSites();
|
sArchaeologyMgr->LoadResearchSites(); // must be after DB2 stores load
|
||||||
sArchaeologyMgr->LoadDigSiteData();
|
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...");
|
TC_LOG_INFO("server.loading", "Loading Treasure Pickers...");
|
||||||
sObjectMgr->LoadTreasurePickerTemplates(); // must be after LoadItemTemplates()
|
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_housing_spell_scripts();
|
||||||
void AddSC_dragonriding_spell_scripts();
|
void AddSC_dragonriding_spell_scripts();
|
||||||
void AddSC_spell_delve_dread_pit();
|
void AddSC_spell_delve_dread_pit();
|
||||||
|
void AddSC_archaeology_spell_scripts();
|
||||||
|
|
||||||
// The name of this function should match:
|
// The name of this function should match:
|
||||||
// void Add${NameOfDirectory}Scripts()
|
// void Add${NameOfDirectory}Scripts()
|
||||||
@@ -61,4 +62,5 @@ void AddSpellsScripts()
|
|||||||
AddSC_housing_spell_scripts();
|
AddSC_housing_spell_scripts();
|
||||||
AddSC_dragonriding_spell_scripts();
|
AddSC_dragonriding_spell_scripts();
|
||||||
AddSC_spell_delve_dread_pit();
|
AddSC_spell_delve_dread_pit();
|
||||||
|
AddSC_archaeology_spell_scripts();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user