Housing: Fix 0x20 crash root cause — Stationary flag on MeshObjects
Retail MeshObjects have HasMeshObject=true, Stationary=false. Our MeshObject constructor had BOTH Stationary=true AND MeshObject=true. The extra Stationary block (16 bytes: X,Y,Z,O) shifted all subsequent parsing of the MeshObject block and fragment data, corrupting the client's entity data and causing NULL+0x20 crashes. Sniff-verified: retail interior components have Stationary=false and HasMeshObject=true. Also restored IsRoom=true on components (sniff confirms ALL retail room components have IsRoom=true).
This commit is contained in:
@@ -24,15 +24,6 @@
|
||||
#include "UpdateData.h"
|
||||
#include "WowCSEntityDefinitions.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
inline void BuildHousingEntityFragments(ByteBuffer& data, std::span<WowCS::EntityFragment const> fragments)
|
||||
{
|
||||
data.append(fragments.data(), fragments.size());
|
||||
data << uint8(WowCS::EntityFragment::End);
|
||||
}
|
||||
}
|
||||
|
||||
HousingRoomEntity::HousingRoomEntity()
|
||||
: WorldObject(false)
|
||||
{
|
||||
@@ -106,7 +97,7 @@ void HousingRoomEntity::BuildCreateUpdateBlockForPlayer(UpdateData* data, Player
|
||||
std::size_t sizePos = buf.wpos();
|
||||
buf << uint32(0);
|
||||
buf << uint8(fieldFlags);
|
||||
BuildHousingEntityFragments(buf, m_entityFragments.GetIds());
|
||||
BuildEntityFragments(buf, m_entityFragments.GetIds());
|
||||
|
||||
for (std::size_t i = 0; i < m_entityFragments.UpdateableCount; ++i)
|
||||
{
|
||||
|
||||
@@ -25,7 +25,10 @@ MeshObject::MeshObject() : WorldObject(false), MapObject()
|
||||
{
|
||||
m_objectTypeId = TYPEID_MESH_OBJECT;
|
||||
|
||||
m_updateFlag.Stationary = true;
|
||||
// Retail MeshObjects: HasPositionFragment=true (from Object), Stationary=false,
|
||||
// HasMeshObject=true. The MeshObject block writes AttachParentGUID + local pos/rot.
|
||||
// Stationary MUST be false ? it writes extra position data that shifts parsing.
|
||||
m_updateFlag.Stationary = false;
|
||||
m_updateFlag.MeshObject = true;
|
||||
|
||||
m_entityFragments.Add(WowCS::EntityFragment::Tag_MeshObject, false);
|
||||
@@ -281,9 +284,14 @@ void MeshObject::InitHousingRoomData(ObjectGuid houseGuid, int32 houseRoomID,
|
||||
if (m_housingRoomData.has_value())
|
||||
return;
|
||||
|
||||
// Set IsRoom=true ? this MeshObject has FHousingRoom_C, so the client's
|
||||
// MeshObjectSystem can safely read FHousingRoom_C.Flags from it.
|
||||
{
|
||||
auto meshData = m_values.ModifyValue(&MeshObject::m_meshObjectData);
|
||||
SetUpdateFieldValue(meshData.ModifyValue(&UF::MeshObjectData::IsRoom), true);
|
||||
}
|
||||
|
||||
// Populate HousingRoomData (FHousingRoom_C fragment data).
|
||||
// Sniff-verified: Room entities carry HouseGUID, HouseRoomID, Flags, FloorIndex,
|
||||
// and a MeshObjects array referencing attached room component MeshObjects.
|
||||
auto roomData = m_values.ModifyValue(&Object::m_housingRoomData, 0);
|
||||
SetUpdateFieldValue(roomData.ModifyValue(&UF::HousingRoomData::HouseGUID), houseGuid);
|
||||
SetUpdateFieldValue(roomData.ModifyValue(&UF::HousingRoomData::HouseRoomID), houseRoomID);
|
||||
@@ -352,7 +360,7 @@ void MeshObject::InitHousingRoomComponentData(ObjectGuid roomGuid,
|
||||
if (m_housingRoomComponentMeshData.has_value())
|
||||
return;
|
||||
|
||||
// Set IsRoom = true on MeshObjectData so the client treats this as a room mesh
|
||||
// Sniff-verified: ALL retail room component MeshObjects have IsRoom=true.
|
||||
{
|
||||
auto meshData = m_values.ModifyValue(&MeshObject::m_meshObjectData);
|
||||
SetUpdateFieldValue(meshData.ModifyValue(&UF::MeshObjectData::IsRoom), true);
|
||||
|
||||
@@ -188,7 +188,7 @@ void BaseEntity::BuildValuesUpdateBlockForPlayer(UpdateData* data, Player const*
|
||||
data->AddUpdateBlock();
|
||||
}
|
||||
|
||||
inline void BaseEntity::BuildEntityFragments(ByteBuffer& data, std::span<WowCS::EntityFragment const> fragments)
|
||||
void BaseEntity::BuildEntityFragments(ByteBuffer& data, std::span<WowCS::EntityFragment const> fragments)
|
||||
{
|
||||
data.append(fragments.data(), fragments.size());
|
||||
data << uint8(WowCS::EntityFragment::End);
|
||||
|
||||
@@ -45,11 +45,11 @@
|
||||
|
||||
HouseInteriorMap::HouseInteriorMap(uint32 id, time_t expiry, uint32 instanceId, ObjectGuid const& owner)
|
||||
: Map(id, expiry, instanceId, DIFFICULTY_NORMAL),
|
||||
_owner(owner),
|
||||
_loadingPlayer(nullptr),
|
||||
_sourceNeighborhoodMapId(0),
|
||||
_sourcePlotIndex(0),
|
||||
_roomsSpawned(false)
|
||||
_owner(owner),
|
||||
_loadingPlayer(nullptr),
|
||||
_sourceNeighborhoodMapId(0),
|
||||
_sourcePlotIndex(0),
|
||||
_roomsSpawned(false)
|
||||
{
|
||||
HouseInteriorMap::InitVisibilityDistance();
|
||||
|
||||
@@ -152,7 +152,7 @@ void HouseInteriorMap::SpawnRoomMeshObjects(Housing* housing, int32 factionRestr
|
||||
|
||||
// 2. RoomWmoData ? Geobox bounds (bounding box for OutsidePlotBounds check)
|
||||
float geoMinX = -35.0f, geoMinY = -30.0f, geoMinZ = -1.01f;
|
||||
float geoMaxX = 35.0f, geoMaxY = 30.0f, geoMaxZ = 125.01f;
|
||||
float geoMaxX = 35.0f, geoMaxY = 30.0f, geoMaxZ = 125.01f;
|
||||
RoomWmoDataEntry const* wmoData = roomWmoDataID ? sRoomWmoDataStore.LookupEntry(roomWmoDataID) : nullptr;
|
||||
if (wmoData)
|
||||
{
|
||||
@@ -206,20 +206,33 @@ void HouseInteriorMap::SpawnRoomMeshObjects(Housing* housing, int32 factionRestr
|
||||
GridCoord roomGrid = Trinity::ComputeGridCoord(roomX, roomY);
|
||||
GridMarkNoUnload(roomGrid.x_coord, roomGrid.y_coord);
|
||||
|
||||
// --- Phase 1: Create component MeshObjects ---
|
||||
// Retail has TWO entity types per room:
|
||||
// 1. Component MeshObjects (objectType=56, FHousingRoomComponentMesh_C) ? visual meshes
|
||||
// 2. HousingRoomEntity (objectType=18, Housing/2 GUID, FHousingRoom_C) ? room data + doors
|
||||
// The MeshObjects attach to the HousingRoomEntity via AttachParentGUID = room->Guid.
|
||||
// We do NOT create a separate "root" MeshObject with FHousingRoom_C ? that causes
|
||||
// duplicate FHousingRoom_C in the client's archetype query, and the MeshObject GUID
|
||||
// (not Housing type) makes GetRoomGUID() return nil in the layout editor.
|
||||
|
||||
int32 roomFlags = roomData->IsBaseRoom() ? 1 : 0;
|
||||
int32 floorIndex = 0;
|
||||
ObjectGuid roomHousingGuid = room->Guid; // Housing/2 GUID for attach parent
|
||||
|
||||
// --- Phase 1b: Create all component MeshObjects ---
|
||||
// --- Phase 1: Create HousingRoomEntity FIRST ---
|
||||
// Must exist on the map BEFORE component MeshObjects because components use
|
||||
// AttachParentGUID = roomHousingGuid. The client resolves this GUID during
|
||||
// entity creation ? if the parent doesn't exist, it crashes (NULL+0x20).
|
||||
// Door data is added after components are created (Phase 3).
|
||||
HousingRoomEntity* housingRoom = new HousingRoomEntity();
|
||||
PhasingHandler::InitDbPhaseShift(housingRoom->GetPhaseShift(), PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
|
||||
housingRoom->SetHouseGUID(housing->GetHouseGuid());
|
||||
housingRoom->SetHouseRoomID(room->RoomEntryId);
|
||||
housingRoom->SetFlags(roomFlags);
|
||||
housingRoom->SetFloorIndex(floorIndex);
|
||||
housingRoom->SetMirroredPosition(roomPos, roomRot, 1.0f);
|
||||
|
||||
if (!housingRoom->Create(roomHousingGuid, this, roomPos))
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap: Failed to add HousingRoomEntity to map (roomEntry={})",
|
||||
room->RoomEntryId);
|
||||
delete housingRoom;
|
||||
continue;
|
||||
}
|
||||
_roomEntities.push_back(housingRoom);
|
||||
|
||||
// --- Phase 2: Create all component MeshObjects ---
|
||||
|
||||
std::vector<MeshObject*> componentMeshes;
|
||||
uint32 wallCount = 0, floorCount = 0, ceilingCount = 0, doorwayCount = 0;
|
||||
@@ -270,10 +283,10 @@ void HouseInteriorMap::SpawnRoomMeshObjects(Housing* housing, int32 factionRestr
|
||||
// Hardcoded fallback (sniff-verified: walls=24, floors=40, ceilings=54)
|
||||
switch (comp.Type)
|
||||
{
|
||||
case 1: roomComponentTextureID = 24; break;
|
||||
case 2: roomComponentTextureID = 40; break;
|
||||
case 3: roomComponentTextureID = 54; break;
|
||||
default: break;
|
||||
case 1: roomComponentTextureID = 24; break;
|
||||
case 2: roomComponentTextureID = 40; break;
|
||||
case 3: roomComponentTextureID = 54; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,14 +337,14 @@ void HouseInteriorMap::SpawnRoomMeshObjects(Housing* housing, int32 factionRestr
|
||||
// Count by component type for diagnostic summary
|
||||
switch (comp.Type)
|
||||
{
|
||||
case HOUSING_ROOM_COMPONENT_WALL: ++wallCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_FLOOR: ++floorCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_CEILING: ++ceilingCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_STAIRS: ++stairsCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_PILLAR: ++pillarCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_DOORWAY_WALL: ++doorwayWallCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_DOORWAY: ++doorwayCount; break;
|
||||
default: ++otherCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_WALL: ++wallCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_FLOOR: ++floorCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_CEILING: ++ceilingCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_STAIRS: ++stairsCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_PILLAR: ++pillarCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_DOORWAY_WALL: ++doorwayWallCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_DOORWAY: ++doorwayCount; break;
|
||||
default: ++otherCount; break;
|
||||
}
|
||||
|
||||
TC_LOG_ERROR("housing", " Component: compID={} type={} fileDataID={} optionID={} themeID={} "
|
||||
@@ -369,36 +382,17 @@ void HouseInteriorMap::SpawnRoomMeshObjects(Housing* housing, int32 factionRestr
|
||||
}
|
||||
}
|
||||
|
||||
// --- Phase 3: Create HousingRoomEntity (objectType=18, Housing/2 GUID) ---
|
||||
// This is the ONLY entity with FHousingRoom_C ? the client's archetype query uses it
|
||||
// to create layout editor pins. GetRoomGUID() validates the Housing GUID type.
|
||||
// --- Phase 4: Populate HousingRoomEntity with component GUIDs + door data ---
|
||||
// The entity was created in Phase 1 (before components) so it exists when
|
||||
// components reference it via AttachParentGUID. Now add the child list + doors.
|
||||
{
|
||||
HousingRoomEntity* housingRoom = new HousingRoomEntity();
|
||||
PhasingHandler::InitDbPhaseShift(housingRoom->GetPhaseShift(), PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
|
||||
housingRoom->SetHouseGUID(housing->GetHouseGuid());
|
||||
housingRoom->SetHouseRoomID(room->RoomEntryId);
|
||||
housingRoom->SetFlags(roomFlags);
|
||||
housingRoom->SetFloorIndex(floorIndex);
|
||||
housingRoom->SetMirroredPosition(roomPos, roomRot, 1.0f);
|
||||
|
||||
// Add component MeshObject GUIDs to the room entity's MeshObjects array.
|
||||
// Only component MeshObjects (with FHousingRoomComponentMesh_C) belong here ?
|
||||
// the root room MeshObject has FHousingRoom_C instead, and the client crashes
|
||||
// if it encounters a child without FHousingRoomComponentMesh_C (NULL+0x21).
|
||||
for (MeshObject* comp : componentMeshes)
|
||||
{
|
||||
if (comp->IsInWorld())
|
||||
housingRoom->AddMeshObject(comp->GetGUID());
|
||||
}
|
||||
|
||||
// Add door data from components that serve as connection points.
|
||||
// DB2 RoomComponent.ConnectionType=1 (All) marks door-capable components.
|
||||
// For walls, also require axis-aligned position (one of X,Y non-zero) to
|
||||
// exclude corner pieces ? Room 1 has CT=1 on ALL walls but corners (30-33)
|
||||
// at diagonal positions are NOT doors in the sniff.
|
||||
// Sniff-verified: Room 46 has 1 door (comp 206 CT=1), Room 1 has 4 doors
|
||||
// (comp 26-29 CT=1, axis-aligned). Corner pieces 30-33 have CT=1 but are
|
||||
// filtered out by the axis-aligned check.
|
||||
// Add door data from door-capable wall components
|
||||
uint32 doorCount = 0;
|
||||
for (RoomComponentData const& comp : *components)
|
||||
{
|
||||
@@ -406,15 +400,9 @@ void HouseInteriorMap::SpawnRoomMeshObjects(Housing* housing, int32 factionRestr
|
||||
|
||||
if (comp.Type == HOUSING_ROOM_COMPONENT_DOORWAY ||
|
||||
comp.Type == HOUSING_ROOM_COMPONENT_DOORWAY_WALL)
|
||||
{
|
||||
// Explicit doorway component (Type=6/7) ? custom geometry rooms
|
||||
isDoor = true;
|
||||
}
|
||||
else if (comp.Type == HOUSING_ROOM_COMPONENT_WALL && comp.ConnectionType != 0)
|
||||
{
|
||||
// Wall with ConnectionType=1 (All) ? potential door connection point.
|
||||
// Additional filter: must be axis-aligned (flat wall, not corner piece).
|
||||
// Flat walls have only one of X,Y non-zero; corners have both.
|
||||
bool hasX = std::abs(comp.OffsetPos[0]) > 0.5f;
|
||||
bool hasY = std::abs(comp.OffsetPos[1]) > 0.5f;
|
||||
if (hasX != hasY)
|
||||
@@ -424,21 +412,19 @@ void HouseInteriorMap::SpawnRoomMeshObjects(Housing* housing, int32 factionRestr
|
||||
if (!isDoor)
|
||||
continue;
|
||||
|
||||
// Determine which adjacent room this door faces (for AttachedRoomGUID)
|
||||
ObjectGuid attachedRoomGuid;
|
||||
if (comp.OffsetPos[0] > 0.5f) // positive X ? faces next slot
|
||||
if (comp.OffsetPos[0] > 0.5f)
|
||||
{
|
||||
auto itr = slotToRoomGuid.find(room->SlotIndex + 1);
|
||||
if (itr != slotToRoomGuid.end())
|
||||
attachedRoomGuid = itr->second;
|
||||
}
|
||||
else if (comp.OffsetPos[0] < -0.5f) // negative X ? faces previous slot
|
||||
else if (comp.OffsetPos[0] < -0.5f)
|
||||
{
|
||||
auto itr = slotToRoomGuid.find(room->SlotIndex - 1);
|
||||
if (itr != slotToRoomGuid.end())
|
||||
attachedRoomGuid = itr->second;
|
||||
}
|
||||
// Y-axis doors are potential future connection points (AttachedRoomGUID stays empty)
|
||||
|
||||
Position doorOffset(comp.OffsetPos[0], comp.OffsetPos[1], comp.OffsetPos[2]);
|
||||
uint8 connType = comp.ConnectionType > 0 ? comp.ConnectionType : 1;
|
||||
@@ -446,19 +432,6 @@ void HouseInteriorMap::SpawnRoomMeshObjects(Housing* housing, int32 factionRestr
|
||||
++doorCount;
|
||||
}
|
||||
|
||||
// WorldObject: AddToMap makes it visible through the visibility system,
|
||||
// sent in the SAME UPDATE_OBJECT as room MeshObjects ? matching retail.
|
||||
if (!housingRoom->Create(roomHousingGuid, this, roomPos))
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap: Failed to add HousingRoomEntity to map (roomEntry={})",
|
||||
room->RoomEntryId);
|
||||
delete housingRoom;
|
||||
housingRoom = nullptr;
|
||||
}
|
||||
|
||||
if (housingRoom)
|
||||
_roomEntities.push_back(housingRoom);
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnRoomMeshObjects: Created HousingRoomEntity "
|
||||
"guid={} roomEntry={} slot={} meshObjects={} doors={}",
|
||||
roomHousingGuid.ToString(), room->RoomEntryId, room->SlotIndex,
|
||||
@@ -803,7 +776,7 @@ void HouseInteriorMap::SpawnSingleInteriorDecor(Housing::PlacedDecor const& deco
|
||||
float roomFacing = roomWorldPos.GetOrientation();
|
||||
float cosF = std::cos(roomFacing);
|
||||
float sinF = std::sin(roomFacing);
|
||||
localX = cosF * dx + sinF * dy;
|
||||
localX = cosF * dx + sinF * dy;
|
||||
localY = -sinF * dx + cosF * dy;
|
||||
localZ = worldZ - roomWorldPos.GetPositionZ();
|
||||
}
|
||||
@@ -962,11 +935,17 @@ bool HouseInteriorMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/
|
||||
housing->SetInInterior(true);
|
||||
|
||||
// Spawn room meshes on first entry
|
||||
if (!_roomsSpawned && player->GetGUID() == _owner)
|
||||
if (player->GetGUID() == _owner)
|
||||
{
|
||||
// Always force a fresh spawn on login ? old entities from a previous binary/session
|
||||
// may have stale fragment formats (e.g., root MeshObjects with FHousingRoom_C that
|
||||
// no longer exist in the current code). DespawnAll is safe here because the player
|
||||
// hasn't received any entities yet (no DESTROY goes to the client).
|
||||
if (_roomsSpawned)
|
||||
DespawnAllRoomMeshObjects();
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::AddPlayerToMap: === SPAWNING ROOMS ===");
|
||||
|
||||
// Log each room for debugging
|
||||
for (Housing::Room const* room : housing->GetRooms())
|
||||
{
|
||||
TC_LOG_ERROR("housing", " Room: guid={} entryId={} slot={} orientation={} mirrored={}",
|
||||
@@ -979,12 +958,6 @@ bool HouseInteriorMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/
|
||||
SpawnRoomMeshObjects(housing, faction);
|
||||
_roomsSpawned = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::AddPlayerToMap: Rooms already spawned "
|
||||
"(_roomsSpawned={}, isOwner={})",
|
||||
_roomsSpawned, player->GetGUID() == _owner);
|
||||
}
|
||||
|
||||
// Always spawn interior decor (handles both first entry and re-entry).
|
||||
// SpawnInteriorDecor skips already-spawned decor via _decorGuidToObjGuid check.
|
||||
@@ -1031,215 +1004,215 @@ bool HouseInteriorMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/
|
||||
uint32 settingsFlags = housing->GetSettingsFlags();
|
||||
|
||||
player->m_Events.AddEventAtOffset([this, playerGuid, houseGuid, neighborhoodGuid, accountGuid, plotIndex, settingsFlags]()
|
||||
{
|
||||
Player* p = ObjectAccessor::FindPlayer(playerGuid);
|
||||
if (!p || !p->IsInWorld())
|
||||
return;
|
||||
|
||||
Housing* housing = p->GetHousing();
|
||||
if (!housing)
|
||||
return;
|
||||
|
||||
// ENTER_PLOT is sent AFTER the AT CREATE in step 8 below.
|
||||
// The sequence is: AT CREATE ? ENTER_PLOT ? re-send Status+Perms.
|
||||
// ENTER_PLOT fires HOUSE_PLOT_ENTERED (FrameScript event 1073) which
|
||||
// loads Blizzard_HousingControls. The handler resets editor state, so
|
||||
// Status+Perms are re-sent afterward to re-establish context.
|
||||
|
||||
// Steps 1-3 (HouseInfo, Status, Permissions) moved to AFTER
|
||||
// ENTER_PLOT below. ENTER_PLOT resets editor state, so sending
|
||||
// Status+Permissions before it is wasteful. The exterior AT handler
|
||||
// (at_housing_plot.cpp) also sends Status+Permissions AFTER ENTER_PLOT.
|
||||
|
||||
// 1) PostTutorialAuras (slots 8, 9, 50)
|
||||
SendPostTutorialAuras(p);
|
||||
|
||||
// 5) Account CREATE + HousingPlayerHouseEntity + decor
|
||||
{
|
||||
housing->PopulateCatalogStorageEntries();
|
||||
housing->SyncUpdateFields();
|
||||
Player* p = ObjectAccessor::FindPlayer(playerGuid);
|
||||
if (!p || !p->IsInWorld())
|
||||
return;
|
||||
|
||||
WorldSession* session = p->GetSession();
|
||||
UpdateData storageUpdate(p->GetMapId());
|
||||
WorldPacket storagePacket;
|
||||
Housing* housing = p->GetHousing();
|
||||
if (!housing)
|
||||
return;
|
||||
|
||||
session->GetBattlenetAccount().BuildCreateUpdateBlockForPlayer(&storageUpdate, p);
|
||||
p->m_clientGUIDs.insert(session->GetBattlenetAccount().GetGUID());
|
||||
// ENTER_PLOT is sent AFTER the AT CREATE in step 8 below.
|
||||
// The sequence is: AT CREATE ? ENTER_PLOT ? re-send Status+Perms.
|
||||
// ENTER_PLOT fires HOUSE_PLOT_ENTERED (FrameScript event 1073) which
|
||||
// loads Blizzard_HousingControls. The handler resets editor state, so
|
||||
// Status+Perms are re-sent afterward to re-establish context.
|
||||
|
||||
if (p->HaveAtClient(&session->GetHousingPlayerHouseEntity()))
|
||||
session->GetHousingPlayerHouseEntity().BuildValuesUpdateBlockForPlayer(&storageUpdate, p);
|
||||
else
|
||||
// Steps 1-3 (HouseInfo, Status, Permissions) moved to AFTER
|
||||
// ENTER_PLOT below. ENTER_PLOT resets editor state, so sending
|
||||
// Status+Permissions before it is wasteful. The exterior AT handler
|
||||
// (at_housing_plot.cpp) also sends Status+Permissions AFTER ENTER_PLOT.
|
||||
|
||||
// 1) PostTutorialAuras (slots 8, 9, 50)
|
||||
SendPostTutorialAuras(p);
|
||||
|
||||
// 5) Account CREATE + HousingPlayerHouseEntity + decor
|
||||
{
|
||||
session->GetHousingPlayerHouseEntity().BuildCreateUpdateBlockForPlayer(&storageUpdate, p);
|
||||
p->m_clientGUIDs.insert(session->GetHousingPlayerHouseEntity().GetGUID());
|
||||
}
|
||||
housing->PopulateCatalogStorageEntries();
|
||||
housing->SyncUpdateFields();
|
||||
|
||||
uint32 meshCreateCount = 0;
|
||||
for (auto const& [decorGuid, meshObjGuid] : _decorGuidToObjGuid)
|
||||
{
|
||||
MeshObject* meshObj = GetMeshObject(meshObjGuid);
|
||||
if (!meshObj || !meshObj->IsInWorld())
|
||||
continue;
|
||||
meshObj->BuildCreateUpdateBlockForPlayer(&storageUpdate, p);
|
||||
p->m_clientGUIDs.insert(meshObjGuid);
|
||||
++meshCreateCount;
|
||||
}
|
||||
WorldSession* session = p->GetSession();
|
||||
UpdateData storageUpdate(p->GetMapId());
|
||||
WorldPacket storagePacket;
|
||||
|
||||
// HousingRoomEntity CREATEs are now sent via the map visibility
|
||||
// system (AddToMap in SpawnRoomMeshObjects). No manual send needed.
|
||||
session->GetBattlenetAccount().BuildCreateUpdateBlockForPlayer(&storageUpdate, p);
|
||||
p->m_clientGUIDs.insert(session->GetBattlenetAccount().GetGUID());
|
||||
|
||||
storageUpdate.BuildPacket(&storagePacket);
|
||||
p->SendDirectMessage(&storagePacket);
|
||||
|
||||
session->GetBattlenetAccount().ClearUpdateMask(true);
|
||||
session->GetHousingPlayerHouseEntity().ClearUpdateMask(true);
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap deferred: Sent Account+budget+{} decor for {}",
|
||||
meshCreateCount, playerGuid.ToString());
|
||||
}
|
||||
|
||||
// Map-level Housing/3 entity (objectType=18) is now included in
|
||||
// the initial UPDATE_OBJECT via Player::BuildCreateUpdateBlockForPlayer.
|
||||
// It must be in the initial batch for the client's type-18 render init.
|
||||
|
||||
// 7) InitiativeServiceStatus
|
||||
{
|
||||
WorldPackets::Housing::InitiativeServiceStatus initStatus;
|
||||
initStatus.ServiceEnabled = true;
|
||||
p->SendDirectMessage(initStatus.Write());
|
||||
}
|
||||
|
||||
// 7) Create AND send the interior plot AreaTrigger LAST.
|
||||
// The AT must be created here (not in pre-spawn) because if it's
|
||||
// on the map during AddPlayerToMap, the visibility system includes
|
||||
// it in the initial UPDATE_OBJECT ? before Status+Permissions.
|
||||
// Retail sends the AT in a separate UPDATE_OBJECT (#11752) AFTER
|
||||
// the main entity data. The client fires HOUSE_PLOT_ENTERED on AT
|
||||
// receipt and immediately checks IsHouseEditorStatusAvailable(),
|
||||
// which requires permissions to already be set.
|
||||
if (_interiorPlotAT.IsEmpty())
|
||||
{
|
||||
float atX = _originX;
|
||||
float atY = _originY;
|
||||
float atZ = _originZ;
|
||||
LoadGrid(atX, atY);
|
||||
|
||||
Position atPos(atX, atY, atZ, 0.0f);
|
||||
AreaTrigger* plotAt = AreaTrigger::CreateStaticAreaTrigger(
|
||||
{ .Id = 37358, .IsCustom = false }, this, atPos, -1);
|
||||
|
||||
if (plotAt)
|
||||
{
|
||||
PhasingHandler::InitDbPhaseShift(plotAt->GetPhaseShift(), PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
|
||||
plotAt->InitHousingPlotData(
|
||||
housing->GetPlotIndex(),
|
||||
playerGuid,
|
||||
houseGuid,
|
||||
accountGuid);
|
||||
|
||||
if (AddToMap(plotAt))
|
||||
{
|
||||
_interiorPlotAT = plotAt->GetGUID();
|
||||
|
||||
// Send CREATE directly to the player (visibility system
|
||||
// won't send it since the player is already on the map)
|
||||
UpdateData atUpdate(p->GetMapId());
|
||||
plotAt->BuildCreateUpdateBlockForPlayer(&atUpdate, p);
|
||||
p->m_clientGUIDs.insert(plotAt->GetGUID());
|
||||
|
||||
WorldPacket atPacket;
|
||||
atUpdate.BuildPacket(&atPacket);
|
||||
p->SendDirectMessage(&atPacket);
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap deferred: Created+sent interior plot AT "
|
||||
"guid={} at ({:.1f},{:.1f},{:.1f}) plotIndex={} for {}",
|
||||
plotAt->GetGUID().ToString(), atX, atY, atZ,
|
||||
housing->GetPlotIndex(), playerGuid.ToString());
|
||||
|
||||
// 8) Plot enter spell packets ? same as exterior AT overlap.
|
||||
// Sniff-verified: exterior sends 3 spell+aura sequences
|
||||
// (1239847@slot50, 469226@slot56, 1266699@slot9) before
|
||||
// ENTER_PLOT. These trigger editor availability on the client.
|
||||
{
|
||||
plotAt->SetAreaTriggerFlag(AreaTriggerFieldFlags::HasPlayers);
|
||||
|
||||
// Spell 1239847 at slot 50 (plot enter tracking)
|
||||
{
|
||||
ObjectGuid castId = ObjectGuid::Create<HighGuid::Cast>(
|
||||
SPELL_CAST_SOURCE_NORMAL, p->GetMapId(), SPELL_HOUSING_PLOT_ENTER,
|
||||
GenerateLowGuid<HighGuid::Cast>());
|
||||
WorldPackets::Spells::AuraUpdate au;
|
||||
au.UpdateAll = false;
|
||||
au.UnitGUID = p->GetGUID();
|
||||
WorldPackets::Spells::AuraInfo ai;
|
||||
ai.Slot = 55; // sniff-verified: retail uses slot 55
|
||||
ai.AuraData.emplace();
|
||||
ai.AuraData->CastID = castId;
|
||||
ai.AuraData->SpellID = SPELL_HOUSING_PLOT_ENTER;
|
||||
ai.AuraData->Flags = AFLAG_NOCASTER;
|
||||
ai.AuraData->ActiveFlags = 1; // sniff-verified: retail uses 1
|
||||
ai.AuraData->CastLevel = 36;
|
||||
au.Auras.push_back(std::move(ai));
|
||||
p->SendDirectMessage(au.Write());
|
||||
}
|
||||
|
||||
// Spell 469226 at slot 56 (plot context)
|
||||
{
|
||||
ObjectGuid castId = ObjectGuid::Create<HighGuid::Cast>(
|
||||
SPELL_CAST_SOURCE_NORMAL, p->GetMapId(), SPELL_HOUSING_PLOT_PRESENCE,
|
||||
GenerateLowGuid<HighGuid::Cast>());
|
||||
WorldPackets::Spells::AuraUpdate au;
|
||||
au.UpdateAll = false;
|
||||
au.UnitGUID = p->GetGUID();
|
||||
WorldPackets::Spells::AuraInfo ai;
|
||||
ai.Slot = 56;
|
||||
ai.AuraData.emplace();
|
||||
ai.AuraData->CastID = castId;
|
||||
ai.AuraData->SpellID = SPELL_HOUSING_PLOT_PRESENCE;
|
||||
ai.AuraData->Flags = AFLAG_NOCASTER;
|
||||
ai.AuraData->ActiveFlags = 1;
|
||||
ai.AuraData->CastLevel = 36;
|
||||
au.Auras.push_back(std::move(ai));
|
||||
p->SendDirectMessage(au.Write());
|
||||
}
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap deferred: Sent plot enter spells (slots 50+56) for {}",
|
||||
playerGuid.ToString());
|
||||
}
|
||||
|
||||
// 9) ENTER_PLOT ? fires HOUSE_PLOT_ENTERED on the client.
|
||||
// The client handler (NeighborhoodSystem vtable[22]) signals
|
||||
// FrameScript event 1073, which dispatches to all UI frames
|
||||
// with OnEvent handlers, triggering the HOUSE_PLOT_ENTERED
|
||||
// Lua event that loads Blizzard_HousingControls.
|
||||
// MUST come AFTER the AT CREATE so the client can look up
|
||||
// the AT GUID in its entity table.
|
||||
{
|
||||
WorldPackets::Neighborhood::NeighborhoodPlayerEnterPlot enterPlot;
|
||||
enterPlot.NeighborhoodEntityGuid = plotAt->GetGUID();
|
||||
p->SendDirectMessage(enterPlot.Write());
|
||||
}
|
||||
|
||||
// Sniff-verified: retail does NOT proactively send Status+Perms.
|
||||
// The client polls via CMSGs after receiving ENTER_PLOT, and
|
||||
// our CMSG handlers (HandleHousingHouseStatus, HandleHousingGetPlayerPermissions)
|
||||
// respond correctly with FlagByte=0xE0 and PermissionFlags=0xE0.
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap deferred: Sent ENTER_PLOT for {} (Status+Perms reactive via CMSG handlers)",
|
||||
playerGuid.ToString());
|
||||
}
|
||||
if (p->HaveAtClient(&session->GetHousingPlayerHouseEntity()))
|
||||
session->GetHousingPlayerHouseEntity().BuildValuesUpdateBlockForPlayer(&storageUpdate, p);
|
||||
else
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap deferred: AddToMap failed for interior plot AT");
|
||||
delete plotAt;
|
||||
session->GetHousingPlayerHouseEntity().BuildCreateUpdateBlockForPlayer(&storageUpdate, p);
|
||||
p->m_clientGUIDs.insert(session->GetHousingPlayerHouseEntity().GetGUID());
|
||||
}
|
||||
|
||||
uint32 meshCreateCount = 0;
|
||||
for (auto const& [decorGuid, meshObjGuid] : _decorGuidToObjGuid)
|
||||
{
|
||||
MeshObject* meshObj = GetMeshObject(meshObjGuid);
|
||||
if (!meshObj || !meshObj->IsInWorld())
|
||||
continue;
|
||||
meshObj->BuildCreateUpdateBlockForPlayer(&storageUpdate, p);
|
||||
p->m_clientGUIDs.insert(meshObjGuid);
|
||||
++meshCreateCount;
|
||||
}
|
||||
|
||||
// HousingRoomEntity CREATEs are now sent via the map visibility
|
||||
// system (AddToMap in SpawnRoomMeshObjects). No manual send needed.
|
||||
|
||||
storageUpdate.BuildPacket(&storagePacket);
|
||||
p->SendDirectMessage(&storagePacket);
|
||||
|
||||
session->GetBattlenetAccount().ClearUpdateMask(true);
|
||||
session->GetHousingPlayerHouseEntity().ClearUpdateMask(true);
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap deferred: Sent Account+budget+{} decor for {}",
|
||||
meshCreateCount, playerGuid.ToString());
|
||||
}
|
||||
|
||||
// Map-level Housing/3 entity (objectType=18) is now included in
|
||||
// the initial UPDATE_OBJECT via Player::BuildCreateUpdateBlockForPlayer.
|
||||
// It must be in the initial batch for the client's type-18 render init.
|
||||
|
||||
// 7) InitiativeServiceStatus
|
||||
{
|
||||
WorldPackets::Housing::InitiativeServiceStatus initStatus;
|
||||
initStatus.ServiceEnabled = true;
|
||||
p->SendDirectMessage(initStatus.Write());
|
||||
}
|
||||
|
||||
// 7) Create AND send the interior plot AreaTrigger LAST.
|
||||
// The AT must be created here (not in pre-spawn) because if it's
|
||||
// on the map during AddPlayerToMap, the visibility system includes
|
||||
// it in the initial UPDATE_OBJECT ? before Status+Permissions.
|
||||
// Retail sends the AT in a separate UPDATE_OBJECT (#11752) AFTER
|
||||
// the main entity data. The client fires HOUSE_PLOT_ENTERED on AT
|
||||
// receipt and immediately checks IsHouseEditorStatusAvailable(),
|
||||
// which requires permissions to already be set.
|
||||
if (_interiorPlotAT.IsEmpty())
|
||||
{
|
||||
float atX = _originX;
|
||||
float atY = _originY;
|
||||
float atZ = _originZ;
|
||||
LoadGrid(atX, atY);
|
||||
|
||||
Position atPos(atX, atY, atZ, 0.0f);
|
||||
AreaTrigger* plotAt = AreaTrigger::CreateStaticAreaTrigger(
|
||||
{ .Id = 37358, .IsCustom = false }, this, atPos, -1);
|
||||
|
||||
if (plotAt)
|
||||
{
|
||||
PhasingHandler::InitDbPhaseShift(plotAt->GetPhaseShift(), PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
|
||||
plotAt->InitHousingPlotData(
|
||||
housing->GetPlotIndex(),
|
||||
playerGuid,
|
||||
houseGuid,
|
||||
accountGuid);
|
||||
|
||||
if (AddToMap(plotAt))
|
||||
{
|
||||
_interiorPlotAT = plotAt->GetGUID();
|
||||
|
||||
// Send CREATE directly to the player (visibility system
|
||||
// won't send it since the player is already on the map)
|
||||
UpdateData atUpdate(p->GetMapId());
|
||||
plotAt->BuildCreateUpdateBlockForPlayer(&atUpdate, p);
|
||||
p->m_clientGUIDs.insert(plotAt->GetGUID());
|
||||
|
||||
WorldPacket atPacket;
|
||||
atUpdate.BuildPacket(&atPacket);
|
||||
p->SendDirectMessage(&atPacket);
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap deferred: Created+sent interior plot AT "
|
||||
"guid={} at ({:.1f},{:.1f},{:.1f}) plotIndex={} for {}",
|
||||
plotAt->GetGUID().ToString(), atX, atY, atZ,
|
||||
housing->GetPlotIndex(), playerGuid.ToString());
|
||||
|
||||
// 8) Plot enter spell packets ? same as exterior AT overlap.
|
||||
// Sniff-verified: exterior sends 3 spell+aura sequences
|
||||
// (1239847@slot50, 469226@slot56, 1266699@slot9) before
|
||||
// ENTER_PLOT. These trigger editor availability on the client.
|
||||
{
|
||||
plotAt->SetAreaTriggerFlag(AreaTriggerFieldFlags::HasPlayers);
|
||||
|
||||
// Spell 1239847 at slot 50 (plot enter tracking)
|
||||
{
|
||||
ObjectGuid castId = ObjectGuid::Create<HighGuid::Cast>(
|
||||
SPELL_CAST_SOURCE_NORMAL, p->GetMapId(), SPELL_HOUSING_PLOT_ENTER,
|
||||
GenerateLowGuid<HighGuid::Cast>());
|
||||
WorldPackets::Spells::AuraUpdate au;
|
||||
au.UpdateAll = false;
|
||||
au.UnitGUID = p->GetGUID();
|
||||
WorldPackets::Spells::AuraInfo ai;
|
||||
ai.Slot = 55; // sniff-verified: retail uses slot 55
|
||||
ai.AuraData.emplace();
|
||||
ai.AuraData->CastID = castId;
|
||||
ai.AuraData->SpellID = SPELL_HOUSING_PLOT_ENTER;
|
||||
ai.AuraData->Flags = AFLAG_NOCASTER;
|
||||
ai.AuraData->ActiveFlags = 1; // sniff-verified: retail uses 1
|
||||
ai.AuraData->CastLevel = 36;
|
||||
au.Auras.push_back(std::move(ai));
|
||||
p->SendDirectMessage(au.Write());
|
||||
}
|
||||
|
||||
// Spell 469226 at slot 56 (plot context)
|
||||
{
|
||||
ObjectGuid castId = ObjectGuid::Create<HighGuid::Cast>(
|
||||
SPELL_CAST_SOURCE_NORMAL, p->GetMapId(), SPELL_HOUSING_PLOT_PRESENCE,
|
||||
GenerateLowGuid<HighGuid::Cast>());
|
||||
WorldPackets::Spells::AuraUpdate au;
|
||||
au.UpdateAll = false;
|
||||
au.UnitGUID = p->GetGUID();
|
||||
WorldPackets::Spells::AuraInfo ai;
|
||||
ai.Slot = 56;
|
||||
ai.AuraData.emplace();
|
||||
ai.AuraData->CastID = castId;
|
||||
ai.AuraData->SpellID = SPELL_HOUSING_PLOT_PRESENCE;
|
||||
ai.AuraData->Flags = AFLAG_NOCASTER;
|
||||
ai.AuraData->ActiveFlags = 1;
|
||||
ai.AuraData->CastLevel = 36;
|
||||
au.Auras.push_back(std::move(ai));
|
||||
p->SendDirectMessage(au.Write());
|
||||
}
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap deferred: Sent plot enter spells (slots 50+56) for {}",
|
||||
playerGuid.ToString());
|
||||
}
|
||||
|
||||
// 9) ENTER_PLOT ? fires HOUSE_PLOT_ENTERED on the client.
|
||||
// The client handler (NeighborhoodSystem vtable[22]) signals
|
||||
// FrameScript event 1073, which dispatches to all UI frames
|
||||
// with OnEvent handlers, triggering the HOUSE_PLOT_ENTERED
|
||||
// Lua event that loads Blizzard_HousingControls.
|
||||
// MUST come AFTER the AT CREATE so the client can look up
|
||||
// the AT GUID in its entity table.
|
||||
{
|
||||
WorldPackets::Neighborhood::NeighborhoodPlayerEnterPlot enterPlot;
|
||||
enterPlot.NeighborhoodEntityGuid = plotAt->GetGUID();
|
||||
p->SendDirectMessage(enterPlot.Write());
|
||||
}
|
||||
|
||||
// Sniff-verified: retail does NOT proactively send Status+Perms.
|
||||
// The client polls via CMSGs after receiving ENTER_PLOT, and
|
||||
// our CMSG handlers (HandleHousingHouseStatus, HandleHousingGetPlayerPermissions)
|
||||
// respond correctly with FlagByte=0xE0 and PermissionFlags=0xE0.
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap deferred: Sent ENTER_PLOT for {} (Status+Perms reactive via CMSG handlers)",
|
||||
playerGuid.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap deferred: AddToMap failed for interior plot AT");
|
||||
delete plotAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap deferred: Complete ? "
|
||||
"HouseInfo+Status+Perms+Auras+Account+Initiative+PlotAT+ENTER_PLOT for {}",
|
||||
playerGuid.ToString());
|
||||
}, Milliseconds(500));
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap deferred: Complete ? "
|
||||
"HouseInfo+Status+Perms+Auras+Account+Initiative+PlotAT+ENTER_PLOT for {}",
|
||||
playerGuid.ToString());
|
||||
}, Milliseconds(500));
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user