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:
luis
2026-03-10 13:36:47 -03:00
parent 1e768c19b2
commit 5def5c28d0
7 changed files with 376 additions and 3 deletions
@@ -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);
+155 -2
View File
@@ -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
+2
View File
@@ -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());
}
+5 -1
View File
@@ -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;
};
+9
View File
@@ -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.