Core/ Archeology merge

This commit is contained in:
luis
2026-09-16 02:20:08 -03:00
parent 1221562029
commit c9b4610887
26 changed files with 5417 additions and 75 deletions
@@ -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,
+368 -2
View File
@@ -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;
}
+75 -6
View File
@@ -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;
+3 -3
View File
@@ -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)
+7 -4
View File
@@ -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*))
+2 -2
View File
@@ -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);
+717 -47
View File
@@ -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;
+59
View File
@@ -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;
+12 -3
View File
@@ -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;
});
}
+3 -1
View File
@@ -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
+1 -1
View File
@@ -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);
+4 -1
View File
@@ -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;
+4 -2
View File
@@ -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();
} }