delves part 1

This commit is contained in:
luis
2026-09-12 05:23:33 -03:00
parent 5be3ce35dd
commit 187b6a81a9
29 changed files with 1564 additions and 422 deletions
@@ -0,0 +1,42 @@
UPDATE `creature_template` SET `speed_walk`=1, `speed_run`=1.142857193946838378, `unit_flags2`=0x800 WHERE `entry` IN (53648, 53102); -- Inferno Hawk
UPDATE `creature_template` SET `speed_walk`=1, `unit_flags2`=0x800 WHERE `entry`=53115; -- Molten Lord
UPDATE `creature_template` SET `unit_flags2`=0x800 WHERE `entry`=53185; -- Flamewaker Overseer
UPDATE `creature_template` SET `unit_flags2`=0x800 WHERE `entry`=53119; -- Flamewaker Forward Guard
UPDATE `creature_template` SET `unit_flags2`=0x800 WHERE `entry`=53120; -- Flamewaker Pathfinder
UPDATE `creature_template` SET `unit_flags2`=0x800 WHERE `entry` IN (53639, 53121); -- Flamewaker Cauterizer
-- Template Addon
UPDATE `creature_template_addon` SET `visibilityDistanceType`=3 WHERE `entry`=53185; -- 53185 (Flamewaker Overseer)
-- Difficulty
UPDATE `creature_template_difficulty` SET `StaticFlags1`=0x30000000, `VerifiedBuild`=69587 WHERE (`Entry`=53102 AND `DifficultyID`=14); -- 53102 (Inferno Hawk) - CanSwim, Floating
UPDATE `creature_template_difficulty` SET `StaticFlags1`=0x0, `StaticFlags4`=0x2000000, `VerifiedBuild`=69587 WHERE (`Entry`=53185 AND `DifficultyID`=14); -- 53185 (Flamewaker Overseer) - HideInCombatLog
UPDATE `creature_template_difficulty` SET `StaticFlags1`=0x0, `StaticFlags4`=0x2000000, `VerifiedBuild`=69587 WHERE (`Entry`=53119 AND `DifficultyID`=14); -- 53119 (Flamewaker Forward Guard) - HideInCombatLog
UPDATE `creature_template_difficulty` SET `StaticFlags1`=0x0, `StaticFlags4`=0x2000000, `VerifiedBuild`=69587 WHERE (`Entry`=53120 AND `DifficultyID`=14); -- 53120 (Flamewaker Pathfinder) - HideInCombatLog
UPDATE `creature_template_difficulty` SET `StaticFlags1`=0x0, `StaticFlags4`=0x2000000, `VerifiedBuild`=69587 WHERE (`Entry`=53121 AND `DifficultyID`=14); -- 53121 (Flamewaker Cauterizer) - HideInCombatLog
DELETE FROM `spell_script_names` WHERE `spell_id`=1219616 AND `ScriptName`='spell_hun_spotters_mark';
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES (1219616, 'spell_hun_spotters_mark');
DELETE FROM `creature_addon` WHERE `guid`=135295;
INSERT INTO `creature_addon` (`guid`, `PathId`, `mount`, `MountCreatureID`, `StandState`, `AnimTier`, `VisFlags`, `SheathState`, `PvPFlags`, `emote`, `aiAnimKit`, `movementAnimKit`, `meleeAnimKit`, `visibilityDistanceType`, `auras`) VALUES (135295, 0, 0, 0, 8, 0, 0, 1, 0, 0, 0, 0, 0, 0, '');
UPDATE `gameobject` SET `PhaseGroup`=0 WHERE `guid`=15498;
DELETE FROM `spell_script_names` WHERE `spell_id`=195072 AND `ScriptName`='spell_dh_fel_rush';
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES (195072, 'spell_dh_fel_rush');
DELETE FROM `spell_script_names` WHERE `spell_id`=197922 AND `ScriptName`='spell_dh_fel_rush_aura';
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES (197922, 'spell_dh_fel_rush_aura');
DELETE FROM `spell_script_names` WHERE `spell_id`=197923 AND `ScriptName`='spell_dh_fel_rush_aura';
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES (197923, 'spell_dh_fel_rush_aura');
DELETE FROM `access_requirement` WHERE `mapId`=2825 AND `difficulty`=205;
INSERT INTO `access_requirement` (`mapId`, `difficulty`, `level_min`, `level_max`, `item`, `item2`, `quest_done_A`, `quest_done_H`, `completed_achievement`, `quest_failed_text`, `comment`) VALUES (2825, 205, 0, 0, 0, 0, 0, 0, 0, NULL, NULL);
UPDATE `creature_template` SET `unit_flags3`=0 WHERE `entry`=51247;
ALTER TABLE `delve_template`
ADD COLUMN `exitMapId` int NOT NULL DEFAULT '-1' COMMENT 'overworld map of exitX/Y/Z/O (-1 = unknown)' AFTER `exitO`;
+3 -6
View File
@@ -61,12 +61,9 @@ namespace Delves
_owners.insert(player->GetGUID());
PopulateDelveData(player);
// Retail: Coffer Key Shards auto-combine into Restored Coffer Keys on delve entry (100 -> 1).
while (player->GetCurrencyQuantity(CURRENCY_COFFER_KEY_SHARDS) >= COFFER_KEY_SHARDS_PER_KEY)
{
player->RemoveCurrency(CURRENCY_COFFER_KEY_SHARDS, int32(COFFER_KEY_SHARDS_PER_KEY));
player->AddCurrency(CURRENCY_RESTORED_COFFER_KEY, 1, CurrencyGainSource::Loot);
}
// No shard -> key conversion here: none of the five captured runs moved currency 3028 on entry
// (REPORT.md 6.2); retail instead completes the hidden entry quest ~6.6 s after SMSG_NEW_WORLD, see
// DelveMgr::OnPlayerEnteredDelve.
// Spawn companion for the first player if group size <= MAX_COMPANION_GROUP_SIZE (4)
if (_state == DelveState::Entering)
+328 -1
View File
@@ -20,9 +20,23 @@
#include "Creature.h"
#include "DatabaseEnv.h"
#include "DB2Stores.h"
#include "DelvesPackets.h"
#include "DelvesRewards.h"
#include "DelvesSeason.h"
#include "Duration.h"
#include "GameTime.h"
#include "GossipDef.h"
#include "Item.h"
#include "ItemEnchantmentMgr.h"
#include "Log.h"
#include "Map.h"
#include "NPCPackets.h"
#include "ObjectMgr.h"
#include "Player.h"
#include "QuestDef.h"
#include "SpellPackets.h"
#include "Timer.h"
#include "WorldSession.h"
namespace Delves
{
@@ -88,7 +102,7 @@ namespace Delves
"entryX, entryY, entryZ, entryO, "
"exitX, exitY, exitZ, exitO, "
"activeScenarioId, rewardScenarioId, worldState26903, finalBossEntry, "
"tieredEntranceId, tieredEntranceUnknown3, entranceUiWidgetSetId, modifierUiWidgetSetTier1 "
"tieredEntranceId, tieredEntranceUnknown3, entranceUiWidgetSetId, modifierUiWidgetSetTier1, exitMapId "
"FROM delve_template");
if (!result)
@@ -132,6 +146,7 @@ namespace Delves
tmpl.TieredEntranceUnknown3 = fields[27].GetUInt32();
tmpl.EntranceUiWidgetSetId = fields[28].GetUInt32();
tmpl.ModifierUiWidgetSetTier1 = fields[29].GetUInt32();
tmpl.ExitMapId = fields[30].GetInt32();
_delveTemplatesByMap[tmpl.MapId] = tmpl;
_delveTemplatesList.push_back(tmpl);
@@ -402,4 +417,316 @@ namespace Delves
return result;
}
// ---------------------------------------------------------------------------------------------
// Retail run flow (12.1.0.69497 captures, C:\sniff\tcharvest\out\delve_research\REPORT.md)
// ---------------------------------------------------------------------------------------------
void DelveMgr::SendTieredEntranceOpen(Player* player, Creature const* entrance)
{
if (!player || !entrance)
return;
DelveTemplate const* tmpl = GetDelveTemplateForEntrance(entrance);
if (!tmpl)
{
TC_LOG_DEBUG("scripts.delves", "DelveMgr::SendTieredEntranceOpen: could not resolve entrance {} (entry {}, map {}) to a delve template",
entrance->GetGUID().ToString(), entrance->GetEntry(), entrance->GetMapId());
return;
}
DelveProgress progress;
DelvesRewards::LoadProgress(player->GetSession()->GetBattlenetAccountId(), progress);
bool const meetsLevel = DelvesSeason::MeetsMinimumLevelRequirement(player);
// REPORT.md 1.3 (gulf 88718 == 98437, eversong 1842501; 12.0.7 The Darkway agrees): every delve entrance reports
// EntranceType 1, field 7 is 234 for every delve, field 6 is 0, fields 3/4/8 and the widget sets are per entrance
// (delve_template), the tier rows are season data (delve_tiered_entrance_tier). The client matches the response
// by the entrance GUID - two spawns of 212407 can be visible at once (REPORT.md 1.2), so echo this spawn's guid.
WorldPackets::Delves::TieredEntranceOpenResponse response;
response.EntranceGUID = entrance->GetGUID();
response.EntranceType = TIERED_ENTRANCE_TYPE_DELVE;
response.MapID = tmpl->MapId;
response.Unknown3 = tmpl->TieredEntranceUnknown3;
response.Unknown4 = tmpl->EntranceUiWidgetSetId;
response.Unknown6 = 0;
response.Unknown7 = DELVE_TIERED_ENTRANCE_FIELD7;
response.Unknown8 = tmpl->TieredEntranceId;
if (MapEntry const* mapEntry = sMapStore.LookupEntry(tmpl->MapId))
response.EntranceDescription = mapEntry->MapName[player->GetSession()->GetSessionDbcLocale()];
response.Tiers.reserve(_tieredEntranceTiers.size());
for (TieredEntranceTierData const& tierRow : _tieredEntranceTiers)
{
WorldPackets::Delves::TieredEntranceTier& tierData = response.Tiers.emplace_back();
tierData.TieredEntranceTierID = tierRow.Id;
tierData.Tier = tierRow.Tier;
tierData.SuggestedILvl = tierRow.SuggestedILvl;
tierData.OverrideTooltipSpellID = tierRow.OverrideTooltipSpellId;
tierData.UnlockPlayerConditionID = tierRow.UnlockPlayerConditionId;
tierData.DynamicUnlockPlayerConditionID = tierRow.DynamicUnlockPlayerConditionId;
tierData.ModifierUIWidgetSetID = tmpl->ModifierUiWidgetSetTier1 && tierRow.Tier ? tmpl->ModifierUiWidgetSetTier1 - (tierRow.Tier - 1) : 0;
tierData.Unlocked = meetsLevel && tierRow.Tier <= progress.HighestTierUnlocked;
tierData.TierDescription = tierRow.Description;
for (TieredEntranceRewardData const& reward : tierRow.Rewards)
{
WorldPackets::Delves::TieredEntranceReward& rewardData = tierData.PreviewTreasureList.emplace_back();
rewardData.RewardType = reward.RewardType;
rewardData.Id = reward.Id;
rewardData.Quantity = reward.Quantity;
rewardData.Context = reward.Context;
}
}
player->SendDirectMessage(response.Write());
}
void DelveMgr::OpenEntranceByProximity(Player* player, Creature const* entrance)
{
if (!player || !entrance || !player->IsInWorld() || player->IsBeingTeleported())
return;
// an entrance NPC standing inside a delve never opens the picker
if (GetDelveTemplate(player->GetMapId()))
return;
// once per approach (REPORT.md 1.1: gulf opened at 88718, CMSG_CLOSE_INTERACTION at 95751 when the player walked
// away, opened again at 98205). WorldSession::HandleCloseInteraction resets the interaction for this guid.
InteractionData& interaction = player->PlayerTalkClass->GetInteractionData();
PlayerInteractionType const interactionType = PlayerInteractionType(DELVE_ENTRANCE_INTERACTION_TYPE);
if (interaction.IsInteractingWith(entrance->GetGUID(), interactionType))
return;
if (!GetDelveTemplateForEntrance(entrance))
return;
interaction.StartInteraction(entrance->GetGUID(), interactionType);
// REPORT.md 1.1, gulf 98205: `PackedGUID(212407) | 4f000000 | 00` = entrance guid + int32 InteractionType 79 + bit Success
WorldPackets::NPC::NPCInteractionOpenResult openResult;
openResult.Npc = entrance->GetGUID();
openResult.InteractionType = interactionType;
openResult.Success = true;
player->SendDirectMessage(openResult.Write());
SendTieredEntranceOpen(player, entrance);
}
void DelveMgr::CloseEntranceByProximity(Player* player, Creature const* entrance)
{
if (!player || !entrance)
return;
InteractionData& interaction = player->PlayerTalkClass->GetInteractionData();
if (interaction.IsInteractingWith(entrance->GetGUID(), PlayerInteractionType(DELVE_ENTRANCE_INTERACTION_TYPE)))
interaction.Reset();
}
void DelveMgr::EnterDelve(Player* player, DelveTemplate const& tmpl, uint8 tier)
{
if (!player || !player->IsInWorld() || tier == 0 || tier > MAX_DELVE_TIER)
return;
// eversong sent CMSG_SELECT_DELVE_ENTRANCE_TIER twice (1844955 and 1846555, the second after
// CMSG_AUTH_CONTINUED_SESSION); retail had already acted on the first (REPORT.md 1.4)
if (player->IsBeingTeleported() || player->GetMapId() == tmpl.MapId)
return;
// Remember where to come back to: retail returns the player to the delve's exit coordinates on the map
// they entered from - Harandar 2694 for the Gulf of Memory, map 0 for the Shadow Enclave (REPORT.md 1.5 / 5).
if (!GetDelveTemplate(player->GetMapId()))
{
if (tmpl.ExitX != 0.0f || tmpl.ExitY != 0.0f)
player->m_delveReturnLocation = WorldLocation(player->GetMapId(), tmpl.ExitX, tmpl.ExitY, tmpl.ExitZ, tmpl.ExitO);
else
player->m_delveReturnLocation = player->GetWorldLocation();
}
player->m_delveSelectedMapId = tmpl.MapId;
player->m_delveSelectedTier = tier;
// the picker closes with the selection; the choice-clear that precedes SMSG_NEW_WORLD is sent by Player::TeleportTo
player->PlayerTalkClass->GetInteractionData().Reset();
// Keep the client-side JamDelveData progression mirror's last-selected delve current.
DelvesRewards::PublishProgress(player);
// REPORT.md 1.6: the per-run world states. Retail delivers them with SMSG_INIT_WORLD_STATES of the delve map and
// re-sends them as SMSG_UPDATE_WORLD_STATE at the same tick (gulf 101842); OnPlayerEnteredDelve puts them on the
// delve Map for that. Sending them ahead of the transfer keeps the picker / HUD consistent while the transfer is
// pending (branch behaviour inherited from npc_delve_entrance::OnGossipSelect, not on the wire).
player->SendUpdateWorldState(WS_DELVE_TIER, tier);
player->SendUpdateWorldState(WS_DELVE_IN_DELVE_FLAG, 1);
player->SendUpdateWorldState(WS_DELVE_MAP_ID, tmpl.MapId);
player->SendUpdateWorldState(WS_DELVE_TIER_SPELL, GetTierSpellId(tier));
if (tmpl.WorldState26903)
player->SendUpdateWorldState(WS_DELVE_UNKNOWN_26903, tmpl.WorldState26903);
TC_LOG_DEBUG("scripts.delves", "DelveMgr::EnterDelve: player {} -> map {} tier {} (return to map {})",
player->GetName(), tmpl.MapId, tier, player->m_delveReturnLocation.GetMapId());
// REPORT.md 1.5: no SMSG_TRANSFER_PENDING, SMSG_NEW_WORLD reason 21 (seamless) ~1.9 s after the select.
// Player::TeleportTo keeps TELE_TO_SEAMLESS for delve maps although they are not cosmetic children of the
// outdoor map. The map difficulty resolves to 208 through the MapDifficulty downscale fallback (REPORT.md 1.7).
player->TeleportTo(tmpl.MapId, tmpl.EntryX, tmpl.EntryY, tmpl.EntryZ, tmpl.EntryO, TELE_TO_SEAMLESS);
}
namespace
{
// REPORT.md 4 / work item 6: the hidden entry quest. Preferred path is the real quest template (so the quest log /
// criteria side effects match retail); when it is not in the world DB - or cannot be taken again - the same rewards
// are granted directly with DisplayToastMethod::QuestComplete (16) toasts, as observed in toasts_items.txt.
void GrantDelveEntryRewards(Player* player)
{
if (Quest const* quest = sObjectMgr->GetQuestTemplate(DELVE_ENTRY_REWARD_QUEST_ID))
{
if (player->CanTakeQuest(quest, false) && player->CanAddQuest(quest, false))
{
player->AddQuestAndCheckCompletion(quest, nullptr);
if (player->GetQuestStatus(DELVE_ENTRY_REWARD_QUEST_ID) == QUEST_STATUS_COMPLETE && player->CanRewardQuest(quest, false))
player->RewardQuest(quest, LootItemType::Item, 0, nullptr, true);
return;
}
}
player->AddCurrency(CURRENCY_COFFER_KEY_SHARDS, DELVE_ENTRY_REWARD_SHARDS, CurrencyGainSource::QuestReward);
player->SendDisplayToast(CURRENCY_COFFER_KEY_SHARDS, DisplayToastType::NewCurrency, false, DELVE_ENTRY_REWARD_SHARDS,
DisplayToastMethod::QuestComplete, DELVE_ENTRY_REWARD_QUEST_ID);
player->AddCurrency(CURRENCY_VOIDLIGHT_MARL, DELVE_ENTRY_REWARD_MARL, CurrencyGainSource::QuestReward);
player->SendDisplayToast(CURRENCY_VOIDLIGHT_MARL, DisplayToastType::NewCurrency, false, DELVE_ENTRY_REWARD_MARL,
DisplayToastMethod::QuestComplete, DELVE_ENTRY_REWARD_QUEST_ID);
if (sObjectMgr->GetItemTemplate(DELVE_ENTRY_REWARD_ITEM))
{
ItemPosCountVec dest;
if (player->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, DELVE_ENTRY_REWARD_ITEM, 1) == EQUIP_ERR_OK)
{
if (Item* item = player->StoreNewItem(dest, DELVE_ENTRY_REWARD_ITEM, true, GenerateItemRandomBonusListId(DELVE_ENTRY_REWARD_ITEM), {}, ItemContext::Quest_Reward))
{
player->SendNewItem(item, 1, true, false);
player->SendDisplayToast(0, DisplayToastType::NewItem, false, 1, DisplayToastMethod::QuestComplete, DELVE_ENTRY_REWARD_QUEST_ID, item);
}
}
else
player->SendItemRetrievalMail(DELVE_ENTRY_REWARD_ITEM, 1, ItemContext::Quest_Reward);
}
}
}
void DelveMgr::OnPlayerEnteredDelve(Player* player)
{
if (!player || !player->IsInWorld())
return;
Map* map = player->GetMap();
DelveTemplate const* tmpl = GetDelveTemplate(map->GetId());
if (!tmpl)
return;
// The run's tier: what the first entrant put on the map, else this player's selection (group members who were
// brought in without selecting keep the instance's tier).
uint8 tier = uint8(std::clamp<int32>(map->GetWorldStateValue(WS_DELVE_TIER), 0, MAX_DELVE_TIER));
if (!tier)
tier = std::clamp<uint8>(player->m_delveSelectedTier, 1, MAX_DELVE_TIER);
player->m_delveSelectedTier = tier;
player->m_delveSelectedMapId = tmpl->MapId;
// REPORT.md 1.6: SMSG_INIT_WORLD_STATES of the delve map carries these and they are re-sent as
// SMSG_UPDATE_WORLD_STATE at the same tick. Setting them on the Map makes INIT carry them for everyone who
// enters later and broadcasts the UPDATE on change; an unchanged value is re-sent to this player only.
auto publish = [&](uint32 worldStateId, int32 value)
{
if (!map->GetWorldStateValues().contains(int32(worldStateId)) || map->GetWorldStateValue(int32(worldStateId)) != value)
map->SetWorldStateValue(int32(worldStateId), value, false);
else
player->SendUpdateWorldState(worldStateId, uint32(value));
};
publish(WS_DELVE_TIER, tier);
publish(WS_DELVE_IN_DELVE_FLAG, 1);
publish(WS_DELVE_MAP_ID, int32(tmpl->MapId));
publish(WS_DELVE_TIER_SPELL, int32(GetTierSpellId(tier)));
if (tmpl->WorldState26903)
publish(WS_DELVE_UNKNOWN_26903, int32(tmpl->WorldState26903));
if (tmpl->LfgDungeonsId)
publish(WS_DELVE_LFG_DUNGEONS_ID, int32(tmpl->LfgDungeonsId));
publish(WS_DELVE_COMPLETE, 0);
publish(WS_DELVE_ENCOUNTER_IN_PROGRESS, 0);
// REPORT.md 4 (work item 6): the hidden entry quest completes ~6.6 s after SMSG_NEW_WORLD (gulf: NEW_WORLD 101829,
// quest credit + toasts ~108400). Once per run: keyed on the instance id so a relog into the same run does not
// grant twice. The event lives on the player's own processor and dies with the player.
if (player->m_delveEntryRewardInstanceId == map->GetInstanceId())
return;
player->m_delveEntryRewardInstanceId = map->GetInstanceId();
uint32 const mapId = map->GetId();
uint32 const instanceId = map->GetInstanceId();
player->m_Events.AddEventAtOffset([player, mapId, instanceId]()
{
if (!player->IsInWorld() || player->GetMapId() != mapId || player->GetInstanceId() != instanceId)
return;
GrantDelveEntryRewards(player);
}, Milliseconds(DELVE_ENTRY_REWARD_DELAY_MS));
}
void DelveMgr::LeaveDelve(Player* player)
{
if (!player || !player->IsInWorld() || player->IsBeingTeleported())
return;
DelveTemplate const* tmpl = GetDelveTemplate(player->GetMapId());
if (!tmpl)
return;
// Destination: the map the player entered from at the exit coordinates (REPORT.md 5 step 6: gulf back to 2694
// (46.226, 810.912, 1109.843), eversong back to 0 (4780.455, -4118.306, 32.133)), else the template's exit map,
// else homebind. Never another delve.
WorldLocation destination = player->m_delveReturnLocation;
if (destination.GetMapId() == MAPID_INVALID || GetDelveTemplate(destination.GetMapId()))
{
if (tmpl->ExitMapId >= 0 && (tmpl->ExitX != 0.0f || tmpl->ExitY != 0.0f))
destination = WorldLocation(uint32(tmpl->ExitMapId), tmpl->ExitX, tmpl->ExitY, tmpl->ExitZ, tmpl->ExitO);
else
destination = player->m_homebind;
}
// REPORT.md 5 step 6: SMSG_SPELL_VISUAL_LOAD_SCREEN (kit 79917, 1500 ms) right after the spell-click on the
// Leave-O-Bot - a seamless SMSG_NEW_WORLD shows no loading screen of its own, this kit covers the swap.
WorldPackets::Spells::SpellVisualLoadScreen loadScreen{ int32(DELVE_EXIT_LOAD_SCREEN_KIT_ID), Milliseconds(DELVE_EXIT_LOAD_SCREEN_DURATION_MS) };
player->SendDirectMessage(loadScreen.Write());
// Branch behaviour kept from go_leave_delve (not on the wire: retail only sends SMSG_INIT_WORLD_STATES of the
// outdoor map after the transfer): zero the per-run states so the HUD drops out of delve mode.
player->SendUpdateWorldState(WS_DELVE_TIER, 0);
player->SendUpdateWorldState(WS_DELVE_IN_DELVE_FLAG, 0);
player->SendUpdateWorldState(WS_DELVE_MAP_ID, 0);
player->SendUpdateWorldState(WS_DELVE_TIER_SPELL, 0);
player->SendUpdateWorldState(WS_DELVE_UNKNOWN_26903, 0);
player->ClearDelveData(int32(tmpl->MapId));
player->m_delveReturnLocation = WorldLocation();
player->m_delveSelectedTier = 0;
player->m_delveSelectedMapId = 0;
TC_LOG_DEBUG("scripts.delves", "DelveMgr::LeaveDelve: player {} leaves map {} -> map {} ({:.1f} {:.1f} {:.1f})",
player->GetName(), tmpl->MapId, destination.GetMapId(), destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ());
// REPORT.md 5: the transfer back is seamless as well (SMSG_NEW_WORLD, no SMSG_TRANSFER_PENDING). Retail's
// NEW_WORLD followed the load screen by several seconds (phase / object despawns and a server hop); here the
// transfer runs once the load-screen kit has faded in, on the player's own event processor.
uint32 const mapId = player->GetMapId();
uint32 const instanceId = player->GetInstanceId();
player->m_Events.AddEventAtOffset([player, mapId, instanceId, destination]()
{
if (!player->IsInWorld() || player->IsBeingTeleported() || player->GetMapId() != mapId || player->GetInstanceId() != instanceId)
return;
player->TeleportTo(destination, TELE_TO_SEAMLESS);
}, Milliseconds(DELVE_EXIT_LOAD_SCREEN_DURATION_MS));
}
} // namespace Delves
+26
View File
@@ -25,6 +25,7 @@
#include <vector>
class Creature;
class Player;
struct DelvesSeasonEntry;
struct PlayerCompanionInfoEntry;
@@ -90,6 +91,31 @@ namespace Delves
std::vector<TieredEntranceTierData> const& GetTieredEntranceTiers() const { return _tieredEntranceTiers; }
TieredEntranceTierData const* GetTieredEntranceTier(uint32 tieredEntranceTierId) const;
// ---------------------------------------------------------------------------------------------
// Retail run flow (12.1.0.69497 captures, C:\sniff\tcharvest\out\delve_research\REPORT.md).
// Shared by WorldSession::Handle* (DelvesHandler.cpp) and the entrance / instance scripts.
// ---------------------------------------------------------------------------------------------
// Builds and sends SMSG_TIERED_ENTRANCE_OPEN_RESPONSE for the delve this entrance spawn opens (REPORT.md 1.3).
// Used by the click path (CMSG_TIERED_ENTRANCE_OPEN) and by OpenEntranceByProximity.
void SendTieredEntranceOpen(Player* player, Creature const* entrance);
// REPORT.md 1.1 (work item 2): a delve entrance (creature 212407 / 251896) opens by itself when the player comes
// into range - SMSG_NPC_INTERACTION_OPEN_RESULT(entrance, type 79) followed by the tier picker. Sent once per
// approach: the open interaction is tracked in the player's InteractionData, cleared by CMSG_CLOSE_INTERACTION
// (gulf 95751), by CloseEntranceByProximity and when the player leaves the map (Player::RemoveFromWorld).
void OpenEntranceByProximity(Player* player, Creature const* entrance);
// Counterpart for the entrance AI when the player leaves range without the client closing the interaction.
void CloseEntranceByProximity(Player* player, Creature const* entrance);
// REPORT.md 1.4 - 1.6 (work item 5): tier selected -> remember where to return, publish the selection and
// seamlessly transfer to the delve map (SMSG_NEW_WORLD reason 21, no SMSG_TRANSFER_PENDING).
void EnterDelve(Player* player, DelveTemplate const& tmpl, uint8 tier);
// Called from the delve instance script's OnPlayerEnter: sets the observed world states on the delve map
// (REPORT.md 1.6) and schedules the hidden entry quest reward once per run (REPORT.md 4, work item 6).
void OnPlayerEnteredDelve(Player* player);
// REPORT.md 5 (work item 11): load screen kit 79917 then seamless transfer to the exit coordinates on the
// map the player entered from (fallback: delve_template exit map, then homebind).
void LeaveDelve(Player* player);
private:
void LoadDelveTemplates();
void LoadTierRewards();
+35 -1
View File
@@ -118,6 +118,9 @@ namespace Delves
// These are sent to the client around delve entry/exit to drive the
// Blizzard_DelvesDifficultyPicker UI's active-tier display, the in-delve HUD
// banner, and the "you're in a delve" persistent flag.
// 12.1.0.69497 (REPORT.md 1.6): all of these arrive in SMSG_INIT_WORLD_STATES of the delve map and are re-sent as
// SMSG_UPDATE_WORLD_STATE at the same tick (gulf 101842: 24430=1, 26345=1, 26423=2964, 26931=1260940, 26903=1277243,
// 5029=3070, 25316=0, 24836=0). They are per instance, so DelveMgr::OnPlayerEnteredDelve sets them on the delve Map.
enum DelveWorldStates : uint32
{
WS_DELVE_TIER = 24430, // Selected tier (1..11)
@@ -125,9 +128,36 @@ namespace Delves
WS_DELVE_MAP_ID = 26423, // Active delve MapID
WS_DELVE_TIER_SPELL = 26931, // The TIER_SPELL_IDS[] value cast for this run
WS_DELVE_UNKNOWN_26903 = 26903, // Per-delve, controls center spell display in tier picker
WS_DELVE_LFG_DUNGEONS_ID = 5029, // Sent inside the instance (OnPlayerEnter)
WS_DELVE_LFG_DUNGEONS_ID = 5029, // LFGDungeons id of the delve (3069/3070/3083, DifficultyID 208)
WS_DELVE_COMPLETE = 25316, // 0 -> 1 with SMSG_SCENARIO_COMPLETED (REPORT.md 5 step 2)
WS_DELVE_ENCOUNTER_IN_PROGRESS = 24836, // 1 at SMSG_ENCOUNTER_START, 0 at SMSG_ENCOUNTER_END (REPORT.md 1.6)
};
// ---------------------------------------------------------------------------
// Entry / exit flow (12.1.0.69497 captures, C:\sniff\tcharvest\out\delve_research\REPORT.md)
// ---------------------------------------------------------------------------
// REPORT.md 1.1 / 6.1: the delve entrance auto-opens by proximity - the server sends SMSG_NPC_INTERACTION_OPEN_RESULT
// (guid of the entrance creature 212407, InteractionType 79, Success) and immediately after it
// SMSG_TIERED_ENTRANCE_OPEN_RESPONSE (gulf 88718 and 98205; a CMSG_CLOSE_INTERACTION for the entrance guid at 95751
// when the player walked away). DBCEnums.h only knows the value as PlaceholderType79.
static constexpr uint32 DELVE_ENTRANCE_INTERACTION_TYPE = 79;
// REPORT.md 4 / 6.2 (work item 6): ~6.6 s after SMSG_NEW_WORLD retail completes a hidden quest that grants
// 25x currency 3310 (Coffer Key Shards) + 100x currency 3316 (Voidlight Marl) + item 263488 with quest-complete toasts.
// 12.1 quest id 96612 (12.0.1 deatholme: 93943).
static constexpr uint32 DELVE_ENTRY_REWARD_QUEST_ID = 96612;
static constexpr uint32 CURRENCY_VOIDLIGHT_MARL = 3316;
static constexpr uint32 DELVE_ENTRY_REWARD_SHARDS = 25;
static constexpr uint32 DELVE_ENTRY_REWARD_MARL = 100;
static constexpr uint32 DELVE_ENTRY_REWARD_ITEM = 263488;
static constexpr uint32 DELVE_ENTRY_REWARD_DELAY_MS = 6600;
// REPORT.md 5 step 6: CMSG_SPELL_CLICK on the Leave-O-Bot (gulf 1118881) -> SMSG_SPELL_VISUAL_LOAD_SCREEN
// (kit 79917, 1500 ms) -> ... -> SMSG_NEW_WORLD back to the originating map at the exit coordinates.
static constexpr uint32 DELVE_EXIT_LOAD_SCREEN_KIT_ID = 79917;
static constexpr uint32 DELVE_EXIT_LOAD_SCREEN_DURATION_MS = 1500;
// ---------------------------------------------------------------------------
// Bountiful Delves
// ---------------------------------------------------------------------------
@@ -344,6 +374,10 @@ namespace Delves
float ExitY = 0.0f;
float ExitZ = 0.0f;
float ExitO = 0.0f;
// Overworld map the exit coordinates belong to (-1 = unknown). REPORT.md 1.5: the Gulf of Memory returns to
// Harandar 2694, the Shadow Enclave and the Darkway to map 0. DelveMgr::LeaveDelve prefers the map the player
// actually came from (stored at entry) and falls back to this.
int32 ExitMapId = -1;
// Per-delve scenario IDs. ActiveScenarioId is the in-progress scenario;
// RewardScenarioId is the completion scenario (often shared across delves ?
+98 -36
View File
@@ -16,18 +16,63 @@
*/
#include "DelvesRewards.h"
#include "Config.h"
#include "DatabaseEnv.h"
#include "DB2Stores.h"
#include "DelveMgr.h"
#include "DelvesCompanion.h"
#include "DelvesSeason.h"
#include "ItemBonusMgr.h"
#include "Log.h"
#include "Loot.h"
#include "LootMgr.h"
#include "Mail.h"
#include "Player.h"
#include "ReputationMgr.h"
#include <Config.h>
#include "WeeklyRewardsMgr.h"
#include "WorldSession.h"
namespace Delves
{
namespace
{
// Rolls a reference_loot_template pool as personal loot at the delve item context and grants every item,
// scaled through ItemBonusMgr (the same authentic ilvl path the M+ rewards use). Mails on full bags.
void GrantDelveLoot(Player* player, uint32 lootId, uint8 itemContext)
{
if (!lootId || !LootTemplates_Reference.HaveLootFor(lootId))
return;
Loot loot(player->GetMap(), ObjectGuid::Empty, LOOT_NONE, nullptr);
loot.FillLoot(lootId, LootTemplates_Reference, player, true /*personal*/, true /*noEmptyError*/,
LOOT_MODE_DEFAULT, ItemContext(itemContext));
for (LootItem const& lootItem : loot.items)
{
if (!lootItem.itemid || !lootItem.count)
continue;
std::vector<int32> bonuses = ItemBonusMgr::GetBonusListsForItem(lootItem.itemid,
ItemBonusMgr::ItemBonusGenerationParams(ItemContext(itemContext)));
ItemPosCountVec dest;
if (player->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, lootItem.itemid, lootItem.count) == EQUIP_ERR_OK)
player->StoreNewItem(dest, lootItem.itemid, true, 0, GuidSet(), ItemContext(itemContext), &bonuses);
else if (Item* item = Item::CreateItem(lootItem.itemid, lootItem.count, ItemContext(itemContext), player, false))
{
item->SetBonuses(bonuses);
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
item->SaveToDB(trans);
MailDraft("Delve Reward", "Your delve reward.")
.AddItem(item)
.SendMailTo(trans, player, MailSender(player, MAIL_STATIONERY_GM), MAIL_CHECK_MASK_COPIED);
CharacterDatabase.CommitTransaction(trans);
}
}
}
}
void DelvesRewards::AwardDelveCompletion(Player* player, uint8 tier, bool bountiful, bool hasRevivesRemaining)
{
uint32 accountId = player->GetSession()->GetBattlenetAccountId();
@@ -47,6 +92,11 @@ namespace Delves
// Award companion XP
AwardCompanionXP(player, tier);
// End-of-run gear (non-bountiful context; retail caps this at the tier-3 track). The item POOL is
// server content (reference_loot_template, Delves.Reward.LootId); the LEVEL comes from the item context.
GrantDelveLoot(player, uint32(sConfigMgr->GetIntDefault("Delves.Reward.LootId", 0)),
GetItemContextForTier(std::min<uint8>(tier, 3), false));
// Handle bountiful rewards
if (bountiful)
{
@@ -55,14 +105,23 @@ namespace Delves
if (hasRevivesRemaining && HasCofferKey(player))
{
ConsumeCofferKey(player);
// TODO: Spawn Bountiful Coffer with enhanced loot
// Bountiful Coffer: the enhanced chest loot at the tier's bounty item context.
GrantDelveLoot(player, uint32(sConfigMgr->GetIntDefault("Delves.Coffer.LootId", 0)),
GetItemContextForTier(tier, true));
TC_LOG_DEBUG("scripts.delves", "Player {} opened Bountiful Coffer (tier {}, ItemContext {})",
player->GetName(), tier, GetItemContextForTier(tier, true));
}
}
// Update Great Vault progress
UpdateGreatVaultProgress(player, tier);
// Great Vault: an endgame delve completion credits the vault's World activity row - the row the client
// fills from WeeklyRewardChestThreshold.db2 Type 6 (live ids 196/197/198, slots at 2/4/8 completions).
// The level recorded per run is the delve TIER, so each slot is awarded the Nth-best tier of the week,
// exactly as a Mythic+ run credits the Dungeon row at its keystone level. The reward ITEM LEVEL that
// retail derives from that tier is content the server does not have a full table for yet (only the two
// endpoints are documented: T1 = 233, T8+ = 259, capped at Hero 1/6), so no ilvl is fabricated here -
// the vault advertises the slot and its tier, and the item comes from the vault reward pool.
if (tier >= DELVE_TIER_ENDGAME_START)
sWeeklyRewardsMgr.RecordActivity(player, WeeklyRewards::ActivityType::World, tier);
// Check for tier unlock (must have revives remaining)
if (hasRevivesRemaining && CanUnlockNextTier(progress.HighestTierUnlocked, tier, hasRevivesRemaining))
@@ -87,10 +146,23 @@ namespace Delves
if (!reward || reward->CrestType == CREST_NONE || reward->CrestCount == 0)
return;
// TODO: Award actual crest currency items based on reward->CrestType and reward->CrestCount
// CrestType maps to specific currency IDs in the game
TC_LOG_DEBUG("scripts.delves", "Awarding {} crests of type {} to {} for tier {}",
reward->CrestCount, reward->CrestType, player->GetName(), tier);
// CrestType -> crest currency (Midnight S1 Dawncrest ladder; config-tunable, guarded on CurrencyTypes.db2
// so a wrong/absent id is a safe no-op). Delves top out at Hero crests (T11), matching retail.
uint32 currencyId = 0;
switch (reward->CrestType)
{
case CREST_WEATHERED: currencyId = uint32(sConfigMgr->GetIntDefault("Delves.Crest.Tier1.CurrencyId", 3383)); break; // Adventurer Dawncrest
case CREST_CARVED: currencyId = uint32(sConfigMgr->GetIntDefault("Delves.Crest.Tier2.CurrencyId", 3341)); break; // Veteran Dawncrest
case CREST_RUNED: currencyId = uint32(sConfigMgr->GetIntDefault("Delves.Crest.Tier3.CurrencyId", 3343)); break; // Champion Dawncrest
case CREST_GILDED: currencyId = uint32(sConfigMgr->GetIntDefault("Delves.Crest.Tier4.CurrencyId", 3345)); break; // Hero Dawncrest
default: break;
}
if (currencyId && sCurrencyTypesStore.LookupEntry(currencyId))
player->AddCurrency(currencyId, reward->CrestCount, CurrencyGainSource::Loot);
TC_LOG_DEBUG("scripts.delves", "Awarded {} crests (currency {}) to {} for tier {}",
reward->CrestCount, currencyId, player->GetName(), tier);
}
void DelvesRewards::AwardCompanionXP(Player* player, uint8 tier)
@@ -102,11 +174,16 @@ namespace Delves
DelvesCompanion::LoadFromDB(player->GetSession()->GetBattlenetAccountId(), state);
DelvesCompanion::AwardCompanionXP(player->GetSession()->GetBattlenetAccountId(), state, xpAmount);
// Mirror the same amount into the retail-visible companion reputation track (Midnight faction 2742
// "Delves: Season 1", RenownCurrencyID 3317) so the client's rep/renown UI tracks companion
// progression. This is a mirror only - the internal CompanionState math above stays authoritative.
// Config-tunable and guarded on Faction.db2, so a wrong/absent id is a safe no-op.
if (uint32 factionId = uint32(sConfigMgr->GetIntDefault("Delves.Companion.FactionId", 2742)))
// Mirror the same amount into the retail-visible companion reputation track so the client's
// rep/renown UI tracks companion progression. This is a mirror only - the internal CompanionState
// math above stays authoritative. Config-tunable and guarded on Faction.db2, so a wrong or absent
// id is a safe no-op.
//
// Faction 2744 "Valeera Sanguinar" / "Trusty Delve Companion", NOT 2742 "Delves: Season 1":
// PlayerCompanionInfo.db2 row 11 is the Midnight row (DelvesSeasonID 4, TraitTreeID 1168,
// CreatureDisplayInfoID 67214) and its FactionID is 2744 - the companion's OWN track, exactly
// mirroring Brann's 2640 on rows 1/9/10. 2742 is the season faction, which is a different thing.
if (uint32 factionId = uint32(sConfigMgr->GetIntDefault("Delves.Companion.FactionId", 2744)))
if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId))
player->GetReputationMgr().ModifyReputation(factionEntry, int32(xpAmount));
@@ -152,29 +229,6 @@ namespace Delves
actualAmount, player->GetName(), progress.WeeklyCofferShards, MAX_COFFER_KEY_SHARDS_PER_WEEK);
}
void DelvesRewards::UpdateGreatVaultProgress(Player* player, uint8 tier)
{
if (tier < DELVE_TIER_ENDGAME_START) // Only tier 4+ counts for vault
return;
// Great Vault tracking uses the existing WeeklyRewardChest system
// Completions at 2/4/8 unlock vault slots
// The highest tier completed determines the vault reward ilvl
// TODO: Integrate with existing WeeklyRewardChestActivity system
TC_LOG_DEBUG("scripts.delves", "Updated Great Vault progress for {} (tier {})", player->GetName(), tier);
}
uint8 DelvesRewards::GetGreatVaultSlotCount(uint32 /*weeklyCompletions*/)
{
//if (weeklyCompletions >= VAULT_SLOT_3_COMPLETIONS) // 8
// return 3;
//if (weeklyCompletions >= VAULT_SLOT_2_COMPLETIONS) // 4
// return 2;
//if (weeklyCompletions >= VAULT_SLOT_1_COMPLETIONS) // 2
// return 1; TODO THOR
return 0;
}
bool DelvesRewards::CanUnlockNextTier(uint8 currentHighest, uint8 completedTier, bool hasRevivesRemaining)
{
// Must complete at current highest tier with revives remaining to unlock next
@@ -225,6 +279,14 @@ namespace Delves
SaveProgress(battlenetAccountId, progress);
}
void DelvesRewards::ResetAllWeeklyProgress()
{
// Weekly rollover for every account at once; online players' cached progress reloads on next use.
CharacterDatabase.Execute("UPDATE delve_progress SET weeklyCompletions = 0, highestTierThisWeek = 0, "
"weeklyBountifulCount = 0, weeklyCofferShards = 0");
TC_LOG_INFO("delves", "DelvesRewards: weekly delve progress reset.");
}
void DelvesRewards::PublishProgress(Player* player)
{
DelveProgress progress;
+4 -3
View File
@@ -51,9 +51,8 @@ namespace Delves
static void ConsumeCofferKey(Player* player);
static void AwardCofferKeyShards(Player* player, uint32 amount);
// Great Vault tracking
static void UpdateGreatVaultProgress(Player* player, uint8 tier);
static uint8 GetGreatVaultSlotCount(uint32 weeklyCompletions);
// Great Vault tracking has no delve-private duplicate: AwardDelveCompletion credits the vault's World
// activity row through WeeklyRewardsMgr, which owns the slot thresholds and the weekly period.
// Tier unlock validation
static bool CanUnlockNextTier(uint8 currentHighest, uint8 completedTier, bool hasRevivesRemaining);
@@ -62,6 +61,8 @@ namespace Delves
static void LoadProgress(uint32 battlenetAccountId, DelveProgress& progress);
static void SaveProgress(uint32 battlenetAccountId, DelveProgress const& progress);
static void ResetWeeklyProgress(uint32 battlenetAccountId, DelveProgress& progress);
// Global weekly rollover: zeroes every account's weekly counters (hooked into World::ResetWeeklyQuests).
static void ResetAllWeeklyProgress();
// Pushes DelveProgress to the client via the ActivePlayer JamDelveData mirror
// (SMSG_UPDATE_OBJECT). Call after any progress mutation while the player is
@@ -154,6 +154,7 @@
#include "MythicPlusData.h"
#include "PerksProgramMgr.h"
#include "DelvesCompanion.h"
#include "DelveMgr.h"
#include "DelvesDefines.h"
#include "DelvesRewards.h"
#include "GarrisonPackets.h"
@@ -1387,6 +1388,7 @@ bool Player::TeleportTo(TeleportLocation const& teleportLocation, TeleportToOpti
// Seamless teleport can happen only if cosmetic maps match
if (!oldmap ||
//(!delveTransfer && todo thor
(oldmap->GetEntry()->CosmeticParentMapID != int32(teleportLocation.Location.GetMapId()) && int32(GetMapId()) != mEntry->CosmeticParentMapID &&
!((oldmap->GetEntry()->CosmeticParentMapID != -1) ^ (oldmap->GetEntry()->CosmeticParentMapID != mEntry->CosmeticParentMapID))))
options &= ~TELE_TO_SEAMLESS;
@@ -1581,6 +1583,13 @@ void Player::RemoveFromWorld()
m_lootRolls.clear();
sOutdoorPvPMgr->HandlePlayerLeaveZone(this, m_zoneUpdateId);
sBattlefieldMgr->HandlePlayerLeaveZone(this, m_zoneUpdateId);
//WowCommunity
// A delve entrance opened by proximity (DelveMgr::OpenEntranceByProximity, interaction type 79) is tracked in
// the interaction data so it is sent once per approach; leaving the map ends the approach.
if (PlayerTalkClass->GetInteractionData().Type == PlayerInteractionType::PlaceholderType79)
PlayerTalkClass->GetInteractionData().Reset();
//WowCommunity
}
GetSession()->GetBattlenetAccount().RemoveFromWorld();
+6
View File
@@ -2416,6 +2416,12 @@ class TC_GAME_API Player final : public Unit, public GridObject<Player>
// Transient per-session selection from CMSG_SELECT_DELVE_ENTRANCE_TIER (re-sent by client on TIERED_ENTRANCE_OPEN).
uint8 m_delveSelectedTier = 0;
uint32 m_delveSelectedMapId = 0;
// Where DelveMgr::LeaveDelve sends the player back: the map they entered the delve from, at the
// delve_template exit coordinates (12.1 captures: gulf 2694, eversong/deatholme 0 - REPORT.md 1.5/5).
// MAPID_INVALID when no entry was seen (relog inside the delve).
WorldLocation m_delveReturnLocation;
// Instance id that already received the hidden entry quest reward (DelveMgr::OnPlayerEnteredDelve, once per run)
uint32 m_delveEntryRewardInstanceId = 0;
//WowCommunity
protected:
UF::UpdateFieldFlag GetUpdateFieldFlagsFor(Player const* target) const override;
+220 -192
View File
@@ -39,8 +39,11 @@
#include "UpdateData.h"
#include "WorldSession.h"
//WowCommunity
#include "Creature.h"
#include "RecentAlliesMgr.h"
#include "LFG.h"
#include "Map.h"
#include "TemporarySummon.h"
//WowCommunity
Seconds Group::CountdownInfo::GetTimeLeft() const
{
@@ -562,6 +565,15 @@ bool Group::AddMember(Player* player)
bool Group::RemoveMember(ObjectGuid guid, RemoveMethod method /*= GROUP_REMOVEMETHOD_DEFAULT*/, ObjectGuid kicker /*= 0*/, const char* reason /*= nullptr*/)
{
// Follower dungeon bots live in m_memberSlots but are not real players.
// Never run player-leave / disband logic for them.
if (guid.IsAnyTypeCreature())
{
RemoveFollowerBotSlot(guid);
SendUpdate();
return true;
}
BroadcastGroupUpdate();
sScriptMgr->OnGroupRemoveMember(this, guid, method, kicker, reason);
@@ -871,7 +883,7 @@ void Group::SendUpdateToPlayer(Player* player, MemberSlot const* slot /*= nullpt
if (slot->guid == citr->guid)
partyUpdate.MyIndex = index;
Player* member = ObjectAccessor::FindConnectedPlayer(citr->guid);
Player* member = citr->guid.IsPlayer() ? ObjectAccessor::FindConnectedPlayer(citr->guid) : nullptr;
WorldPackets::Party::PartyPlayerInfo& playerInfos = partyUpdate.PlayerList.emplace_back();
@@ -881,7 +893,10 @@ void Group::SendUpdateToPlayer(Player* player, MemberSlot const* slot /*= nullpt
playerInfos.FactionGroup = Player::GetFactionGroupForRace(citr->race);
playerInfos.Connected = member && member->GetSession() && !member->GetSession()->PlayerLogout();
if (citr->guid.IsAnyTypeCreature())
playerInfos.Connected = true;
else
playerInfos.Connected = member && member->GetSession() && !member->GetSession()->PlayerLogout();
playerInfos.Subgroup = citr->group; // groupid
playerInfos.Flags = citr->flags; // See enum GroupMemberFlags
@@ -1895,239 +1910,252 @@ void Group::SetRestrictPingsTo(RestrictPingsTo restrictTo)
//WowCommunity
enum followerbots
{
BOT_HEALER = 209072, //Crenna
BOT_TANK = 209057, //Garrick
BOT_DPS_1 = 209065, //austin
BOT_DPS_2 = 214390, //Shuja Grimaxe
BOT_DPS_3 = 209059, //Meredy
BOT_HEALER = 209072, // Crenna
BOT_TANK = 209057, // Garrick
BOT_DPS_1 = 209065, // Austin
BOT_DPS_2 = 214390, // Shuja Grimaxe
BOT_DPS_3 = 209059, // Meredy
SPELL_DUNGEON_ASSISTENCE = 426756,
};
static uint32 const FollowerBotEntries[] = { BOT_HEALER, BOT_TANK, BOT_DPS_1, BOT_DPS_2, BOT_DPS_3 };
bool Group::IsFollowerDungeon(Player const* player) const
{
if (!player)
return false;
if (player->GetDungeonDifficultyID() == DIFFICULTY_FOLLOWER_DUNGEON)
return true;
if (player->GetMap() && player->GetMap()->GetDifficultyID() == DIFFICULTY_FOLLOWER_DUNGEON)
return true;
auto isFollowerDungeonId = [](uint32 dungeonId) -> bool
{
if (!dungeonId)
return false;
if (LFGDungeonsEntry const* dungeon = sLFGDungeonsStore.LookupEntry(dungeonId))
return dungeon->DifficultyID == DIFFICULTY_FOLLOWER_DUNGEON;
return false;
};
if (isFollowerDungeonId(sLFGMgr->GetDungeon(player->GetGUID())))
return true;
if (isLFGGroup() && isFollowerDungeonId(sLFGMgr->GetDungeon(GetGUID())))
return true;
return false;
}
void Group::AddFollowerModeBots(Player* player)
{
if (!player->GetGroup() || !player->GetGroup()->GetLeaderGUID())
if (!player || player->GetGroup() != this || !GetLeaderGUID())
return;
// Check if this is a follower dungeon - either by difficulty ID or by current map difficulty
bool isFollowerDungeon = false;
if (player->GetGroup()->isLFGGroup())
if (!IsFollowerDungeon(player))
return;
// Only the leader summons, and only once per group.
if (GetLeaderGUID() != player->GetGUID())
return;
if (!m_followerBots.empty())
return;
for (uint32 entry : FollowerBotEntries)
if (player->GetSummonedCreatureByEntry(entry))
return;
player->CastSpell(player, SPELL_DUNGEON_ASSISTENCE);
std::vector<uint32> botsToSummon;
switch (player->GetRoleForGroup())
{
if (player->GetDungeonDifficultyID() == DIFFICULTY_FOLLOWER_DUNGEON)
isFollowerDungeon = true;
else if (player->GetMap() && player->GetMap()->GetDifficultyID() == DIFFICULTY_FOLLOWER_DUNGEON)
isFollowerDungeon = true;
// Also check if the LFG dungeon is marked as follower dungeon
else if (sLFGMgr->GetDungeon(player->GetGUID()))
{
uint32 dungeonId = sLFGMgr->GetDungeon(player->GetGUID());
if (LFGDungeonsEntry const* dungeon = sLFGDungeonsStore.LookupEntry(dungeonId))
{
if (dungeon->DifficultyID == DIFFICULTY_FOLLOWER_DUNGEON)
isFollowerDungeon = true;
}
}
case ROLE_TANK:
botsToSummon = { BOT_HEALER, BOT_DPS_1, BOT_DPS_2, BOT_DPS_3 };
break;
case ROLE_HEALER:
botsToSummon = { BOT_TANK, BOT_DPS_1, BOT_DPS_2, BOT_DPS_3 };
break;
default:
botsToSummon = { BOT_TANK, BOT_HEALER, BOT_DPS_2, BOT_DPS_3 };
break;
}
if (isFollowerDungeon)
static float const offsetX[] = { 1.0f, 2.0f, 1.5f, 2.5f };
static float const offsetY[] = { 1.5f, 2.5f, 1.0f, 1.0f };
for (size_t i = 0; i < botsToSummon.size(); ++i)
{
player->CastSpell(player, SPELL_DUNGEON_ASSISTENCE);
std::vector<uint32> botsToSummon;
if (player->IsTankPlayer())
if (Creature* bot = player->SummonCreature(botsToSummon[i],
player->GetPositionX() + offsetX[i],
player->GetPositionY() + offsetY[i],
player->GetPositionZ(),
player->GetOrientation(), TEMPSUMMON_MANUAL_DESPAWN))
{
botsToSummon = { BOT_HEALER, BOT_DPS_1, BOT_DPS_2, BOT_DPS_3 };
}
else if (player->GetRoleForGroup() == ROLE_HEALER)
{
botsToSummon = { BOT_TANK, BOT_DPS_1, BOT_DPS_2, BOT_DPS_3 };
}
else if (player->GetRoleForGroup() == ROLE_DAMAGE)
{
botsToSummon = { BOT_TANK, BOT_HEALER, BOT_DPS_2, BOT_DPS_3 };
}
float offsetX[] = { 1.0f, 2.0f, 1.5f, 2.5f };
float offsetY[] = { 1.5f, 2.5f, 1.0f, 1.0f };
float offsetZ[] = { 1.0f, -1.0f, 1.5f, 1.5f };
for (size_t i = 0; i < botsToSummon.size(); ++i)
{
if (Creature* bot = player->SummonCreature(botsToSummon[i],
player->GetPositionX() + offsetX[i],
player->GetPositionY() + offsetY[i],
player->GetPositionZ() + offsetZ[i],
0.0f, TEMPSUMMON_MANUAL_DESPAWN))
{
// Make the bot appear as a group member
bot->SetUnitFlag(UNIT_FLAG_PLAYER_CONTROLLED);
bot->SetUnitFlag2(UNIT_FLAG2_REGENERATE_POWER);
// Set bot as friendly to player and group
bot->SetFaction(player->GetFaction());
bot->SetLevel(player->GetLevel());
// Add to group visually (this might need custom implementation)
AddBotToGroupDisplay(bot, player);
// Make bot follow player
bot->GetMotionMaster()->MoveFollow(player, PET_FOLLOW_DIST, bot->GetFollowAngle());
// Set bot's owner
bot->SetOwnerGUID(player->GetGUID());
bot->SetCreatorGUID(player->GetGUID());
}
bot->SetUnitFlag2(UNIT_FLAG2_REGENERATE_POWER);
bot->SetFaction(player->GetFaction());
bot->SetLevel(player->GetLevel());
bot->SetOwnerGUID(player->GetGUID());
bot->SetCreatorGUID(player->GetGUID());
AddBotToGroupDisplay(bot, player);
}
}
}
// Helper function to add bot to group display
void Group::AddBotToGroupDisplay(Creature* bot, Player* owner)
{
if (!bot || !owner)
return;
m_followerBots.push_back(bot->GetGUID());
bot->SetFaction(owner->GetFaction());
bot->SetLevel(owner->GetLevel());
bot->SetOwnerGUID(owner->GetGUID());
bot->SetCreatorGUID(owner->GetGUID());
// Create a fake group member entry
WorldPackets::Party::PartyUpdate partyUpdate;
uint8 roles = lfg::LfgRoles::PLAYER_ROLE_DAMAGE;
switch (bot->GetEntry())
{
case BOT_TANK:
roles = lfg::LfgRoles::PLAYER_ROLE_TANK;
break;
case BOT_HEALER:
roles = lfg::LfgRoles::PLAYER_ROLE_HEALER;
break;
default:
break;
}
// Set bot information
WorldPackets::Party::PartyPlayerInfo botInfo;
botInfo.GUID = bot->GetGUID();
botInfo.Name = bot->GetName();
botInfo.Class = owner->GetClass(); // Use owner's class or set appropriate class
botInfo.Connected = true; // Show as online
botInfo.Subgroup = owner->GetSubGroup();
botInfo.Flags = MEMBER_FLAG_ASSISTANT; // Or appropriate flags
botInfo.RolesAssigned = 0; // Set appropriate role
// Send to all group members
BroadcastPacket(partyUpdate.Write(), false);
AddFollowerMember(bot, roles);
}
void Group::DespawnFollowerModeBots(Player* player)
{
if (Group* group = player->GetGroup())
group->RemoveMember(player->GetGUID());
if (!player)
return;
player->UnsummonCreatureByEntry(BOT_HEALER, 1000);
player->UnsummonCreatureByEntry(BOT_TANK, 1000);
player->UnsummonCreatureByEntry(BOT_DPS_1, 1000);
player->UnsummonCreatureByEntry(BOT_DPS_2, 1000);
player->UnsummonCreatureByEntry(BOT_DPS_3, 1000);
uint32 playerMembers = 0;
for (MemberSlot const& slot : m_memberSlots)
if (slot.guid.IsPlayer())
++playerMembers;
bool const despawnAll = player->GetGUID() == m_leaderGuid || playerMembers <= 1;
auto unsummonBot = [player](ObjectGuid const& guid)
{
if (Map* map = player->GetMap())
if (Creature* bot = map->GetCreature(guid))
if (TempSummon* summon = bot->ToTempSummon())
summon->UnSummon();
};
if (despawnAll)
{
std::vector<ObjectGuid> bots = m_followerBots;
for (ObjectGuid const& guid : bots)
{
RemoveFollowerBotSlot(guid);
unsummonBot(guid);
}
for (uint32 entry : FollowerBotEntries)
player->UnsummonCreatureByEntry(entry, 0);
}
else
{
for (uint32 entry : FollowerBotEntries)
{
if (Creature* bot = player->GetSummonedCreatureByEntry(entry))
{
RemoveFollowerBotSlot(bot->GetGUID());
player->UnsummonCreatureByEntry(entry, 0);
}
}
}
SendUpdate();
}
void Group::RemoveFollowerBotSlot(ObjectGuid guid)
{
member_witerator slot = _getMemberWSlot(guid);
if (slot != m_memberSlots.end())
{
SubGroupCounterDecrease(slot->group);
m_memberSlots.erase(slot);
}
for (auto itr = m_followerBots.begin(); itr != m_followerBots.end();)
{
if (*itr == guid)
itr = m_followerBots.erase(itr);
else
++itr;
}
}
bool Group::AddFollowerMember(Creature* creature, uint8 lfgRoles)
{
if (!creature)
return false;
if (IsMember(creature->GetGUID()))
return true;
if (!isRaidGroup() && GetMembersCount() >= MAX_GROUP_SIZE)
return false;
uint8 subGroup = 0;
if (m_subGroupsCounts)
{
bool groupFound = false;
for (; subGroup < MAX_RAID_SUBGROUPS; ++subGroup)
{
if (m_subGroupsCounts[subGroup] < MAX_GROUP_SIZE)
{
groupFound = true;
break;
}
}
if (!groupFound)
return false;
}
MemberSlot member;
member.guid = creature->GetGUID();
member.name = creature->GetName();
member.race = Races(creature->GetRace());
member._class = creature->GetClass();
member.group = subGroup;
member.flags = 0;
member.roles = lfgRoles;
member.readyChecked = false;
m_memberSlots.push_back(member);
if (std::ranges::find(m_followerBots, creature->GetGUID()) == m_followerBots.end())
m_followerBots.push_back(creature->GetGUID());
SubGroupCounterIncrease(subGroup);
SendUpdate();
return true;
}
bool Group::AddFollowerMemberTank(Creature* creature)
{
// Get first not-full group
uint8 subGroup = 0;
if (m_subGroupsCounts)
{
bool groupFound = false;
for (; subGroup < MAX_RAID_SUBGROUPS; ++subGroup)
{
if (m_subGroupsCounts[subGroup] < MAX_GROUP_SIZE)
{
groupFound = true;
break;
}
}
// We are raid group and no one slot is free
if (!groupFound)
return false;
}
MemberSlot member;
member.guid = creature->GetGUID();
member.name = creature->GetName();
member.group = subGroup;
member.flags = 0;
member.roles = ROLE_TANK;
m_memberSlots.push_back(member);
SubGroupCounterIncrease(subGroup);
SendUpdate();
sScriptMgr->OnGroupAddMember(this, creature->GetGUID());
return true;
return AddFollowerMember(creature, lfg::LfgRoles::PLAYER_ROLE_TANK);
}
bool Group::AddFollowerMemberDPS(Creature* creature)
{
// Get first not-full group
uint8 subGroup = 0;
if (m_subGroupsCounts)
{
bool groupFound = false;
for (; subGroup < MAX_RAID_SUBGROUPS; ++subGroup)
{
if (m_subGroupsCounts[subGroup] < MAX_GROUP_SIZE)
{
groupFound = true;
break;
}
}
// We are raid group and no one slot is free
if (!groupFound)
return false;
}
MemberSlot member;
member.guid = creature->GetGUID();
member.name = creature->GetName();
member.group = subGroup;
member.flags = 0;
member.roles = ROLE_DAMAGE;
m_memberSlots.push_back(member);
SubGroupCounterIncrease(subGroup);
SendUpdate();
sScriptMgr->OnGroupAddMember(this, creature->GetGUID());
return true;
return AddFollowerMember(creature, lfg::LfgRoles::PLAYER_ROLE_DAMAGE);
}
bool Group::AddFollowerMemberHealer(Creature* creature)
{
// Get first not-full group
uint8 subGroup = 0;
if (m_subGroupsCounts)
{
bool groupFound = false;
for (; subGroup < MAX_RAID_SUBGROUPS; ++subGroup)
{
if (m_subGroupsCounts[subGroup] < MAX_GROUP_SIZE)
{
groupFound = true;
break;
}
}
// We are raid group and no one slot is free
if (!groupFound)
return false;
}
MemberSlot member;
member.guid = creature->GetGUID();
member.name = creature->GetName();
member.group = subGroup;
member.flags = 0;
member.roles = ROLE_HEALER;
m_memberSlots.push_back(member);
SubGroupCounterIncrease(subGroup);
SendUpdate();
sScriptMgr->OnGroupAddMember(this, creature->GetGUID());
return true;
return AddFollowerMember(creature, lfg::LfgRoles::PLAYER_ROLE_HEALER);
}
//WowCommunity
+3
View File
@@ -429,6 +429,9 @@ class TC_GAME_API Group
private:
std::vector<ObjectGuid> m_followerBots;
bool AddFollowerMember(Creature* creature, uint8 lfgRoles);
void RemoveFollowerBotSlot(ObjectGuid guid);
bool IsFollowerDungeon(Player const* player) const;
//WowCommunity
protected:
+94 -65
View File
@@ -17,7 +17,7 @@
#include "WorldSession.h"
#include "Creature.h"
#include "DB2Stores.h"
#include "DatabaseEnv.h"
#include "DelveMgr.h"
#include "DelvesDefines.h"
#include "DelvesPackets.h"
@@ -26,7 +26,10 @@
#include "Group.h"
#include "Log.h"
#include "Map.h"
#include "ObjectAccessor.h"
#include "Player.h"
#include "QueryCallback.h"
#include "StringFormat.h"
void WorldSession::HandleDelveTeleportOut(WorldPackets::Delves::DelveTeleportOut& /*delveTeleportOut*/)
{
@@ -36,9 +39,65 @@ void WorldSession::HandleDelveTeleportOut(WorldPackets::Delves::DelveTeleportOut
TC_LOG_DEBUG("network", "CMSG_DELVE_TELEPORT_OUT received from player {}", player->GetName());
// Teleport player out of the delve instance to their bind point
if (player->GetMap()->Instanceable())
player->TeleportTo(player->m_homebind);
// Never sent by the 12.x client in any of the five captured runs (REPORT.md 1.1 / 5); retail leaves through the
// Leave-O-Bot spell-click or the "Leave Delve" object, both of which call DelveMgr::LeaveDelve from scripts.
// Kept as a safety net with the same exit semantics (originating map, template exit, homebind).
if (sDelveMgr->GetDelveTemplate(player->GetMapId()))
sDelveMgr->LeaveDelve(player);
}
void WorldSession::HandleRequestPartyEligibilityForDelveTiers(WorldPackets::Delves::RequestPartyEligibilityForDelveTiers& packet)
{
Player* player = GetPlayer();
if (!player)
return;
TC_LOG_DEBUG("network", "CMSG_REQUEST_PARTY_ELIGIBILITY_FOR_DELVE_TIERS received from player {} for mapId {}",
player->GetName(), packet.MapID);
// REPORT.md 1.1 / 6.1: SMSG_PARTY_ELIGIBILITY_FOR_DELVE_TIERS_RESPONSE was never sent in any of the four solo
// runs (0 frames in five captures) - the client sends the request right after the picker opens (gulf 2964,
// eversong 2952) and gets nothing back. Only a party has rows to fill in.
Group const* group = player->GetGroup();
if (!group || group->isRaidGroup())
return;
// One packet per member (68275: PackedGUID + uint32 + uint32 + bool, no count framing; semantics UNVERIFIED, see
// DelvesPackets.h). Tier progress lives in delve_progress per battlenet account - read it asynchronously, one
// query per member, and answer from the callback; no synchronous DB round trip on the world thread.
for (GroupReference const& itr : group->GetMembers())
{
Player const* member = itr.GetSource();
if (!member || !member->GetSession())
continue;
ObjectGuid const memberGuid = member->GetGUID();
uint8 const levelEligible = Delves::DelvesSeason::MeetsMinimumLevelRequirement(member) ? 1 : 0;
// same SELECT as CHAR_SEL_DELVE_PROGRESS, which is prepared on the synchronous connection only
std::string const query = Trinity::StringFormat("SELECT highestTierUnlocked FROM delve_progress WHERE battlenetAccountId = {}",
member->GetSession()->GetBattlenetAccountId());
GetQueryProcessor().AddCallback(CharacterDatabase.AsyncQuery(query.c_str()).WithCallback([this, memberGuid, levelEligible](QueryResult result)
{
if (!GetPlayer() || !GetPlayer()->IsInWorld())
return;
// default progress: tiers 1-3 are open for every fresh character (REPORT.md 1.3 unlock state)
uint8 highestTierUnlocked = Delves::DelveProgress().HighestTierUnlocked;
if (result)
highestTierUnlocked = (*result)[0].GetUInt8();
uint8 const maxTier = levelEligible ? std::min<uint8>(highestTierUnlocked, Delves::MAX_DELVE_TIER) : 0;
WorldPackets::Delves::PartyEligibilityForDelveTiersResponse response;
response.PlayerGUID = memberGuid;
response.MaxEligibleTier = maxTier;
response.ReasonOrFlags = 0; // UNVERIFIED - needs a group capture
response.IsEligible = maxTier > 0; // UNVERIFIED - needs a group capture
SendPacket(response.Write());
}));
}
}
void WorldSession::HandleSelectDelveEntranceTier(WorldPackets::Delves::SelectDelveEntranceTier& packet)
@@ -47,11 +106,11 @@ void WorldSession::HandleSelectDelveEntranceTier(WorldPackets::Delves::SelectDel
if (!player)
return;
TC_LOG_DEBUG("network", "CMSG_SELECT_DELVE_ENTRANCE_TIER received from player {} mapId {} tier {}",
player->GetName(), packet.MapID, packet.Tier);
TC_LOG_DEBUG("network", "CMSG_SELECT_DELVE_ENTRANCE_TIER received from player {} entrance {} tier {}",
player->GetName(), packet.EntranceGUID.ToString(), packet.Tier);
// 12.1 wire: the client echoes the TieredEntranceTierID the picker advertised (75..85 on the
// 12.1 delve season), not the 1-based tier. Accept a bare tier number too (legacy pickers).
// 12.1 delve season), not the 1-based tier. Accept a bare tier number too (legacy pickers).
uint8 tier = 0;
if (Delves::TieredEntranceTierData const* tierRow = sDelveMgr->GetTieredEntranceTier(packet.Tier))
tier = tierRow->Tier;
@@ -69,10 +128,25 @@ void WorldSession::HandleSelectDelveEntranceTier(WorldPackets::Delves::SelectDel
if (tier > progress.HighestTierUnlocked)
return;
// Selection is consumed by the subsequent CMSG_TIERED_ENTRANCE_OPEN flow; the client
// re-sends the tier on entrance. We accept and validate here so eligibility is logged.
player->m_delveSelectedTier = tier;
player->m_delveSelectedMapId = packet.MapID;
// The wire carries the entrance ObjectGuid, not a MapID - re-derive the delve from the entrance spawn
// (DelveMgr::GetDelveTemplateForEntrance; Creature::GetGossipMenuId() is always 0, see DelveMgr.cpp).
Delves::DelveTemplate const* tmpl = nullptr;
if (packet.EntranceGUID.IsCreatureOrVehicle())
if (Creature const* entrance = ObjectAccessor::GetCreature(*player, packet.EntranceGUID))
tmpl = sDelveMgr->GetDelveTemplateForEntrance(entrance);
if (!tmpl)
{
TC_LOG_DEBUG("network", "CMSG_SELECT_DELVE_ENTRANCE_TIER: could not resolve entrance {} to a delve template",
packet.EntranceGUID.ToString());
return;
}
// REPORT.md 1.4 - 1.6 (work item 5): the select IS the entry. Retail answers it with the phase shift,
// SMSG_SUSPEND_TOKEN + SMSG_PLAYER_CHOICE_CLEAR and, ~1.9 s later, a seamless SMSG_NEW_WORLD (reason 21, no
// SMSG_TRANSFER_PENDING) to the delve's entry position (gulf 100129 -> 101829). Nothing else is requested from
// the client in between. Shared with the entrance gossip script.
sDelveMgr->EnterDelve(player, *tmpl, tier);
}
void WorldSession::HandleTieredEntranceOpen(WorldPackets::Delves::TieredEntranceOpen& packet)
@@ -84,62 +158,17 @@ void WorldSession::HandleTieredEntranceOpen(WorldPackets::Delves::TieredEntrance
TC_LOG_DEBUG("network", "CMSG_TIERED_ENTRANCE_OPEN received from player {} entrance {}",
player->GetName(), packet.EntranceGUID.ToString());
Delves::DelveTemplate const* tmpl = nullptr;
if (packet.EntranceGUID.IsCreatureOrVehicle())
if (Creature const* entrance = ObjectAccessor::GetCreature(*player, packet.EntranceGUID))
tmpl = sDelveMgr->GetDelveTemplateByGossipMenuId(entrance->GetGossipMenuId());
if (!tmpl)
// REPORT.md 1.1: in 12.1 delve entrances (EntranceType 1) open by proximity - the server pushes the picker
// (DelveMgr::OpenEntranceByProximity from the entrance AI). This CMSG is still sent by click-to-open entrances
// (69273 Sites witness: creature 264322 -> map 3075 "Naigtal") and is kept for them; the response is built by
// the same code either way.
Creature const* entrance = packet.EntranceGUID.IsCreatureOrVehicle() ? ObjectAccessor::GetCreature(*player, packet.EntranceGUID) : nullptr;
if (!entrance)
{
TC_LOG_DEBUG("network", "CMSG_TIERED_ENTRANCE_OPEN: could not resolve entrance {} to a delve template",
packet.EntranceGUID.ToString());
TC_LOG_DEBUG("network", "CMSG_TIERED_ENTRANCE_OPEN: entrance {} is not a creature in range of player {}",
packet.EntranceGUID.ToString(), player->GetName());
return;
}
Delves::DelveProgress progress;
Delves::DelvesRewards::LoadProgress(GetBattlenetAccountId(), progress);
bool meetsLevel = Delves::DelvesSeason::MeetsMinimumLevelRequirement(player);
// 12.1.0.69497 captures (Gulf of Memory, Shadow Enclave; 12.0.7 The Darkway agrees): every delve
// entrance reports EntranceType 1, field 7 is 234 for every delve, field 6 is 0, fields 3/4/8 and
// the widget sets are per entrance (delve_template), the tier rows are season data
// (delve_tiered_entrance_tier). The client matches the response by the echoed entrance GUID.
WorldPackets::Delves::TieredEntranceOpenResponse response;
response.EntranceGUID = packet.EntranceGUID;
response.EntranceType = Delves::TIERED_ENTRANCE_TYPE_DELVE;
response.MapID = tmpl->MapId;
response.Unknown3 = tmpl->TieredEntranceUnknown3;
response.Unknown4 = tmpl->EntranceUiWidgetSetId;
response.Unknown6 = 0;
response.Unknown7 = Delves::DELVE_TIERED_ENTRANCE_FIELD7;
response.Unknown8 = tmpl->TieredEntranceId;
if (MapEntry const* mapEntry = sMapStore.LookupEntry(tmpl->MapId))
response.EntranceDescription = mapEntry->MapName[GetSessionDbcLocale()];
std::vector<Delves::TieredEntranceTierData> const& tiers = sDelveMgr->GetTieredEntranceTiers();
response.Tiers.reserve(tiers.size());
for (Delves::TieredEntranceTierData const& tierRow : tiers)
{
WorldPackets::Delves::TieredEntranceTier& tierData = response.Tiers.emplace_back();
tierData.TieredEntranceTierID = tierRow.Id;
tierData.Tier = tierRow.Tier;
tierData.SuggestedILvl = tierRow.SuggestedILvl;
tierData.OverrideTooltipSpellID = tierRow.OverrideTooltipSpellId;
tierData.UnlockPlayerConditionID = tierRow.UnlockPlayerConditionId;
tierData.DynamicUnlockPlayerConditionID = tierRow.DynamicUnlockPlayerConditionId;
tierData.ModifierUIWidgetSetID = tmpl->ModifierUiWidgetSetTier1 && tierRow.Tier ? tmpl->ModifierUiWidgetSetTier1 - (tierRow.Tier - 1) : 0;
tierData.Unlocked = meetsLevel && tierRow.Tier <= progress.HighestTierUnlocked;
tierData.TierDescription = tierRow.Description;
for (Delves::TieredEntranceRewardData const& reward : tierRow.Rewards)
{
WorldPackets::Delves::TieredEntranceReward& rewardData = tierData.PreviewTreasureList.emplace_back();
rewardData.RewardType = reward.RewardType;
rewardData.Id = reward.Id;
rewardData.Quantity = reward.Quantity;
rewardData.Context = reward.Context;
}
}
SendPacket(response.Write());
sDelveMgr->SendTieredEntranceOpen(player, entrance);
}
-44
View File
@@ -1447,50 +1447,6 @@ void WorldSession::HandleAddonList(WorldPackets::Addon::AddonList& addonList)
addon.Name, addon.Version, addon.Loaded, addon.Disabled);
}
void WorldSession::HandleRequestPartyEligibilityForDelveTiers(WorldPackets::Misc::RequestPartyEligibilityForDelveTiers& packet)
{
Player* player = GetPlayer();
if (!player)
return;
TC_LOG_DEBUG("network", "CMSG_REQUEST_PARTY_ELIGIBILITY_FOR_DELVE_TIERS received from player {} for mapId {}",
player->GetName(), packet.MapID);
auto computeMaxEligibleTier = [&](Player const* member) -> uint8
{
if (!Delves::DelvesSeason::MeetsMinimumLevelRequirement(member))
return 0;
Delves::DelveProgress progress;
Delves::DelvesRewards::LoadProgress(member->GetSession()->GetBattlenetAccountId(), progress);
return std::min<uint8>(progress.HighestTierUnlocked, Delves::MAX_DELVE_TIER);
};
auto sendForMember = [&](Player const* member)
{
WorldPackets::Delves::PartyEligibilityForDelveTiersResponse response;
response.PlayerName = member->GetName();
response.MaxEligibleTier = computeMaxEligibleTier(member);
SendPacket(response.Write());
};
// Always emit at least the requesting player so the client populates its own row.
sendForMember(player);
if (Group const* group = player->GetGroup())
{
if (group->isRaidGroup())
return;
for (GroupReference const& itr : group->GetMembers())
{
Player const* member = itr.GetSource();
if (!member || member == player)
continue;
sendForMember(member);
}
}
}
void WorldSession::HandleRafClaimActivityReward(WorldPackets::RaF::RafClaimActivityReward& /*rafClaimActivityReward*/)
{
// ClaimRafActivity(packet.ActivityID);
+1 -1
View File
@@ -487,7 +487,7 @@ namespace ScriptHelpers
return;
WorldPackets::Delves::ShowDelvesCompanionConfigurationUI configUI;
configUI.CreatureOrSpellID = companionConfigValue;
configUI.Unknown = companionConfigValue;
player->GetSession()->SendPacket(configUI.Write());
}
@@ -21,9 +21,16 @@ namespace WorldPackets
{
namespace Delves
{
void SelectDelveEntranceTier::Read()
void RequestPartyEligibilityForDelveTiers::Read()
{
_worldPacket >> MapID;
}
void SelectDelveEntranceTier::Read()
{
// 68275 wire: PackedGUID entranceGuid + uint32 tier (sender 0x7FF729155A10).
_worldPacket >> EntranceGUID;
_worldPacket >> Tier;
}
@@ -35,29 +42,30 @@ namespace WorldPackets
return &_worldPacket;
}
WorldPacket const* DelvesAccountDataElementChanged::Write()
{
_worldPacket << uint32(DataElementID);
_worldPacket << uint32(Value);
return &_worldPacket;
}
// DelvesAccountDataElementChanged intentionally has no class ? PDE state is
// delivered to the client via ActivePlayer UpdateFields, not a dedicated SMSG.
// See DelvesPackets.h for the IDA-traced reasoning.
WorldPacket const* ShowDelvesCompanionConfigurationUI::Write()
{
_worldPacket << uint32(CreatureOrSpellID);
// 12.1.0.69497 wire (gulf 1096575, eversong 2688539; 12.0.1 deatholme 75171): one uint32.
_worldPacket << uint32(Unknown);
return &_worldPacket;
}
WorldPacket const* PartyEligibilityForDelveTiersResponse::Write()
{
_worldPacket.WriteBits(PlayerName.size(), 6);
// 68275 wire (read ctor 0x7FF7290BBA40): PackedGUID + uint32 + uint32 + bool(MSB).
// One member per packet ? no count framing. Field semantics UNVERIFIED ? see header.
_worldPacket << PlayerGUID;
_worldPacket << uint32(MaxEligibleTier);
_worldPacket << uint32(ReasonOrFlags);
_worldPacket.WriteBit(IsEligible);
_worldPacket.FlushBits();
_worldPacket.WriteString(PlayerName);
_worldPacket << uint8(MaxEligibleTier);
return &_worldPacket;
}
void TieredEntranceOpen::Read()
{
// 68275 wire: PackedGuid only (12B observed; sender 0x7FF7291559C0).
+71 -28
View File
@@ -28,7 +28,8 @@ namespace WorldPackets
namespace Delves
{
// CMSG_DELVE_TELEPORT_OUT (0x3B012C)
// CMSG_DELVE_TELEPORT_OUT (0x3B012E @ 12.0.7.68275)
// 68275 binary (sender 0x7FF7291558F0): empty body, opcode only.
class DelveTeleportOut final : public ClientPacket
{
public:
@@ -37,8 +38,30 @@ namespace WorldPackets
void Read() override {}
};
// CMSG_SELECT_DELVE_ENTRANCE_TIER (0x3B0134)
// Lua: C_DelvesUI.SelectDelveEntranceTier(tier). Client wraps with active PDE MapID.
// CMSG_REQUEST_PARTY_ELIGIBILITY_FOR_DELVE_TIERS (0x3A02F0 @ 12.0.7.68275; was 0x3A02F4 at 67186)
// Lua signature: C_DelvesUI.RequestPartyEligibilityForDelveTiers(mapID)
// 68275 binary (sender 0x7FF72914CAB0) confirms 4-byte payload (uint32 MapID only);
// matches the build-66562 sniff.
class RequestPartyEligibilityForDelveTiers final : public ClientPacket
{
public:
explicit RequestPartyEligibilityForDelveTiers(WorldPacket&& packet) : ClientPacket(CMSG_REQUEST_PARTY_ELIGIBILITY_FOR_DELVE_TIERS, std::move(packet)) {}
void Read() override;
uint32 MapID = 0;
};
// CMSG_SELECT_DELVE_ENTRANCE_TIER (0x3B0134 @ 12.0.7.68275)
// Lua signature: C_DelvesUI.SelectDelveEntranceTier(tier) ? single Lua arg.
//
// Wire resolved from the 68275 binary (sender 0x7FF729155A10, opcode immediate
// at 0x7FF729155A23): PackedGUID entranceGuid + uint32 tier. The "16-byte struct
// copied from the 40-entry table" of the earlier 67186 read is that ObjectGuid.
// Tier is uint32, not uint8. The GUID is believed to be the delve entrance/POI
// object the picker is bound to ? // UNVERIFIED ? needs sniff (the field widths
// and order ARE certain; only the GUID's referent is inferred). The server
// re-derives the delve MapID from this GUID in HandleSelectDelveEntranceTier.
class SelectDelveEntranceTier final : public ClientPacket
{
public:
@@ -46,11 +69,12 @@ namespace WorldPackets
void Read() override;
uint32 MapID = 0;
uint8 Tier = 0;
ObjectGuid EntranceGUID;
uint32 Tier = 0;
};
// SMSG_SHOW_DELVES_DISPLAY_UI (build 67186 = 0x420359)
// SMSG_SHOW_DELVES_DISPLAY_UI (0x420359 @ 12.0.7.68275)
// 68275 binary (read ctor 0x7FF7290BB840): empty body (remaining-span, 0-length in practice).
class ShowDelvesDisplayUI final : public ServerPacket
{
public:
@@ -61,22 +85,31 @@ namespace WorldPackets
uint32 Unknown = 0; // 12.1 wire: one uint32 (2796, 2742 observed; UiMap/Map id range, meaning unverified)
};
// SMSG_DELVES_ACCOUNT_DATA_ELEMENT_CHANGED (build 67186 = 0x42035A)
// Wire: uint32 DataElementID, uint32 Value (per IDA-decoded JamSMsgDelvesAccountDataElementChanged).
class DelvesAccountDataElementChanged final : public ServerPacket
{
public:
explicit DelvesAccountDataElementChanged() : ServerPacket(SMSG_DELVES_ACCOUNT_DATA_ELEMENT_CHANGED, 8) {}
// SMSG_DELVES_ACCOUNT_DATA_ELEMENT_CHANGED (0x42035A @ 12.0.7.68275)
//
// Intentionally NO packet class. PlayerDataElement (PDE) state on the client is
// stored on CGActivePlayer_C in two `(vector<PlayerDataElement>, vector<uint32>)`
// fields (decompiled from `sub_7FF75C204150`, the CGActivePlayer destructor;
// fields at qword offsets 651 and 658 ? Account and Character respectively). The
// Lua event `DELVES_ACCOUNT_DATA_ELEMENT_CHANGED` is broadcast by mirror handlers
// registered against those vector fields with the signature
// void(CGActivePlayer_C&, PlayerDataElement const& oldElem,
// PlayerDataElement const& newElem, unsigned int idx)
// (typename string at `0x7FF75F3A9AE0` in IDA build 66198). Mirror handlers fire
// from UpdateField changes ? server-side we populate the ActivePlayer UpdateFields
// (AccountDataElements / CharacterDataElements / DelveData) inside SMSG_UPDATE_OBJECT,
// which drives the client event automatically.
// 68275 note: the dedicated SMSG's read ctor (0x7FF7290BB8C0) captures the entire
// remaining payload as an opaque account-data element blob that is fed to the same
// CGActivePlayer mirror deserializer (0x7FF72920BCF0) ? i.e. it is an alternative
// runtime delta channel for the same mirror stream. The exact blob framing is
// // UNVERIFIED ? needs sniff; we deliver via SMSG_UPDATE_OBJECT instead.
WorldPacket const* Write() override;
uint32 DataElementID = 0;
uint32 Value = 0;
};
// SMSG_SHOW_DELVES_COMPANION_CONFIGURATION_UI (build 67186 = 0x42035B)
// Sniff confirms 4-byte payload ? value matches a creature/spell ID.
// Lua doc: "Signaled when SpellScript indicates that a curio has been learned or upgraded."
// SMSG_SHOW_DELVES_COMPANION_CONFIGURATION_UI (0x42035B @ 12.0.7.68275)
// 68275 binary (read ctor 0x7FF7290BB940): the client reads an EMPTY body (a
// remaining-bytes span expected to be 0-length). The earlier 66709 sniff's 4-byte
// payload is ignored by the 68275 reader (trailing bytes are benign), so the
// packet carries no fields. UI-trigger only.
class ShowDelvesCompanionConfigurationUI final : public ServerPacket
{
public:
@@ -84,21 +117,31 @@ namespace WorldPackets
WorldPacket const* Write() override;
uint32 CreatureOrSpellID = 0;
uint32 Unknown = 0; // 12.1 wire: one uint32 (271132 / 249219 / 249222 observed after a delve; meaning unverified)
};
// SMSG_PARTY_ELIGIBILITY_FOR_DELVE_TIERS_RESPONSE (build 67186 = 0x42035D)
// Lua event payload: (playerName: string, maxEligibleLevel: number) ? one event firing per packet.
// Wire: TC strings use bit-length prefix, then bytes. Sent once per evaluated party member.
// SMSG_PARTY_ELIGIBILITY_FOR_DELVE_TIERS_RESPONSE (0x42035D @ 12.0.7.68275)
// 68275 binary (read ctor 0x7FF7290BBA40) reads exactly, in order:
// PackedGUID + uint32 + uint32 + uint8 (bool = byte>>7)
// There is NO count/array framing ? the packet carries a single member entry, so
// the server sends one packet per party member. The Lua event
// PARTY_ELIGIBILITY_FOR_DELVE_TIERS_CHANGED carries (playerName, maxEligibleLevel);
// the name is resolved client-side from the GUID.
// Semantics of the two uint32s and the bool are // UNVERIFIED ? needs sniff.
// Best-hypothesis mapping used here: first uint32 = max eligible tier (matches the
// Lua event's maxEligibleLevel), second uint32 = ineligibility reason/flags (0 when
// eligible), bool = is-eligible. Wire widths/order ARE certain.
class PartyEligibilityForDelveTiersResponse final : public ServerPacket
{
public:
explicit PartyEligibilityForDelveTiersResponse() : ServerPacket(SMSG_PARTY_ELIGIBILITY_FOR_DELVE_TIERS_RESPONSE, 64) {}
explicit PartyEligibilityForDelveTiersResponse() : ServerPacket(SMSG_PARTY_ELIGIBILITY_FOR_DELVE_TIERS_RESPONSE, 16 + 2 + 4 + 4 + 1) {}
WorldPacket const* Write() override;
std::string PlayerName;
uint8 MaxEligibleTier = 0;
ObjectGuid PlayerGUID;
uint32 MaxEligibleTier = 0; // UNVERIFIED ? needs sniff (client field +0x30)
uint32 ReasonOrFlags = 0; // UNVERIFIED ? needs sniff (client field +0x34)
bool IsEligible = false; // UNVERIFIED ? needs sniff (client field +0x38, wire byte MSB)
};
// CMSG_TIERED_ENTRANCE_OPEN (0x3B0133 @ 12.0.7.68275)
@@ -962,11 +962,6 @@ void ClaimWeeklyReward::Read()
_worldPacket >> RewardID;
}
void RequestPartyEligibilityForDelveTiers::Read()
{
_worldPacket >> MapID;
}
void MythicPlusRequestMapStats::Read()
{
_worldPacket >> NpcGUID;
@@ -1246,16 +1246,6 @@ namespace WorldPackets
int32 RewardID = 0;
};
class RequestPartyEligibilityForDelveTiers final : public ClientPacket
{
public:
RequestPartyEligibilityForDelveTiers(WorldPacket&& packet) : ClientPacket(CMSG_REQUEST_PARTY_ELIGIBILITY_FOR_DELVE_TIERS, std::move(packet)) {}
void Read() override;
uint32 MapID = 0;
};
class MythicPlusRequestMapStats final : public ClientPacket
{
public:
+2 -2
View File
@@ -278,6 +278,7 @@ namespace WorldPackets
namespace Delves
{
class DelveTeleportOut;
class RequestPartyEligibilityForDelveTiers;
class SelectDelveEntranceTier;
class TieredEntranceOpen;
}
@@ -848,7 +849,6 @@ namespace WorldPackets
class AccountStoreBeginPurchaseOrRefund;
class OverrideScreenFlash;
class GlobalGetChallengeModeRewards;
class RequestPartyEligibilityForDelveTiers;
class SetPreferredCemetery;
class CloseTraitSystemInteraction;
class ReportStuckInCombat;
@@ -2470,7 +2470,6 @@ public:
//WowCommunity
void HandleChallengeModeRequestLeaders(WorldPackets::ChallengeMode::RequestLeaders& request);
void HandleRequestPartyEligibilityForDelveTiers(WorldPackets::Misc::RequestPartyEligibilityForDelveTiers& packet);
// Adventure Journal
void HandleAdventureJournalOpenQuest(WorldPackets::AdventureJournal::AdventureJournalOpenQuest& openQuest);
@@ -2609,6 +2608,7 @@ public:
// Delves
void HandleDelveTeleportOut(WorldPackets::Delves::DelveTeleportOut& delveTeleportOut);
void HandleRequestPartyEligibilityForDelveTiers(WorldPackets::Delves::RequestPartyEligibilityForDelveTiers& requestPartyEligibilityForDelveTiers);
void HandleSelectDelveEntranceTier(WorldPackets::Delves::SelectDelveEntranceTier& selectDelveEntranceTier);
void HandleTieredEntranceOpen(WorldPackets::Delves::TieredEntranceOpen& tieredEntranceOpen);
+4
View File
@@ -134,6 +134,7 @@
#include "RitualSiteMgr.h"
#include "AbyssAnglersMgr.h"
#include "TurbulentTimewaysMgr.h"
#include "DelvesRewards.h"
//WowCommunity
#include <zlib.h>
@@ -3346,6 +3347,9 @@ void World::ResetWeeklyQuests()
// state (vault run history, vault claim, keystone adjustment, affix week index) is keyed on that boundary.
sChallengeModeMgr.OnWeeklyReset();
// Delves: weekly completion / bountiful / coffer-shard counters roll over with the weekly reset
// (REPORT.md 6.4: ResetAllWeeklyProgress had no caller, so the counters never reset)
Delves::DelvesRewards::ResetAllWeeklyProgress();
//WowCommunity
// reselect pools
+1 -1
View File
@@ -179,7 +179,7 @@ public:
handler->PSendSysMessage("Highest Tier This Week: {}", progress.HighestTierThisWeek);
handler->PSendSysMessage("Weekly Bountiful: {}", progress.WeeklyBountifulCount);
handler->PSendSysMessage("Weekly Coffer Shards: {}/{}", progress.WeeklyCofferShards, Delves::MAX_COFFER_KEY_SHARDS_PER_WEEK);
handler->PSendSysMessage("Great Vault Slots: {}", Delves::DelvesRewards::GetGreatVaultSlotCount(progress.WeeklyCompletions));
//handler->PSendSysMessage("Great Vault Slots: {}", Delves::DelvesRewards::GetGreatVaultSlotCount(progress.WeeklyCompletions));
return true;
}
@@ -0,0 +1,106 @@
/*
* 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/>.
*/
/*
* The Darkway - map 3003, difficulty 208, scenario 3184, LFGDungeons 3083, AreaTable 16642, entered from map 0.
* Evidence: REPORT.md 2.2 / 6.5 and the shadowmoon 12.0.7.68974 capture, which ENDS INSIDE the delve during step
* 16101 - so the boss fight, the completion and the reward objects were never observed for this map.
*
* Scenario 3184 steps: 16133 "Technician Mireille spoken to" (GameEvent 98477) -> 16100 "Sabotaged Ley Lines
* examined" (GameEvent 101992) -> 16102 "Voidbreaker Oglok slain" (kill 252102) -> 16101 "Ley Line Focusers"
* (CastSpell 1256207 x6 / GameEvent 101861 x6) -> 16103 "Infiltrator Gulkat slain" (criteria 60399 / GameEvent 85913).
*
* The final boss is DungeonEncounter 3361 "Infiltrator Gulkat" (DungeonEncounter.db2, MapID 3003). The world DB has
* THREE creature_template rows of that name - 251015, 251600, 256817 - and no capture shows which one the delve
* spawns, so delve_template.finalBossEntry stays 0 and the death of any of the three counts (IsFinalBoss override).
* Reward object placements are unknown for this map; DelveInstanceScript falls back to the boss's death position.
*/
#include "delves_common.h"
#include "Creature.h"
#include "ScriptMgr.h"
namespace
{
static char const* const TheDarkwayScriptName = "instance_the_darkway_delve";
enum TheDarkwayData
{
BOSS_INFILTRATOR_GULKAT = 0,
MAX_ENCOUNTER
};
enum TheDarkwayIds : uint32
{
DUNGEON_ENCOUNTER_INFILTRATOR_GULKAT = 3361,
};
// creature_template rows named "Infiltrator Gulkat" (tch_ws_world) - the capture never reached him
constexpr uint32 InfiltratorGulkatEntries[] = { 251015, 251600, 256817 };
DungeonEncounterData const encounters[] =
{
{ BOSS_INFILTRATOR_GULKAT, {{ DUNGEON_ENCOUNTER_INFILTRATOR_GULKAT }} }
};
class instance_the_darkway_delve : public InstanceMapScript
{
public:
instance_the_darkway_delve() : InstanceMapScript(TheDarkwayScriptName, 3003) { }
struct instance_the_darkway_delve_InstanceScript : public Delves::DelveInstanceScript
{
instance_the_darkway_delve_InstanceScript(InstanceMap* map)
: DelveInstanceScript(map, 1 /* tier resolved at OnPlayerEnter from m_delveSelectedTier */)
{
SetBossNumber(MAX_ENCOUNTER);
LoadDungeonEncounterData(encounters);
}
bool IsFinalBoss(Creature const* creature) const override
{
for (uint32 entry : InfiltratorGulkatEntries)
if (creature->GetEntry() == entry)
return true;
return false;
}
void OnUnitDeath(Unit* unit) override
{
// No boss AI is bound (entry unknown): close the encounter here so SMSG_BOSS_KILL 3361 goes out.
if (Creature const* creature = unit ? unit->ToCreature() : nullptr)
if (IsFinalBoss(creature) && GetBossState(BOSS_INFILTRATOR_GULKAT) != DONE)
SetBossState(BOSS_INFILTRATOR_GULKAT, DONE);
DelveInstanceScript::OnUnitDeath(unit);
}
};
InstanceScript* GetInstanceScript(InstanceMap* map) const override
{
return new instance_the_darkway_delve_InstanceScript(map);
}
};
} // anonymous namespace
void AddSC_instance_the_darkway_delve()
{
new instance_the_darkway_delve();
}
@@ -0,0 +1,50 @@
/*
* 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/>.
*/
/*
* The Gulf of Memory (map 2964) - Mul'tha'ul, creature 250939, DungeonEncounter 3359.
*
* gulf 12.1.0.69497: SMSG_ENCOUNTER_START 3359 / INSTANCE_ENCOUNTER_START / ENGAGE_UNIT at 969466 (ws 24836 = 1),
* SMSG_INSTANCE_ENCOUNTER_DISENGAGE_UNIT + SMSG_ENCOUNTER_END + SMSG_BOSS_KILL 3359 + INSTANCE_ENCOUNTER_END at
* 1062320 (ws 24836 = 0). BossAI + InstanceScript::SetBossState produce exactly those frames; the scenario
* completion that follows (PROGRESS 60399 -> SCENARIO_COMPLETED 3177) is raised by DelveInstanceScript::OnUnitDeath
* through delve_template.finalBossEntry = 250939.
*
* No combat kit: not one of his spells is recoverable from the client DB2s or from the capture's SPELL_START frames
* by caster entry at this point (REPORT.md 4 lists only the creature census). He keeps default melee behaviour.
*/
#include "ScriptedCreature.h"
#include "ScriptMgr.h"
namespace
{
// Matches BOSS_MULTHAUL in instance_gulf_of_memory_delve.cpp (SetBossNumber(1)).
constexpr uint32 BOSS_MULTHAUL = 0;
// struct boss_multhaul : public BossAI
// {
// boss_multhaul(Creature* creature) : BossAI(creature, BOSS_MULTHAUL) { }
// };
} // anonymous namespace
void AddSC_gulf_of_memory_encounters()
{
//RegisterCreatureAI(boss_multhaul);
}
@@ -0,0 +1,101 @@
/*
* 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/>.
*/
/*
* The Gulf of Memory - map 2964, difficulty 208, scenario 3177, LFGDungeons 3070, AreaTable 16595, entered from
* Harandar (2694). Evidence: REPORT.md 2.2 / 4 / 5 and the gulf 12.1.0.69497 capture.
*
* Scenario 3177 steps: 16081 "Gather Junk" -> 16083 "Free Webbed Haranir" -> 16084 "Remembered horrors defeated" ->
* 16082 "Mul'tha'ul defeated" (criteria 60399 / GameEvent 85913). The boss (250939, DungeonEncounter 3359) is NOT a
* static spawn: his create block arrives 373 ms after step 16082 becomes current (SCENARIO_STATE 864714 -> create
* 865087) at the position below, so he is summoned when the scenario reaches that step.
*
* Steps 0-2 (candle spell 1266697 x4, GameEvents 99942 x8 / 99943 x4, weighted kills 250912-250919 + 92589 x10) are
* work item 8 and not scripted here; the base class completes the delve on the boss kill regardless.
*/
#include "delves_common.h"
#include "DB2Structure.h"
#include "Map.h"
#include "ScriptMgr.h"
namespace
{
static char const* const GulfOfMemoryScriptName = "instance_gulf_of_memory_delve";
enum GulfOfMemoryData
{
BOSS_MULTHAUL = 0,
MAX_ENCOUNTER
};
enum GulfOfMemoryIds : uint32
{
NPC_MULTHAUL = 250939, // "Mul'tha'ul" <Lord of the Deeps>, Elite, hp x10.0 (census)
DUNGEON_ENCOUNTER_MULTHAUL = 3359, // DungeonEncounter.db2, MapID 2964 (SMSG_ENCOUNTER_START 969466, SMSG_BOSS_KILL 1062320)
SCENARIO_STEP_MULTHAUL = 16082, // ScenarioStep OrderIndex 3 of 3177
};
// gulf 865087: create block of 250939, creature position quad (self-validated decode, matches 2026_08_08_41_world.sql)
Position const MulthaulSpawnPos = { -198.668f, 645.146f, 176.693f, 6.2232f };
DungeonEncounterData const encounters[] =
{
{ BOSS_MULTHAUL, {{ DUNGEON_ENCOUNTER_MULTHAUL }} }
};
class instance_gulf_of_memory_delve : public InstanceMapScript
{
public:
instance_gulf_of_memory_delve() : InstanceMapScript(GulfOfMemoryScriptName, 2964) { }
struct instance_gulf_of_memory_delve_InstanceScript : public Delves::DelveInstanceScript
{
instance_gulf_of_memory_delve_InstanceScript(InstanceMap* map)
: DelveInstanceScript(map, 1 /* tier resolved at OnPlayerEnter from m_delveSelectedTier */)
{
SetBossNumber(MAX_ENCOUNTER);
LoadDungeonEncounterData(encounters);
}
void OnScenarioStepChanged(ScenarioStepEntry const* step) override
{
if (!step || step->ID != SCENARIO_STEP_MULTHAUL || _bossSummoned)
return;
_bossSummoned = true;
instance->SummonCreature(NPC_MULTHAUL, MulthaulSpawnPos);
}
private:
bool _bossSummoned = false;
};
InstanceScript* GetInstanceScript(InstanceMap* map) const override
{
return new instance_gulf_of_memory_delve_InstanceScript(map);
}
};
} // anonymous namespace
void AddSC_instance_gulf_of_memory_delve()
{
new instance_gulf_of_memory_delve();
}
@@ -0,0 +1,67 @@
/*
* 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 "delves_common.h"
#include "ScriptMgr.h"
namespace
{
static char const* const ShadowEnclaveScriptName = "instance_shadow_enclave_delve";
enum ShadowEnclaveData
{
BOSS_LORD_ANTENORIAN = 0,
MAX_ENCOUNTER
};
// DungeonEncounter.db2 3368 "Antenorian", MapID 2952 - SMSG_ENCOUNTER_START 2493067 / SMSG_BOSS_KILL 3368 2611872
// (eversong 12.1.0.69497; deatholme 698731 / 742616). Binding it here is what makes SetBossState(DONE) send
// SMSG_BOSS_KILL and the encounter frames.
DungeonEncounterData const encounters[] =
{
{ BOSS_LORD_ANTENORIAN, {{ 3368 }} }
};
class instance_shadow_enclave_delve : public InstanceMapScript
{
public:
instance_shadow_enclave_delve() : InstanceMapScript(ShadowEnclaveScriptName, 2952) { }
struct instance_shadow_enclave_delve_InstanceScript : public Delves::DelveInstanceScript
{
instance_shadow_enclave_delve_InstanceScript(InstanceMap* map)
: DelveInstanceScript(map, 1 /* tier resolved at OnPlayerEnter from m_delveSelectedTier */)
{
SetBossNumber(MAX_ENCOUNTER);
LoadDungeonEncounterData(encounters);
}
};
InstanceScript* GetInstanceScript(InstanceMap* map) const override
{
return new instance_shadow_enclave_delve_InstanceScript(map);
}
};
} // anonymous namespace
void AddSC_instance_shadow_enclave_delve()
{
new instance_shadow_enclave_delve();
}
@@ -0,0 +1,194 @@
/*
* 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/>.
*/
/*
* The Shadow Enclave (Midnight S1 delve) - scenario objective creatures.
* Map 2952, Difficulty 208 (Delves), Scenario 3154, LFGDungeons 3069, AreaTable 16594.
*
* WHY THESE NEED SCRIPTS AT ALL
* -----------------------------
* Every step of scenario 3154 is driven by Criteria Type 92
* (CriteriaType::AnyoneTriggerGameEventScenario, DBCEnums.h:639 - "Anyone will Trigger game event
* {GameEvents} (Scenario Only)"), whose Asset is a GameEvents id, NOT a creature entry. GameEvents
* is not shipped in the client DB2 set and no event_scripts / smart_scripts row in integ_world
* references any of these ids, so nothing in data connects "this creature died" to "this step
* advances". The server has to raise the event explicitly - that is what these scripts do, and it
* is the whole of what they do.
*
* SCENARIO 3154 STEP TABLE
* ------------------------
* Read out of the 12.0.7 client DB2s (ScenarioStep.db2 -> CriteriaTree.db2 -> Criteria.db2) and
* corroborated on the wire by C:\sniff\alliance_deatholme_delve\dumps\
* dump_12.0.1.66562_2026-03-26_08-04-06.pkt ("Deatholme delve" = The Shadow Enclave), which
* contains a complete run and whose SMSG_SCENARIO_STATE step order is
* 16032 -> 16028 -> 16029 -> 16030 -> 16031 -> SMSG_SCENARIO_COMPLETED(3154):
*
* Order 0 Step 16032 tree 214779/214780 criteria 107837 event 99761 amount 1
* "Pursue Antenorian" -- NOT scripted here, no
* ScriptName exists for it
* Order 1 Step 16028 tree 214781/214782 criteria 107838 event 99763 amount 2
* Void Focus destroyed -> npc_void_focus_se (creature 250266)
* Order 2 Step 16029 tree 214768
* 221264 "Rituals stopped" amount 3 -> 214769 criteria 107834 event 99755
* -> npc_darkcaller (250242 / 251898 / 251899)
* 214770 "Cultists purged" amount 200, weighted trash events 87796/87797/87798/87795
* -- NOT scripted here, see OPEN ITEMS below
* Order 3 Step 16030 tree 214775/214776 criteria 107835 event 99756 amount 3
* "Antenorian's Devoted slain" (creature 250275)
* -- NOT scripted here, no ScriptName exists for it
* Order 4 Step 16031 tree 214777/214778 criteria 60399 event 85913 amount 1
* Antenorian slain -> npc_lord_antenorian (creature 246717)
*
* Event 85913 is a shared "delve boss slain" event reused by 32 criteria trees across many delves,
* so it is safe to raise from the boss's death: it only satisfies whichever delve scenario is
* actually running on the map.
*
* The final boss identity is not guesswork either. In the same capture:
* SMSG_ENCOUNTER_START EncounterID 3368, DifficultyID 208
* SMSG_INSTANCE_ENCOUNTER_ENGAGE_UNIT map 2952, entry 246717
* SMSG_BOSS_KILL DungeonEncounterID 3368
* SMSG_QUEST_UPDATE_ADD_CREDIT map 2952, entry 246717 (quest 86636 "Void Walk With Me")
* SMSG_SCENARIO_COMPLETED ScenarioID 3154
* SMSG_QUERY_CREATURE_RESPONSE entry 246717 = "Lord Antenorian"
* and DungeonEncounter.db2 id 3368 is Name "Antenorian", MapID 2952.
*
* OPEN ITEMS (deliberately not implemented - no evidence, so no invented behaviour)
* --------------------------------------------------------------------------------
* * No combat kit. Not one spell id for Lord Antenorian, the Darkcallers or the Void Focus is
* recoverable from the client DB2s or from any capture on this machine, so these creatures get
* no ability rotation. They keep TC's default melee behaviour; nothing is faked.
* * "Cultists purged" (tree 214770) needs per-trash-mob credit against four weighted events with
* no creature->event mapping in any source. It stays unimplemented.
* * Step 0 "Pursue Antenorian" (event 99761) has no creature to hang off - it is almost certainly
* an area trigger, which no source on this machine identifies.
* * MOST IMPORTANTLY: map 2952 has ZERO creature and ZERO gameobject spawns in integ_world, and
* no spawn file for it exists anywhere in this repo or in any other world DB on this box. These
* scripts are correct but unreachable until the roster is imported. The 66562 capture above
* carries the full 167-entry creature roster with SMSG_UPDATE_OBJECT positions on map 2952 and
* is the obvious source for that import.
*/
#include "Creature.h"
#include "DBCEnums.h"
#include "InstanceScript.h"
#include "Log.h"
#include "ScriptedCreature.h"
#include "ScriptMgr.h"
namespace
{
// Criteria.db2 Asset values (GameEvents ids) for scenario 3154's Type-92 criteria. See the step
// table in the file header for the full derivation.
enum ShadowEnclaveScenarioEvents : uint32
{
EVENT_VOID_FOCUS_DESTROYED = 99763, // step order 1, criteria 107838, amount 2
EVENT_RITUAL_STOPPED = 99755, // step order 2, criteria 107834, amount 3
// 85913 (step order 4, criteria 60399 "Antenorian slain") is raised by DelveInstanceScript::OnUnitDeath for the
// template's final boss - see Delves::GAME_EVENT_DELVE_BOSS_SLAIN in delves_common.h.
};
// Matches BOSS_LORD_ANTENORIAN in instance_shadow_enclave_delve.cpp (SetBossNumber(1)).
constexpr uint32 BOSS_LORD_ANTENORIAN = 0;
// creature_text GroupID for Lord Antenorian's death yell, inserted by
// sql/updates/world/master/2026_04_29_03_world.sql (CreatureID 246717, GroupID 0, Type 14 = Yell).
constexpr uint8 SAY_ANTENORIAN_DEATH = 0;
// Raises a scenario game event for every player in the delve instance. Delve maps are
// InstanceType 5 (Scenario), which is the gate CriteriaHandler applies to Type-92 criteria.
void RaiseScenarioEvent(Creature* source, uint32 gameEventId)
{
InstanceScript* instance = source->GetInstanceScript();
if (!instance)
{
TC_LOG_DEBUG("scripts.delves",
"The Shadow Enclave: creature {} (entry {}) died outside an instance script; scenario event {} not raised.",
source->GetGUID().ToString(), source->GetEntry(), gameEventId);
return;
}
instance->DoUpdateCriteria(CriteriaType::AnyoneTriggerGameEventScenario, gameEventId, 0, source);
TC_LOG_DEBUG("scripts.delves",
"The Shadow Enclave: creature entry {} died, raised scenario game event {}.",
source->GetEntry(), gameEventId);
}
// ---------------------------------------------------------------------------------------------
// Void Focus - creature 250266. Scenario 3154 step order 1 needs two of them destroyed.
// ---------------------------------------------------------------------------------------------
struct npc_void_focus_se : public ScriptedAI
{
npc_void_focus_se(Creature* creature) : ScriptedAI(creature) { }
void JustDied(Unit* /*killer*/) override
{
RaiseScenarioEvent(me, EVENT_VOID_FOCUS_DESTROYED);
}
};
// ---------------------------------------------------------------------------------------------
// Darkcallers - creatures 250242 (Lysaille), 251898 (Thelamorn), 251899 (Cimberon).
// Scenario 3154 step order 2, sub-tree 221264 "Rituals stopped", needs all three.
// ---------------------------------------------------------------------------------------------
struct npc_darkcaller : public ScriptedAI
{
npc_darkcaller(Creature* creature) : ScriptedAI(creature) { }
void JustDied(Unit* /*killer*/) override
{
RaiseScenarioEvent(me, EVENT_RITUAL_STOPPED);
}
};
// ---------------------------------------------------------------------------------------------
// Lord Antenorian - creature 246717, the delve's final boss (DungeonEncounter 3368).
// ---------------------------------------------------------------------------------------------
struct npc_lord_antenorian : public ScriptedAI
{
npc_lord_antenorian(Creature* creature) : ScriptedAI(creature) { }
void JustEngagedWith(Unit* who) override
{
ScriptedAI::JustEngagedWith(who);
// SMSG_ENCOUNTER_START 3368 (eversong 2493067, deatholme 698731); ws 24836 -> 1
if (InstanceScript* instance = me->GetInstanceScript())
instance->SetBossState(BOSS_LORD_ANTENORIAN, IN_PROGRESS);
}
void JustDied(Unit* /*killer*/) override
{
Talk(SAY_ANTENORIAN_DEATH);
// GameEvent 85913 (EVENT_ANTENORIAN_SLAIN) and the completion itself are raised by
// Delves::DelveInstanceScript::OnUnitDeath via delve_template.finalBossEntry = 246717; this only closes the
// encounter frame (SMSG_ENCOUNTER_END + SMSG_BOSS_KILL 3368, eversong 2611872).
if (InstanceScript* instance = me->GetInstanceScript())
instance->SetBossState(BOSS_LORD_ANTENORIAN, DONE);
}
};
} // anonymous namespace
void AddSC_shadow_enclave_encounters()
{
RegisterCreatureAI(npc_void_focus_se);
RegisterCreatureAI(npc_darkcaller);
RegisterCreatureAI(npc_lord_antenorian);
}
@@ -81,7 +81,7 @@ namespace
// record) are not symbolicated in the 67186 IDA db, so we send the
// creature entry as the most defensible interpretation.
WorldPackets::Delves::ShowDelvesCompanionConfigurationUI packet;
packet.CreatureOrSpellID = me->GetEntry();
packet.Unknown = me->GetEntry();
player->SendDirectMessage(packet.Write());
return true;
}
+68 -4
View File
@@ -107,7 +107,9 @@ enum HunterSpells
SPELL_HUNTER_KILL_COMMAND = 34026,
SPELL_HUNTER_KILL_COMMAND_TRIGGER = 83381,
SPELL_HUNTER_KILL_COMMAND_CHARGE = 118171,
SPELL_HUNTER_BESTIAL_WRATH = 38371,
SPELL_HUNTER_BESTIAL_WRATH = 19574,
SPELL_HUNTER_BESTIAL_WRATH_PET = 38371,
SPELL_HUNTER_BESTIAL_WRATH_DAMAGE = 344572,
SPELL_HUNTER_ANIMAL_COMPANION = 267116,
SPELL_HUNTER_SUMMON_ANIMAL_COMPANION = 273277,
SPELL_HUNTER_WILD_THRASH = 1264359,
@@ -1638,8 +1640,8 @@ class spell_hun_call_pet : public SpellScript
}
};
// 38371 - Bestial Wrath
class spell_hun_bestial_wrath : public SpellScript
// 38371 - Bestial Wrath (legacy pet enrage buff)
class spell_hun_bestial_wrath_pet : public SpellScript
{
void HandleAfterCast()
{
@@ -1653,13 +1655,74 @@ class spell_hun_bestial_wrath : public SpellScript
{
// Ensure the pet gets the Bestial Wrath effects
// The spell data has TARGET_UNIT_NEARBY_ENTRY but may not target the pet correctly
caster->CastSpell(pet, SPELL_HUNTER_BESTIAL_WRATH, true);
caster->CastSpell(pet, SPELL_HUNTER_BESTIAL_WRATH_PET, true);
}
}
}
void Register() override
{
AfterCast += SpellCastFn(spell_hun_bestial_wrath_pet::HandleAfterCast);
}
};
// 19574 - Bestial Wrath
class spell_hun_bestial_wrath : public SpellScript
{
bool Validate(SpellInfo const* /*spellInfo*/) override
{
return ValidateSpellInfo({ SPELL_HUNTER_BESTIAL_WRATH_DAMAGE });
}
SpellCastResult CheckCast()
{
Unit* caster = GetCaster();
if (!caster || caster->GetTypeId() != TYPEID_PLAYER)
return SPELL_CAST_OK;
Pet* pet = caster->ToPlayer()->GetPet();
if (!pet)
return SPELL_FAILED_NO_PET;
if (!pet->IsAlive())
{
SetCustomCastResultMessage(SPELL_CUSTOM_ERROR_PET_IS_DEAD);
return SPELL_FAILED_CUSTOM_ERROR;
}
return SPELL_CAST_OK;
}
void HandleAfterCast() const
{
Unit* caster = GetCaster();
if (!caster || caster->GetTypeId() != TYPEID_PLAYER)
return;
Player* player = caster->ToPlayer();
Pet* pet = player->GetPet();
if (!pet || !pet->IsAlive())
return;
// Removes all crowd control effects from your pet, working like a PvP trinket
pet->RemoveAurasWithMechanic(IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK, AURA_REMOVE_BY_DEFAULT, SPELL_HUNTER_BESTIAL_WRATH);
// Grant the pet its own Bestial Wrath aura so both of you deal increased damage.
// The pet re-enters this script on its own cast, but the player-only guard above prevents recursion.
pet->CastSpell(pet, SPELL_HUNTER_BESTIAL_WRATH, TRIGGERED_FULL_MASK);
// The pet instantly deals Physical damage (344572) to its target
Unit* target = pet->GetVictim();
if ((!target || !pet->IsValidAttackTarget(target)) && !player->GetTarget().IsEmpty())
target = ObjectAccessor::GetUnit(*player, player->GetTarget());
if (target && pet->IsValidAttackTarget(target) && pet->IsWithinLOSInMap(target))
pet->CastSpell(target, SPELL_HUNTER_BESTIAL_WRATH_DAMAGE, true);
}
void Register() override
{
OnCheckCast += SpellCheckCastFn(spell_hun_bestial_wrath::CheckCast);
AfterCast += SpellCastFn(spell_hun_bestial_wrath::HandleAfterCast);
}
};
@@ -1890,6 +1953,7 @@ void AddSC_hunter_spell_scripts()
RegisterSpellScript(spell_hun_kill_command_proc);
RegisterSpellScript(spell_hun_call_pet);
RegisterSpellScript(spell_hun_bestial_wrath);
RegisterSpellScript(spell_hun_bestial_wrath_pet);
RegisterSpellScript(spell_hun_wild_thrash);
RegisterSpellScript(spell_hun_trick_shots);
RegisterSpellScript(spell_hun_aspect_of_the_hydra);
+10 -10
View File
@@ -110,11 +110,11 @@ struct npc_garrick_expedition_209057 : public ScriptedAI
{
if (Player* player = unit->ToPlayer())
{
Group* group = player->GetGroup();
me->AI()->Talk(0);
me->SetOwnerGUID(player->GetGUID()); //set player owner
me->GetMotionMaster()->MoveFollow(player, 2.0f, PET_FOLLOW_ANGLE, {}, MOTION_SLOT_ACTIVE);
group->AddFollowerMemberTank(me);
if (Group* group = player->GetGroup())
group->AddFollowerMemberTank(me);
if (int32 playerFaction = player->GetFaction())
me->SetFaction(playerFaction);
@@ -249,8 +249,8 @@ struct npc_Shuja_Grimaxe_214390 : public ScriptedAI
{
if (Player* player = unit->ToPlayer())
{
Group* group = player->GetGroup();
group->AddFollowerMemberDPS(me);
if (Group* group = player->GetGroup())
group->AddFollowerMemberDPS(me);
me->AI()->Talk(0);
me->SetOwnerGUID(player->GetGUID()); //set player owner
me->GetMotionMaster()->MoveFollow(player, 2.0f, 60.f, {}, MOTION_SLOT_ACTIVE);
@@ -372,8 +372,8 @@ struct npc_Austin_Huxworth_209065 : public ScriptedAI
{
if (Player* player = unit->ToPlayer())
{
Group* group = player->GetGroup();
group->AddFollowerMemberDPS(me);
if (Group* group = player->GetGroup())
group->AddFollowerMemberDPS(me);
me->AI()->Talk(0);
me->SetOwnerGUID(player->GetGUID()); //set player owner
me->GetMotionMaster()->MoveFollow(player, 4.0f, 75.f, {}, MOTION_SLOT_ACTIVE);
@@ -471,8 +471,8 @@ struct npc_Meredy_Huntswell_209059 : public ScriptedAI
{
if (Player* player = unit->ToPlayer())
{
Group* group = player->GetGroup();
group->AddFollowerMemberDPS(me);
if (Group* group = player->GetGroup())
group->AddFollowerMemberDPS(me);
me->SetOwnerGUID(player->GetGUID()); //set player owner
me->GetMotionMaster()->MoveFollow(player, 4.0f, 30.f, {}, MOTION_SLOT_ACTIVE);
@@ -551,8 +551,8 @@ struct npc_Crenna_Earth_Daughter_209072 : public ScriptedAI
{
if (Player* player = unit->ToPlayer())
{
Group* group = player->GetGroup();
group->AddFollowerMemberHealer(me);
if (Group* group = player->GetGroup())
group->AddFollowerMemberHealer(me);
me->AI()->Talk(0);
me->SetOwnerGUID(player->GetGUID()); //set player owner
me->GetMotionMaster()->MoveFollow(player, 4.0f, 80.f, {}, MOTION_SLOT_ACTIVE);