Housing: Wire up initiative/endeavor system and fix interior editor unlock
- Register PlayerInitiativeComponent_C entity fragment (FragmentID 37) on player load so C_NeighborhoodInitiative Lua API returns initiative state - Populate InitiativeInfo fields (duration, progress, milestone, cycle, contribution) and Houses set from InitiativeManager data - Send SMSG_INITIATIVE_SERVICE_STATUS (0x80=enabled) proactively on both exterior and interior map entry so IsInitiativeEnabled() returns true immediately - Set IsInitiative flag on cornerstone UI response when neighborhood has active initiative - Add InsertSetUpdateFieldValue friend declaration to SetUpdateFieldSetter (was missing, preventing set-type update field insertion from compiling) - Send SMSG_HOUSING_GET_CURRENT_HOUSE_INFO_RESPONSE in interior map so editor UI has proper house context - Add SendPostTutorialAuras to HouseInteriorMap to unlock all editor modes (expert, cleanup, layout, customize) — auras are lost on map transfer and must be re-sent
This commit is contained in:
@@ -288,6 +288,9 @@ namespace UF
|
||||
template<typename T>
|
||||
struct SetUpdateFieldSetter
|
||||
{
|
||||
template<typename F>
|
||||
friend bool InsertSetUpdateFieldValue(SetUpdateFieldSetter<F>& setter, std::type_identity_t<F> const& key);
|
||||
|
||||
template<typename F>
|
||||
friend bool RemoveSetUpdateFieldValue(SetUpdateFieldSetter<F>& setter, std::type_identity_t<F> const& key);
|
||||
|
||||
|
||||
@@ -3790,6 +3790,8 @@ void Player::ClearValuesChangesMask()
|
||||
{
|
||||
m_values.ClearChangesMask(&Player::m_playerData);
|
||||
m_values.ClearChangesMask(&Player::m_activePlayerData);
|
||||
m_values.ClearChangesMask(&Player::m_playerHouseInfoComponentData);
|
||||
m_values.ClearChangesMask(&Player::m_playerInitiativeComponentData);
|
||||
Unit::ClearValuesChangesMask();
|
||||
}
|
||||
|
||||
@@ -18849,9 +18851,14 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
|
||||
holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_HOUSING_CATALOG)))
|
||||
_housings.push_back(std::move(housing));
|
||||
|
||||
// TODO: When the housing tutorial questline is implemented, this brute-force approach
|
||||
// (setting all tutorial bits + injecting closedInfoFramesAccountWide / housingTutorialsEnabled
|
||||
// CVars) must be replaced with proper quest-driven tutorial progression. The client Lua UI
|
||||
// sets FrameTutorialAccount bits (e.g. HousingModesUnlocked=38) individually as the player
|
||||
// completes each tutorial step. At that point, remove the blanket CVar injection below and
|
||||
// let the questline handlers set the appropriate closedInfoFramesAccountWide bits on completion.
|
||||
//
|
||||
// Mark all tutorials as seen. Retail sniff shows all 256 tutorial bits set to 1 (0xFF bytes).
|
||||
// Without this, the client blocks housing cleanup/expert modes with "Mode not available
|
||||
// while in the tutorial" (FrameTutorialAccount.HousingModesUnlocked = bit 38).
|
||||
if (GetSession())
|
||||
{
|
||||
bool needsUpdate = false;
|
||||
@@ -18865,6 +18872,62 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
|
||||
}
|
||||
if (needsUpdate)
|
||||
GetSession()->SendTutorialsData();
|
||||
|
||||
// The 256-bit server tutorial flags (above) are separate from the client's
|
||||
// FrameTutorialAccount UI flags. The client stores those in the CVar bitfield
|
||||
// "closedInfoFramesAccountWide" within the GLOBAL_CONFIG_CACHE account data.
|
||||
// Without setting bit 38 (HousingModesUnlocked), the housing editor UI keeps
|
||||
// expert/cleanup/layout modes locked with "Tutorial Mode" error.
|
||||
// Also disable housing tutorials entirely via housingTutorialsEnabled=0.
|
||||
AccountData const* configCache = GetSession()->GetAccountData(GLOBAL_CONFIG_CACHE);
|
||||
std::string configData = configCache ? configCache->Data : "";
|
||||
bool configModified = false;
|
||||
|
||||
// Helper lambda: set or replace a CVar value in the config string
|
||||
auto ensureCVar = [&](std::string_view cvarName, std::string_view value)
|
||||
{
|
||||
std::string setPrefix = std::string("SET ") + std::string(cvarName) + " \"";
|
||||
size_t pos = configData.find(setPrefix);
|
||||
if (pos != std::string::npos)
|
||||
{
|
||||
// Replace existing value
|
||||
size_t valStart = pos + setPrefix.size();
|
||||
size_t valEnd = configData.find('"', valStart);
|
||||
if (valEnd != std::string::npos)
|
||||
{
|
||||
std::string oldVal = configData.substr(valStart, valEnd - valStart);
|
||||
if (oldVal != value)
|
||||
{
|
||||
configData.replace(valStart, valEnd - valStart, value);
|
||||
configModified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Append new CVar
|
||||
if (!configData.empty() && configData.back() != '\n')
|
||||
configData += '\n';
|
||||
configData += "SET ";
|
||||
configData += cvarName;
|
||||
configData += " \"";
|
||||
configData += value;
|
||||
configData += "\"\n";
|
||||
configModified = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Mark ALL FrameTutorialAccount bits as seen (48 bits = 2 uint32 words, all 1s)
|
||||
ensureCVar("closedInfoFramesAccountWide", "4294967295 4294967295");
|
||||
// Disable housing tutorial gates entirely
|
||||
ensureCVar("housingTutorialsEnabled", "0");
|
||||
|
||||
if (configModified)
|
||||
{
|
||||
GetSession()->SetAccountData(GLOBAL_CONFIG_CACHE, GameTime::GetGameTime(), configData);
|
||||
TC_LOG_DEBUG("housing", "Player::LoadFromDB: Injected housing tutorial CVars into GLOBAL_CONFIG_CACHE for account {}",
|
||||
GetSession()->GetAccountId());
|
||||
}
|
||||
}
|
||||
|
||||
// Always register PlayerHouseInfoComponent_C fragment on the Player entity.
|
||||
@@ -18915,6 +18978,96 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
|
||||
mirrorHouse.InitiativeFavor = sInitiativeManager.GetPlayerContribution(nhGuid, activeInit->InitiativeID, GetGUID().GetCounter());
|
||||
}
|
||||
}
|
||||
// Register PlayerInitiativeComponent_C fragment (FragmentID 37) on the Player entity.
|
||||
// The client's C_NeighborhoodInitiative Lua API reads initiative state from this fragment.
|
||||
// Without it, GetNeighborhoodInitiativeInfo() returns nil and the initiative/endeavor UI
|
||||
// never appears. Sniff-verified: all neighborhood players have this fragment.
|
||||
if (!m_playerInitiativeComponentData.has_value())
|
||||
{
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Player::m_playerInitiativeComponentData, 0)
|
||||
.ModifyValue(&UF::PlayerInitiativeComponentData::NeighborhoodGUID), ObjectGuid::Empty);
|
||||
m_entityFragments.Add(WowCS::EntityFragment::PlayerInitiativeComponent_C, false,
|
||||
WowCS::GetRawFragmentData(m_playerInitiativeComponentData));
|
||||
}
|
||||
|
||||
// Populate initiative data for the player's neighborhood
|
||||
if (!_housings.empty() && _housings[0] && !_housings[0]->GetNeighborhoodGuid().IsEmpty())
|
||||
{
|
||||
ObjectGuid nhGuid = _housings[0]->GetNeighborhoodGuid();
|
||||
uint64 nhLowGuid = nhGuid.GetCounter();
|
||||
ActiveInitiative* activeInit = sInitiativeManager.GetActiveInitiative(nhLowGuid);
|
||||
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Player::m_playerInitiativeComponentData, 0)
|
||||
.ModifyValue(&UF::PlayerInitiativeComponentData::NeighborhoodGUID), nhGuid);
|
||||
|
||||
if (activeInit)
|
||||
{
|
||||
NeighborhoodInitiativeEntry const* initEntry = sNeighborhoodInitiativeStore.LookupEntry(activeInit->InitiativeID);
|
||||
uint32 cycleID = sInitiativeManager.GetActiveCycleForInitiative(activeInit->InitiativeID);
|
||||
|
||||
// Calculate remaining duration from start time + DB2 duration
|
||||
int64 remainingDuration = 0;
|
||||
if (initEntry && initEntry->Duration > 0)
|
||||
{
|
||||
int64 elapsed = static_cast<int64>(GameTime::GetGameTime()) - static_cast<int64>(activeInit->StartTime);
|
||||
remainingDuration = std::max<int64>(0, static_cast<int64>(initEntry->Duration) - elapsed);
|
||||
}
|
||||
|
||||
// Calculate progress in the 0-1000 scale (sniff: ProgressRequired=1000)
|
||||
float progressRequired = 1000.0f;
|
||||
float currentProgress = activeInit->Progress * progressRequired;
|
||||
|
||||
// Find current milestone
|
||||
int32 currentMilestoneID = -1;
|
||||
auto milestones = sInitiativeManager.GetMilestonesForCycle(cycleID);
|
||||
for (auto const& m : milestones)
|
||||
{
|
||||
if (activeInit->Progress < m.ProgressRequired)
|
||||
{
|
||||
currentMilestoneID = static_cast<int32>(m.MilestoneID);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
float playerContribution = static_cast<float>(
|
||||
sInitiativeManager.GetPlayerContribution(nhLowGuid, activeInit->InitiativeID, GetGUID().GetCounter()));
|
||||
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Player::m_playerInitiativeComponentData, 0)
|
||||
.ModifyValue(&UF::PlayerInitiativeComponentData::InitiativeInfo)
|
||||
.ModifyValue(&UF::PlayerInitiativeInfo::RemainingDuration), remainingDuration);
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Player::m_playerInitiativeComponentData, 0)
|
||||
.ModifyValue(&UF::PlayerInitiativeComponentData::InitiativeInfo)
|
||||
.ModifyValue(&UF::PlayerInitiativeInfo::CurrentInitiativeID), static_cast<int32>(activeInit->InitiativeID));
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Player::m_playerInitiativeComponentData, 0)
|
||||
.ModifyValue(&UF::PlayerInitiativeComponentData::InitiativeInfo)
|
||||
.ModifyValue(&UF::PlayerInitiativeInfo::CurrentMilestoneID), currentMilestoneID);
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Player::m_playerInitiativeComponentData, 0)
|
||||
.ModifyValue(&UF::PlayerInitiativeComponentData::InitiativeInfo)
|
||||
.ModifyValue(&UF::PlayerInitiativeInfo::CurrentCycleID), static_cast<int32>(cycleID));
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Player::m_playerInitiativeComponentData, 0)
|
||||
.ModifyValue(&UF::PlayerInitiativeComponentData::InitiativeInfo)
|
||||
.ModifyValue(&UF::PlayerInitiativeInfo::ProgressRequired), progressRequired);
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Player::m_playerInitiativeComponentData, 0)
|
||||
.ModifyValue(&UF::PlayerInitiativeComponentData::InitiativeInfo)
|
||||
.ModifyValue(&UF::PlayerInitiativeInfo::CurrentProgress), currentProgress);
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Player::m_playerInitiativeComponentData, 0)
|
||||
.ModifyValue(&UF::PlayerInitiativeComponentData::InitiativeInfo)
|
||||
.ModifyValue(&UF::PlayerInitiativeInfo::PlayerTotalContribution), playerContribution);
|
||||
|
||||
// Add house GUIDs to the Houses set
|
||||
for (auto const& h : _housings)
|
||||
{
|
||||
if (h && !h->GetHouseGuid().IsEmpty())
|
||||
InsertSetUpdateFieldValue(m_values.ModifyValue(&Player::m_playerInitiativeComponentData, 0)
|
||||
.ModifyValue(&UF::PlayerInitiativeComponentData::Houses), h->GetHouseGuid());
|
||||
}
|
||||
|
||||
TC_LOG_DEBUG("housing", "Player::LoadFromDB: Populated PlayerInitiativeComponentData: "
|
||||
"InitiativeID={} CycleID={} Progress={:.1f}/{:.0f} Milestone={} Duration={}",
|
||||
activeInit->InitiativeID, cycleID, currentProgress, progressRequired,
|
||||
currentMilestoneID, remainingDuration);
|
||||
}
|
||||
}
|
||||
|
||||
//WowCommunity
|
||||
|
||||
|
||||
@@ -3047,6 +3047,8 @@ class TC_GAME_API Player final : public Unit, public GridObject<Player>
|
||||
|
||||
// Housing entity fragment (optional - only set when player has housing data)
|
||||
UF::OptionalUpdateField<UF::PlayerHouseInfoComponentData, int32(WowCS::EntityFragment::PlayerHouseInfoComponent_C), 0> m_playerHouseInfoComponentData;
|
||||
// Initiative entity fragment (optional - initiative/endeavor state for UI)
|
||||
UF::OptionalUpdateField<UF::PlayerInitiativeComponentData, int32(WowCS::EntityFragment::PlayerInitiativeComponent_C), 0> m_playerInitiativeComponentData;
|
||||
|
||||
void SetAreaSpiritHealer(Creature* creature);
|
||||
ObjectGuid const& GetSpiritHealerGUID() const { return _areaSpiritHealerGUID; }
|
||||
|
||||
@@ -1566,6 +1566,10 @@ void WorldSession::HandleNeighborhoodOpenCornerstoneUI(WorldPackets::Neighborhoo
|
||||
response.CanPurchase = !isOwned;
|
||||
response.NeighborhoodName = neighborhood->GetName();
|
||||
|
||||
// Set IsInitiative when the neighborhood has an active initiative/endeavor
|
||||
uint64 nhLowGuid = neighborhood->GetGuid().GetCounter();
|
||||
response.IsInitiative = (sInitiativeManager.GetActiveInitiative(nhLowGuid) != nullptr);
|
||||
|
||||
if (isOwned)
|
||||
{
|
||||
// Owned plot: show owner info, no purchase available
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
#include "ObjectMgr.h"
|
||||
#include "PhasingHandler.h"
|
||||
#include "Player.h"
|
||||
#include "Spell.h"
|
||||
#include "SpellAuraDefines.h"
|
||||
#include "SpellPackets.h"
|
||||
#include "World.h"
|
||||
#include "WorldSession.h"
|
||||
|
||||
@@ -753,6 +756,27 @@ bool HouseInteriorMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/
|
||||
}
|
||||
}
|
||||
|
||||
// Send SMSG_HOUSING_GET_CURRENT_HOUSE_INFO_RESPONSE so the client knows the
|
||||
// active house context. Without this, the client has no house context and the
|
||||
// editor UI (C_HouseEditor.GetHouseEditorModeAvailability) won't show.
|
||||
// The exterior map (HousingMap::AddPlayerToMap) sends this ? the interior must too.
|
||||
{
|
||||
WorldPackets::Housing::HousingGetCurrentHouseInfoResponse houseInfo;
|
||||
houseInfo.House.HouseGuid = housing->GetHouseGuid();
|
||||
houseInfo.House.OwnerGuid = player->GetGUID();
|
||||
houseInfo.House.NeighborhoodGuid = housing->GetNeighborhoodGuid();
|
||||
houseInfo.House.PlotId = housing->GetPlotIndex();
|
||||
houseInfo.House.AccessFlags = housing->GetSettingsFlags();
|
||||
houseInfo.House.HasMoveOutTime = false;
|
||||
houseInfo.Result = 0;
|
||||
player->SendDirectMessage(houseInfo.Write());
|
||||
|
||||
TC_LOG_DEBUG("housing", "HouseInteriorMap::AddPlayerToMap: Sent CURRENT_HOUSE_INFO "
|
||||
"HouseGuid={} OwnerGuid={} NeighborhoodGuid={} PlotId={}",
|
||||
housing->GetHouseGuid().ToString(), player->GetGUID().ToString(),
|
||||
housing->GetNeighborhoodGuid().ToString(), housing->GetPlotIndex());
|
||||
}
|
||||
|
||||
// Send SMSG_HOUSE_INTERIOR_ENTER_HOUSE
|
||||
WorldPackets::Housing::HouseInteriorEnterHouse enterHouse;
|
||||
enterHouse.HouseGuid = housing->GetHouseGuid();
|
||||
@@ -766,6 +790,20 @@ bool HouseInteriorMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/
|
||||
statusResponse.NeighborhoodGuid = housing->GetNeighborhoodGuid();
|
||||
statusResponse.Status = 1; // Interior
|
||||
player->SendDirectMessage(statusResponse.Write());
|
||||
|
||||
// Send post-tutorial auras so the client unlocks all editor modes.
|
||||
// These auras signal "tutorial complete" and are lost on map transfer.
|
||||
// The exterior (HousingMap) sends them via SendPostTutorialAuras ?
|
||||
// the interior must do the same or the editor remains locked.
|
||||
SendPostTutorialAuras(player);
|
||||
|
||||
// Send SMSG_INITIATIVE_SERVICE_STATUS so IsInitiativeEnabled() returns true
|
||||
// in interior. Sniff-verified: server responds with 0x80 (enabled).
|
||||
{
|
||||
WorldPackets::Housing::InitiativeServiceStatus initStatus;
|
||||
initStatus.ServiceEnabled = true;
|
||||
player->SendDirectMessage(initStatus.Write());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -798,3 +836,163 @@ void HouseInteriorMap::RemovePlayerFromMap(Player* player, bool remove)
|
||||
|
||||
Map::RemovePlayerFromMap(player, remove);
|
||||
}
|
||||
|
||||
void HouseInteriorMap::SendPostTutorialAuras(Player* player)
|
||||
{
|
||||
// Sniff-verified: After quest 94455 "Home at Last" completion, three "post-tutorial" auras
|
||||
// are applied at slots 8, 9, 50. These signal "tutorial complete" to the client and unlock
|
||||
// all editor modes (expert, cleanup, layout, customize). Auras are lost on map transfer,
|
||||
// so they must be re-sent when entering both the exterior AND interior housing maps.
|
||||
// Slot 8: spell 1285428 (NoCaster, ActiveFlags=1)
|
||||
// Slot 9: spell 1285424 (NoCaster, ActiveFlags=1)
|
||||
// Slot 50: spell 1266699 (NoCaster|Scalable, ActiveFlags=1, Points=1)
|
||||
//
|
||||
// TODO: When the housing tutorial questline is implemented, these auras should only be
|
||||
// sent for players who have actually completed the tutorial quest (94455 "Home at Last").
|
||||
|
||||
// Spell 1285428 at slot 8
|
||||
{
|
||||
ObjectGuid castId = ObjectGuid::Create<HighGuid::Cast>(
|
||||
SPELL_CAST_SOURCE_NORMAL, player->GetMapId(), SPELL_HOUSING_TUTORIAL_DONE_1,
|
||||
player->GetMap()->GenerateLowGuid<HighGuid::Cast>());
|
||||
|
||||
WorldPackets::Spells::AuraUpdate auraUpdate;
|
||||
auraUpdate.UpdateAll = false;
|
||||
auraUpdate.UnitGUID = player->GetGUID();
|
||||
|
||||
WorldPackets::Spells::AuraInfo auraInfo;
|
||||
auraInfo.Slot = 8;
|
||||
auraInfo.AuraData.emplace();
|
||||
auraInfo.AuraData->CastID = castId;
|
||||
auraInfo.AuraData->SpellID = SPELL_HOUSING_TUTORIAL_DONE_1;
|
||||
auraInfo.AuraData->Flags = AFLAG_NOCASTER;
|
||||
auraInfo.AuraData->ActiveFlags = 1;
|
||||
auraInfo.AuraData->CastLevel = 36;
|
||||
auraInfo.AuraData->Applications = 0;
|
||||
auraUpdate.Auras.push_back(std::move(auraInfo));
|
||||
player->SendDirectMessage(auraUpdate.Write());
|
||||
|
||||
WorldPackets::Spells::SpellStart spellStart;
|
||||
spellStart.Cast.CasterGUID = player->GetGUID();
|
||||
spellStart.Cast.CasterUnit = player->GetGUID();
|
||||
spellStart.Cast.CastID = castId;
|
||||
spellStart.Cast.SpellID = SPELL_HOUSING_TUTORIAL_DONE_1;
|
||||
spellStart.Cast.CastFlags = CAST_FLAG_PENDING | CAST_FLAG_HAS_TRAJECTORY | CAST_FLAG_UNKNOWN_3 | CAST_FLAG_UNKNOWN_4; // 15
|
||||
spellStart.Cast.CastTime = 0;
|
||||
player->SendDirectMessage(spellStart.Write());
|
||||
|
||||
WorldPackets::Spells::SpellGo spellGo;
|
||||
spellGo.Cast.CasterGUID = player->GetGUID();
|
||||
spellGo.Cast.CasterUnit = player->GetGUID();
|
||||
spellGo.Cast.CastID = castId;
|
||||
spellGo.Cast.SpellID = SPELL_HOUSING_TUTORIAL_DONE_1;
|
||||
spellGo.Cast.CastFlags = CAST_FLAG_PENDING | CAST_FLAG_UNKNOWN_3 | CAST_FLAG_UNKNOWN_4 | CAST_FLAG_UNKNOWN_9 | CAST_FLAG_UNKNOWN_10; // 781
|
||||
spellGo.Cast.CastFlagsEx = 16;
|
||||
spellGo.Cast.CastFlagsEx2 = 4;
|
||||
spellGo.Cast.CastTime = getMSTime();
|
||||
spellGo.Cast.Target.Flags = TARGET_FLAG_UNIT;
|
||||
spellGo.Cast.HitTargets.push_back(player->GetGUID());
|
||||
spellGo.Cast.HitStatus.emplace_back(uint8(0));
|
||||
spellGo.LogData.Initialize(player);
|
||||
player->SendDirectMessage(spellGo.Write());
|
||||
}
|
||||
|
||||
// Spell 1285424 at slot 9
|
||||
{
|
||||
ObjectGuid castId = ObjectGuid::Create<HighGuid::Cast>(
|
||||
SPELL_CAST_SOURCE_NORMAL, player->GetMapId(), SPELL_HOUSING_TUTORIAL_DONE_2,
|
||||
player->GetMap()->GenerateLowGuid<HighGuid::Cast>());
|
||||
|
||||
WorldPackets::Spells::AuraUpdate auraUpdate;
|
||||
auraUpdate.UpdateAll = false;
|
||||
auraUpdate.UnitGUID = player->GetGUID();
|
||||
|
||||
WorldPackets::Spells::AuraInfo auraInfo;
|
||||
auraInfo.Slot = 9;
|
||||
auraInfo.AuraData.emplace();
|
||||
auraInfo.AuraData->CastID = castId;
|
||||
auraInfo.AuraData->SpellID = SPELL_HOUSING_TUTORIAL_DONE_2;
|
||||
auraInfo.AuraData->Flags = AFLAG_NOCASTER;
|
||||
auraInfo.AuraData->ActiveFlags = 1;
|
||||
auraInfo.AuraData->CastLevel = 36;
|
||||
auraInfo.AuraData->Applications = 0;
|
||||
auraUpdate.Auras.push_back(std::move(auraInfo));
|
||||
player->SendDirectMessage(auraUpdate.Write());
|
||||
|
||||
WorldPackets::Spells::SpellStart spellStart;
|
||||
spellStart.Cast.CasterGUID = player->GetGUID();
|
||||
spellStart.Cast.CasterUnit = player->GetGUID();
|
||||
spellStart.Cast.CastID = castId;
|
||||
spellStart.Cast.SpellID = SPELL_HOUSING_TUTORIAL_DONE_2;
|
||||
spellStart.Cast.CastFlags = CAST_FLAG_PENDING | CAST_FLAG_HAS_TRAJECTORY | CAST_FLAG_UNKNOWN_3 | CAST_FLAG_UNKNOWN_4;
|
||||
spellStart.Cast.CastTime = 0;
|
||||
player->SendDirectMessage(spellStart.Write());
|
||||
|
||||
WorldPackets::Spells::SpellGo spellGo;
|
||||
spellGo.Cast.CasterGUID = player->GetGUID();
|
||||
spellGo.Cast.CasterUnit = player->GetGUID();
|
||||
spellGo.Cast.CastID = castId;
|
||||
spellGo.Cast.SpellID = SPELL_HOUSING_TUTORIAL_DONE_2;
|
||||
spellGo.Cast.CastFlags = CAST_FLAG_PENDING | CAST_FLAG_UNKNOWN_3 | CAST_FLAG_UNKNOWN_4 | CAST_FLAG_UNKNOWN_9 | CAST_FLAG_UNKNOWN_10;
|
||||
spellGo.Cast.CastFlagsEx = 16;
|
||||
spellGo.Cast.CastFlagsEx2 = 4;
|
||||
spellGo.Cast.CastTime = getMSTime();
|
||||
spellGo.Cast.Target.Flags = TARGET_FLAG_UNIT;
|
||||
spellGo.Cast.HitTargets.push_back(player->GetGUID());
|
||||
spellGo.Cast.HitStatus.emplace_back(uint8(0));
|
||||
spellGo.LogData.Initialize(player);
|
||||
player->SendDirectMessage(spellGo.Write());
|
||||
}
|
||||
|
||||
// Spell 1266699 at slot 50 (same ID as SPELL_HOUSING_PLOT_ENTER_2, different slot + Points)
|
||||
{
|
||||
ObjectGuid castId = ObjectGuid::Create<HighGuid::Cast>(
|
||||
SPELL_CAST_SOURCE_NORMAL, player->GetMapId(), SPELL_HOUSING_TUTORIAL_DONE_3,
|
||||
player->GetMap()->GenerateLowGuid<HighGuid::Cast>());
|
||||
|
||||
WorldPackets::Spells::AuraUpdate auraUpdate;
|
||||
auraUpdate.UpdateAll = false;
|
||||
auraUpdate.UnitGUID = player->GetGUID();
|
||||
|
||||
WorldPackets::Spells::AuraInfo auraInfo;
|
||||
auraInfo.Slot = 50;
|
||||
auraInfo.AuraData.emplace();
|
||||
auraInfo.AuraData->CastID = castId;
|
||||
auraInfo.AuraData->SpellID = SPELL_HOUSING_TUTORIAL_DONE_3;
|
||||
auraInfo.AuraData->Flags = AFLAG_NOCASTER | AFLAG_SCALABLE;
|
||||
auraInfo.AuraData->ActiveFlags = 1;
|
||||
auraInfo.AuraData->CastLevel = 36;
|
||||
auraInfo.AuraData->Applications = 0;
|
||||
auraInfo.AuraData->Points.push_back(1.0f);
|
||||
auraUpdate.Auras.push_back(std::move(auraInfo));
|
||||
player->SendDirectMessage(auraUpdate.Write());
|
||||
|
||||
WorldPackets::Spells::SpellStart spellStart;
|
||||
spellStart.Cast.CasterGUID = player->GetGUID();
|
||||
spellStart.Cast.CasterUnit = player->GetGUID();
|
||||
spellStart.Cast.CastID = castId;
|
||||
spellStart.Cast.SpellID = SPELL_HOUSING_TUTORIAL_DONE_3;
|
||||
spellStart.Cast.CastFlags = CAST_FLAG_PENDING | CAST_FLAG_HAS_TRAJECTORY | CAST_FLAG_UNKNOWN_3 | CAST_FLAG_UNKNOWN_4;
|
||||
spellStart.Cast.CastTime = 0;
|
||||
player->SendDirectMessage(spellStart.Write());
|
||||
|
||||
WorldPackets::Spells::SpellGo spellGo;
|
||||
spellGo.Cast.CasterGUID = player->GetGUID();
|
||||
spellGo.Cast.CasterUnit = player->GetGUID();
|
||||
spellGo.Cast.CastID = castId;
|
||||
spellGo.Cast.SpellID = SPELL_HOUSING_TUTORIAL_DONE_3;
|
||||
spellGo.Cast.CastFlags = CAST_FLAG_PENDING | CAST_FLAG_UNKNOWN_3 | CAST_FLAG_UNKNOWN_4 | CAST_FLAG_UNKNOWN_9 | CAST_FLAG_UNKNOWN_10;
|
||||
spellGo.Cast.CastFlagsEx = 16;
|
||||
spellGo.Cast.CastFlagsEx2 = 4;
|
||||
spellGo.Cast.CastTime = getMSTime();
|
||||
spellGo.Cast.Target.Flags = TARGET_FLAG_UNIT;
|
||||
spellGo.Cast.HitTargets.push_back(player->GetGUID());
|
||||
spellGo.Cast.HitStatus.emplace_back(uint8(0));
|
||||
spellGo.LogData.Initialize(player);
|
||||
player->SendDirectMessage(spellGo.Write());
|
||||
}
|
||||
|
||||
TC_LOG_DEBUG("housing", "HouseInteriorMap::SendPostTutorialAuras: Sent 3 post-tutorial aura sequences "
|
||||
"(1285428@s8, 1285424@s9, 1266699@s50) for player {}",
|
||||
player->GetGUID().ToString());
|
||||
}
|
||||
|
||||
@@ -72,6 +72,10 @@ public:
|
||||
/// Despawn a single decor item by its Housing decor GUID.
|
||||
void DespawnDecorItem(ObjectGuid decorGuid);
|
||||
|
||||
/// Send post-tutorial aura packets so the client knows the tutorial is complete
|
||||
/// and unlocks all editor modes (expert, cleanup, layout, customize).
|
||||
void SendPostTutorialAuras(Player* player);
|
||||
|
||||
private:
|
||||
ObjectGuid _owner;
|
||||
Player* _loadingPlayer; ///< @workaround Player not in ObjectAccessor during login
|
||||
@@ -87,7 +91,7 @@ private:
|
||||
/// GUIDs of all spawned room MeshObjects, indexed by room GUID
|
||||
std::unordered_map<ObjectGuid /*roomGuid*/, std::vector<ObjectGuid>> _roomMeshObjects;
|
||||
|
||||
/// Decor GUID → visual object GUID (for despawning individual decor items)
|
||||
/// Decor GUID ? visual object GUID (for despawning individual decor items)
|
||||
std::unordered_map<ObjectGuid, ObjectGuid> _decorGuidToObjGuid;
|
||||
};
|
||||
|
||||
|
||||
@@ -781,6 +781,15 @@ bool HousingMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/)
|
||||
houseInfo.House.AccessFlags, housing ? "yes" : "no");
|
||||
player->SendDirectMessage(houseInfoPkt);
|
||||
|
||||
// Send SMSG_INITIATIVE_SERVICE_STATUS proactively so IsInitiativeEnabled() returns
|
||||
// true immediately. Without this, the client waits for a poll response before showing
|
||||
// initiative/endeavor UI elements. Sniff-verified: server responds with 0x80 (enabled).
|
||||
{
|
||||
WorldPackets::Housing::InitiativeServiceStatus initStatus;
|
||||
initStatus.ServiceEnabled = true;
|
||||
player->SendDirectMessage(initStatus.Write());
|
||||
}
|
||||
|
||||
// ENTER_PLOT must be sent AFTER SMSG_UPDATE_OBJECT creates the AT on the client.
|
||||
// UPDATE_OBJECT is flushed after AddPlayerToMap returns, so sending ENTER_PLOT
|
||||
// here synchronously would reference an AT GUID the client doesn't know yet.
|
||||
|
||||
Reference in New Issue
Block a user