Housing: Data-driven interior rooms, decor persistence, and room type fix
- Rewrite SpawnRoomMeshObjects to iterate ALL components per room from DB2 data, each getting InitHousingRoomComponentData with proper geobox - Add faction-aware theme selection via GetFactionDefaultThemeID (Alliance=6, Horde=2) and FindRoomComponentOption per-component lookup - Fix GetDefaultVisualRoomEntry to deterministically pick lowest-ID room (Room 1 = Square Small) instead of non-deterministic unordered_map pick - Add runtime migration in LoadFromDB to replace wrong visual room entry (e.g., Octagon 9 → Square 1) on next login - Fix interior decor not visible: PLACE/MOVE/REMOVE handlers now support HouseInteriorMap via SpawnSingleInteriorDecor and UpdateDecorPosition - SpawnInteriorDecor runs on every interior entry (not gated by _roomsSpawned), with duplicate-spawn prevention - Fix room GUID using subType=2 (was 0 which returned ObjectGuid::Empty) - Fix DB2 OffsetRot degrees-to-radians conversion for component quaternions - Teleport player to visual room center on interior entry - Add MeshObject::InitHousingDecorData, InitHousingRoomData, InitHousingRoomComponentData, InitHousingFixtureData for entity fragments - Add SpawnRoomForPlot to HousingMap for exterior room entities with geobox - Add SQL migrations for base room and visual room auto-placement - Enhanced diagnostic logging for exterior decor spawn success/failure
This commit is contained in:
@@ -149,6 +149,36 @@ bool MeshObject::Create(Map* map, Position const& pos, QuaternionData const& rot
|
||||
return true;
|
||||
}
|
||||
|
||||
void MeshObject::InitHousingDecorData(ObjectGuid decorGuid, ObjectGuid houseGuid, uint8 flags,
|
||||
ObjectGuid roomEntityGuid /*= ObjectGuid::Empty*/)
|
||||
{
|
||||
if (m_housingDecorData.has_value())
|
||||
return;
|
||||
|
||||
// Sniff-verified: FHousingDecor_C entity fragment on MeshObject decor entities.
|
||||
// TargetGameObjectGUID is EMPTY (0x0) in ALL retail sniffs.
|
||||
// AttachParentGUID points to the room entity (Housing/18 base room) the decor is placed in.
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingDecorData, 0)
|
||||
.ModifyValue(&UF::HousingDecorData::DecorGUID), decorGuid);
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingDecorData, 0)
|
||||
.ModifyValue(&UF::HousingDecorData::AttachParentGUID), roomEntityGuid);
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingDecorData, 0)
|
||||
.ModifyValue(&UF::HousingDecorData::Flags), flags);
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingDecorData, 0)
|
||||
.ModifyValue(&UF::HousingDecorData::TargetGameObjectGUID), ObjectGuid::Empty);
|
||||
|
||||
auto persistedRef = m_values.ModifyValue(&Object::m_housingDecorData, 0)
|
||||
.ModifyValue(&UF::HousingDecorData::PersistedData, 0);
|
||||
SetUpdateFieldValue(persistedRef.ModifyValue(&UF::DecorStoragePersistedData::HouseGUID), houseGuid);
|
||||
SetUpdateFieldValue(persistedRef.ModifyValue(&UF::DecorStoragePersistedData::SourceType), uint8(0));
|
||||
|
||||
m_entityFragments.Add(WowCS::EntityFragment::FHousingDecor_C, IsInWorld(),
|
||||
WowCS::GetRawFragmentData(m_housingDecorData));
|
||||
|
||||
TC_LOG_DEBUG("housing", "MeshObject::InitHousingDecorData: guid={} decorGuid={} houseGuid={} flags={} roomEntity={}",
|
||||
GetGUID().ToString(), decorGuid.ToString(), houseGuid.ToString(), flags, roomEntityGuid.ToString());
|
||||
}
|
||||
|
||||
void MeshObject::InitHousingFixtureData(ObjectGuid houseGuid, int32 exteriorComponentID,
|
||||
int32 houseExteriorWmoDataID, uint8 exteriorComponentType /*= 9*/,
|
||||
uint8 houseSize /*= 2*/, int32 exteriorComponentHookID /*= -1*/)
|
||||
|
||||
@@ -60,6 +60,12 @@ public:
|
||||
int32 houseExteriorWmoDataID, uint8 exteriorComponentType = 9,
|
||||
uint8 houseSize = 2, int32 exteriorComponentHookID = -1);
|
||||
|
||||
// Housing decor (adds FHousingDecor_C entity fragment for placed decor items)
|
||||
// Sniff-verified: retail decor is ALWAYS MeshObject (never GO). TargetGameObjectGUID=empty.
|
||||
// roomEntityGuid: the Housing/18 room entity this decor is attached to.
|
||||
void InitHousingDecorData(ObjectGuid decorGuid, ObjectGuid houseGuid, uint8 flags,
|
||||
ObjectGuid roomEntityGuid = ObjectGuid::Empty);
|
||||
|
||||
// Housing room entity (adds FHousingRoom_C + Tag_HousingRoom entity fragments)
|
||||
// Creates a room data entity that the client uses to identify the plot's room type.
|
||||
void InitHousingRoomData(ObjectGuid houseGuid, int32 houseRoomID, int32 flags, int32 floorIndex);
|
||||
|
||||
@@ -558,13 +558,17 @@ void WorldSession::HandleHousingDecorPlace(WorldPackets::Housing::HousingDecorPl
|
||||
HousingResult result = housing->PlaceDecorWithGuid(housingDecorPlace.DecorGuid, decorEntryId,
|
||||
posX, posY, posZ, rotX, rotY, rotZ, rotW, housingDecorPlace.RoomGuid);
|
||||
|
||||
// Spawn decor GO on the map if placement succeeded
|
||||
// Spawn decor MeshObject on the map if placement succeeded.
|
||||
// Sniff-verified: ALL retail decor is MeshObject (never GO). The server sends an UPDATE_OBJECT
|
||||
// CREATE for the MeshObject + FHousingDecor_C immediately after placement.
|
||||
if (result == HOUSING_RESULT_SUCCESS)
|
||||
{
|
||||
if (Housing::PlacedDecor const* newDecor = housing->GetPlacedDecor(housingDecorPlace.DecorGuid))
|
||||
{
|
||||
if (HousingMap* housingMap = dynamic_cast<HousingMap*>(player->GetMap()))
|
||||
housingMap->SpawnDecorItem(housing->GetPlotIndex(), *newDecor, housing->GetHouseGuid());
|
||||
else if (HouseInteriorMap* interiorMap = dynamic_cast<HouseInteriorMap*>(player->GetMap()))
|
||||
interiorMap->SpawnSingleInteriorDecor(*newDecor, housing->GetHouseGuid());
|
||||
}
|
||||
|
||||
// Sniff: UPDATE_OBJECT (BNetAccount with ChangeType=3, HouseGUID=set) arrives BEFORE response.
|
||||
@@ -622,12 +626,12 @@ void WorldSession::HandleHousingDecorMove(WorldPackets::Housing::HousingDecorMov
|
||||
// Update decor GO position on the map
|
||||
if (result == HOUSING_RESULT_SUCCESS)
|
||||
{
|
||||
Position newPos(posX, posY, posZ);
|
||||
QuaternionData newRot(rotX, rotY, rotZ, rotW);
|
||||
if (HousingMap* housingMap = dynamic_cast<HousingMap*>(player->GetMap()))
|
||||
{
|
||||
Position newPos(posX, posY, posZ);
|
||||
QuaternionData newRot(rotX, rotY, rotZ, rotW);
|
||||
housingMap->UpdateDecorPosition(housing->GetPlotIndex(), housingDecorMove.DecorGuid, newPos, newRot);
|
||||
}
|
||||
else if (HouseInteriorMap* interiorMap = dynamic_cast<HouseInteriorMap*>(player->GetMap()))
|
||||
interiorMap->UpdateDecorPosition(housingDecorMove.DecorGuid, newPos, newRot);
|
||||
}
|
||||
|
||||
WorldPackets::Housing::HousingDecorMoveResponse response;
|
||||
@@ -665,8 +669,11 @@ void WorldSession::HandleHousingDecorRemove(WorldPackets::Housing::HousingDecorR
|
||||
// Despawn the decor GO from the map and update Account entity
|
||||
if (result == HOUSING_RESULT_SUCCESS)
|
||||
{
|
||||
// Support both exterior (HousingMap) and interior (HouseInteriorMap)
|
||||
if (HousingMap* housingMap = dynamic_cast<HousingMap*>(player->GetMap()))
|
||||
housingMap->DespawnDecorItem(plotIndex, decorGuid);
|
||||
else if (HouseInteriorMap* interiorMap = dynamic_cast<HouseInteriorMap*>(player->GetMap()))
|
||||
interiorMap->DespawnDecorItem(decorGuid);
|
||||
|
||||
// Sniff: RemoveDecor deletes the Account entry, but retail keeps it with HouseGUID=Empty
|
||||
// Re-add the entry with HouseGUID=Empty to return it to storage
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
*/
|
||||
|
||||
#include "HouseInteriorMap.h"
|
||||
#include "DB2Stores.h"
|
||||
#include "DBCEnums.h"
|
||||
#include "GameObjectData.h"
|
||||
#include "Housing.h"
|
||||
#include "HousingDefines.h"
|
||||
#include "HousingMgr.h"
|
||||
@@ -25,28 +27,30 @@
|
||||
#include "MeshObject.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "ObjectGridLoader.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "PhasingHandler.h"
|
||||
#include "Player.h"
|
||||
#include "World.h"
|
||||
#include "WorldSession.h"
|
||||
|
||||
// Interior map spawn origin (from NeighborhoodMap ID=7 DB2 data)
|
||||
// Interior map spawn origin (from NeighborhoodMap ID=7 DB2 data)
|
||||
static constexpr float INTERIOR_ORIGIN_X = -1000.0f;
|
||||
static constexpr float INTERIOR_ORIGIN_Y = -1000.0f;
|
||||
static constexpr float INTERIOR_ORIGIN_Z = 0.1f;
|
||||
|
||||
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();
|
||||
|
||||
TC_LOG_DEBUG("housing", "HouseInteriorMap: Created interior map {} instanceId {} for owner {}",
|
||||
id, instanceId, owner.ToString());
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::CTOR: Created interior map {} instanceId {} for owner {} "
|
||||
"(this={}, _roomsSpawned={})",
|
||||
id, instanceId, owner.ToString(), (void*)this, _roomsSpawned);
|
||||
}
|
||||
|
||||
void HouseInteriorMap::InitVisibilityDistance()
|
||||
@@ -74,7 +78,7 @@ Housing* HouseInteriorMap::GetOwnerHousing()
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void HouseInteriorMap::SpawnRoomMeshObjects(Housing* housing)
|
||||
void HouseInteriorMap::SpawnRoomMeshObjects(Housing* housing, int32 factionRestriction)
|
||||
{
|
||||
if (!housing)
|
||||
return;
|
||||
@@ -82,14 +86,19 @@ void HouseInteriorMap::SpawnRoomMeshObjects(Housing* housing)
|
||||
std::vector<Housing::Room const*> rooms = housing->GetRooms();
|
||||
if (rooms.empty())
|
||||
{
|
||||
TC_LOG_INFO("housing", "HouseInteriorMap::SpawnRoomMeshObjects: No rooms to spawn for owner {} "
|
||||
"(new house — rooms will appear when placed via editor)",
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnRoomMeshObjects: No rooms to spawn for owner {} "
|
||||
"(new house ? rooms will appear when placed via editor)",
|
||||
_owner.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
int32 factionThemeID = sHousingMgr.GetFactionDefaultThemeID(factionRestriction);
|
||||
uint32 totalMeshes = 0;
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnRoomMeshObjects: Starting spawn for {} rooms "
|
||||
"(owner={}, factionThemeID={}, houseGuid={})",
|
||||
uint32(rooms.size()), _owner.ToString(), factionThemeID, housing->GetHouseGuid().ToString());
|
||||
|
||||
for (Housing::Room const* room : rooms)
|
||||
{
|
||||
HouseRoomData const* roomData = sHousingMgr.GetHouseRoomData(room->RoomEntryId);
|
||||
@@ -101,25 +110,57 @@ void HouseInteriorMap::SpawnRoomMeshObjects(Housing* housing)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all mesh components for this room type
|
||||
std::vector<RoomComponentData> const* components = sHousingMgr.GetRoomComponents(roomData->RoomWmoDataID);
|
||||
// --- DB2 lookups ---
|
||||
|
||||
// 1. HouseRoom ? RoomWmoDataID
|
||||
int32 roomWmoDataID = roomData->RoomWmoDataID;
|
||||
|
||||
// 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;
|
||||
RoomWmoDataEntry const* wmoData = roomWmoDataID ? sRoomWmoDataStore.LookupEntry(roomWmoDataID) : nullptr;
|
||||
if (wmoData)
|
||||
{
|
||||
geoMinX = wmoData->BoundingBoxMinX;
|
||||
geoMinY = wmoData->BoundingBoxMinY;
|
||||
geoMinZ = wmoData->BoundingBoxMinZ;
|
||||
geoMaxX = wmoData->BoundingBoxMaxX;
|
||||
geoMaxY = wmoData->BoundingBoxMaxY;
|
||||
geoMaxZ = wmoData->BoundingBoxMaxZ;
|
||||
}
|
||||
|
||||
// 3. Get ALL components for this room
|
||||
std::vector<RoomComponentData> const* components = sHousingMgr.GetRoomComponents(roomWmoDataID);
|
||||
if (!components || components->empty())
|
||||
{
|
||||
TC_LOG_DEBUG("housing", "HouseInteriorMap::SpawnRoomMeshObjects: No components for "
|
||||
"roomWmoDataID {} (room entry {} '{}', slot {})",
|
||||
roomData->RoomWmoDataID, room->RoomEntryId, roomData->Name, room->SlotIndex);
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnRoomMeshObjects: No components for "
|
||||
"room '{}' (entry={}, roomWmoDataID={})",
|
||||
roomData->Name, room->RoomEntryId, roomWmoDataID);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate room world position from slot index.
|
||||
// Slot 0 (base room) = interior origin. Additional slots are offset based on
|
||||
// room bounding boxes and doorway connections. Sniff-verified: rooms are ~23yd
|
||||
// squares arranged on a 24yd grid (HOUSING_ROOM_GRID_SPACING).
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnRoomMeshObjects: Room '{}' entry={} slot={} "
|
||||
"roomWmoDataID={} has {} components, geobox=({:.2f},{:.2f},{:.2f})->({:.2f},{:.2f},{:.2f})",
|
||||
roomData->Name, room->RoomEntryId, room->SlotIndex,
|
||||
roomWmoDataID, uint32(components->size()),
|
||||
geoMinX, geoMinY, geoMinZ, geoMaxX, geoMaxY, geoMaxZ);
|
||||
|
||||
// Use the first component's FileDataID as the room entity mesh
|
||||
int32 roomEntityFileDataID = 6322976; // fallback
|
||||
for (auto const& c : *components)
|
||||
{
|
||||
if (c.ModelFileDataID > 0)
|
||||
{
|
||||
roomEntityFileDataID = c.ModelFileDataID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Calculate room world position ---
|
||||
|
||||
float roomX = INTERIOR_ORIGIN_X + static_cast<float>(room->SlotIndex) * HOUSING_ROOM_GRID_SPACING;
|
||||
float roomY = INTERIOR_ORIGIN_Y;
|
||||
float roomZ = INTERIOR_ORIGIN_Z;
|
||||
|
||||
// Room orientation: 0-3 for 90-degree increments
|
||||
float roomFacing = static_cast<float>(room->Orientation) * (M_PI / 2.0f);
|
||||
|
||||
Position roomPos(roomX, roomY, roomZ, roomFacing);
|
||||
@@ -129,113 +170,153 @@ void HouseInteriorMap::SpawnRoomMeshObjects(Housing* housing)
|
||||
roomRot.z = std::sin(roomFacing / 2.0f);
|
||||
roomRot.w = std::cos(roomFacing / 2.0f);
|
||||
|
||||
// Load the grid cell at the room position so objects can be placed there
|
||||
LoadGrid(roomX, roomY);
|
||||
|
||||
// Spawn the first component as the root piece, rest as children
|
||||
MeshObject* rootMesh = nullptr;
|
||||
// Lock the grid so it never unloads while the interior is active
|
||||
GridCoord roomGrid = Trinity::ComputeGridCoord(roomX, roomY);
|
||||
GridMarkNoUnload(roomGrid.x_coord, roomGrid.y_coord);
|
||||
|
||||
// --- Phase 1: Create ALL entities BEFORE adding to map ---
|
||||
// This matches the exterior SpawnRoomForPlot pattern:
|
||||
// create room entity + all components, link them via AddRoomMeshObject,
|
||||
// THEN add to map so the room entity's create packet includes all MeshObjects.
|
||||
|
||||
int32 roomFlags = roomData->IsBaseRoom() ? 1 : 0;
|
||||
int32 floorIndex = 0;
|
||||
|
||||
MeshObject* roomEntity = MeshObject::CreateMeshObject(this, roomPos, roomRot, 1.0f,
|
||||
roomEntityFileDataID, /*isWMO*/ true,
|
||||
ObjectGuid::Empty, /*attachFlags*/ 3, nullptr);
|
||||
|
||||
if (!roomEntity)
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnRoomMeshObjects: "
|
||||
"Failed to create room entity (roomEntry={}, slot={})",
|
||||
room->RoomEntryId, room->SlotIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
PhasingHandler::InitDbPhaseShift(roomEntity->GetPhaseShift(), PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
|
||||
roomEntity->InitHousingRoomData(housing->GetHouseGuid(), room->RoomEntryId, roomFlags, floorIndex);
|
||||
|
||||
// --- Phase 2: Create all component MeshObjects and link to room entity ---
|
||||
|
||||
std::vector<MeshObject*> componentMeshes;
|
||||
|
||||
for (RoomComponentData const& comp : *components)
|
||||
{
|
||||
if (comp.ModelFileDataID <= 0)
|
||||
continue;
|
||||
|
||||
// Component local-space position (relative to room center)
|
||||
Position compPos(comp.OffsetPos[0], comp.OffsetPos[1], comp.OffsetPos[2], 0.0f);
|
||||
// Look up faction-aware RoomComponentOption for this component
|
||||
RoomComponentOptionEntry const* optEntry = sHousingMgr.FindRoomComponentOption(comp.ID, factionThemeID);
|
||||
|
||||
// Component local-space rotation (Euler angles → quaternion)
|
||||
float rx = comp.OffsetRot[0];
|
||||
float ry = comp.OffsetRot[1];
|
||||
float rz = comp.OffsetRot[2];
|
||||
int32 compFileDataID = comp.ModelFileDataID;
|
||||
int32 roomComponentOptionID = 0;
|
||||
int32 houseThemeID = 0;
|
||||
int32 field24 = 0;
|
||||
int32 roomComponentTextureID = 0;
|
||||
|
||||
if (optEntry)
|
||||
{
|
||||
if (optEntry->ModelFileDataID > 0)
|
||||
compFileDataID = optEntry->ModelFileDataID;
|
||||
roomComponentOptionID = static_cast<int32>(optEntry->ID);
|
||||
houseThemeID = optEntry->HouseThemeID;
|
||||
field24 = static_cast<int32>(optEntry->SubType);
|
||||
}
|
||||
|
||||
// Component position/rotation: local to room entity
|
||||
Position compPos(comp.OffsetPos[0], comp.OffsetPos[1], comp.OffsetPos[2], 0.0f);
|
||||
QuaternionData compRot;
|
||||
// DB2 OffsetRot is in DEGREES ? convert to radians for quaternion math
|
||||
static constexpr float DEG_TO_RAD = static_cast<float>(M_PI / 180.0);
|
||||
float rx = comp.OffsetRot[0] * DEG_TO_RAD;
|
||||
float ry = comp.OffsetRot[1] * DEG_TO_RAD;
|
||||
float rz = comp.OffsetRot[2] * DEG_TO_RAD;
|
||||
compRot.x = std::sin(rx / 2.0f) * std::cos(ry / 2.0f) * std::cos(rz / 2.0f);
|
||||
compRot.y = std::cos(rx / 2.0f) * std::sin(ry / 2.0f) * std::cos(rz / 2.0f);
|
||||
compRot.z = std::cos(rx / 2.0f) * std::cos(ry / 2.0f) * std::sin(rz / 2.0f);
|
||||
compRot.w = std::cos(rx / 2.0f) * std::cos(ry / 2.0f) * std::cos(rz / 2.0f);
|
||||
|
||||
MeshObject* mesh = nullptr;
|
||||
MeshObject* componentMesh = MeshObject::CreateMeshObject(this, compPos, compRot, 1.0f,
|
||||
compFileDataID, /*isWMO*/ true,
|
||||
roomEntity->GetGUID(), /*attachFlags*/ 3, &roomPos);
|
||||
|
||||
if (!rootMesh)
|
||||
if (!componentMesh)
|
||||
{
|
||||
// First component = root piece at room world position
|
||||
mesh = MeshObject::CreateMeshObject(this, roomPos, roomRot, 1.0f,
|
||||
comp.ModelFileDataID, /*isWMO*/ true,
|
||||
ObjectGuid::Empty, /*attachFlags*/ 0, nullptr);
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnRoomMeshObjects: "
|
||||
"CreateMeshObject failed for component (compID={}, fileDataID={}, roomEntry={})",
|
||||
comp.ID, compFileDataID, room->RoomEntryId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mesh)
|
||||
{
|
||||
mesh->InitHousingFixtureData(housing->GetHouseGuid(),
|
||||
static_cast<int32>(comp.ID), roomData->RoomWmoDataID,
|
||||
/*exteriorComponentType*/ comp.Type, /*houseSize*/ 2, /*hookID*/ -1);
|
||||
PhasingHandler::InitDbPhaseShift(componentMesh->GetPhaseShift(), PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
|
||||
componentMesh->InitHousingRoomComponentData(roomEntity->GetGUID(),
|
||||
roomComponentOptionID, static_cast<int32>(comp.ID),
|
||||
comp.Type, field24,
|
||||
houseThemeID, roomComponentTextureID,
|
||||
/*roomComponentTypeParam*/ 0,
|
||||
geoMinX, geoMinY, geoMinZ,
|
||||
geoMaxX, geoMaxY, geoMaxZ);
|
||||
|
||||
PhasingHandler::InitDbPhaseShift(mesh->GetPhaseShift(),
|
||||
PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
|
||||
// Link component to room entity BEFORE either is on the map
|
||||
roomEntity->AddRoomMeshObject(componentMesh->GetGUID());
|
||||
componentMeshes.push_back(componentMesh);
|
||||
|
||||
if (AddToMap(mesh))
|
||||
{
|
||||
rootMesh = mesh;
|
||||
_roomMeshObjects[room->Guid].push_back(mesh->GetGUID());
|
||||
++totalMeshes;
|
||||
}
|
||||
else
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnRoomMeshObjects: "
|
||||
"AddToMap failed for root mesh (fileDataID={}, roomEntry={}, slot={})",
|
||||
comp.ModelFileDataID, room->RoomEntryId, room->SlotIndex);
|
||||
delete mesh;
|
||||
mesh = nullptr;
|
||||
}
|
||||
}
|
||||
TC_LOG_ERROR("housing", " Component: compID={} type={} fileDataID={} optionID={} themeID={} "
|
||||
"pos=({:.2f},{:.2f},{:.2f}) rot=({:.4f},{:.4f},{:.4f}) quat=({:.4f},{:.4f},{:.4f},{:.4f})",
|
||||
comp.ID, comp.Type, compFileDataID, roomComponentOptionID, houseThemeID,
|
||||
comp.OffsetPos[0], comp.OffsetPos[1], comp.OffsetPos[2],
|
||||
comp.OffsetRot[0], comp.OffsetRot[1], comp.OffsetRot[2],
|
||||
compRot.x, compRot.y, compRot.z, compRot.w);
|
||||
}
|
||||
|
||||
// --- Phase 3: Add room entity to map FIRST (create packet includes all MeshObjects) ---
|
||||
|
||||
if (!AddToMap(roomEntity))
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnRoomMeshObjects: "
|
||||
"AddToMap failed for room entity (roomEntry={}, slot={})",
|
||||
room->RoomEntryId, room->SlotIndex);
|
||||
delete roomEntity;
|
||||
for (MeshObject* comp : componentMeshes)
|
||||
delete comp;
|
||||
continue;
|
||||
}
|
||||
|
||||
_roomMeshObjects[room->Guid].push_back(roomEntity->GetGUID());
|
||||
++totalMeshes;
|
||||
|
||||
// --- Phase 4: Add all component MeshObjects to map ---
|
||||
|
||||
for (MeshObject* componentMesh : componentMeshes)
|
||||
{
|
||||
if (AddToMap(componentMesh))
|
||||
{
|
||||
_roomMeshObjects[room->Guid].push_back(componentMesh->GetGUID());
|
||||
++totalMeshes;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Child component = attached to root with local offset
|
||||
mesh = MeshObject::CreateMeshObject(this, compPos, compRot, 1.0f,
|
||||
comp.ModelFileDataID, /*isWMO*/ true,
|
||||
rootMesh->GetGUID(), /*attachFlags*/ 3, &roomPos);
|
||||
|
||||
if (mesh)
|
||||
{
|
||||
mesh->InitHousingFixtureData(housing->GetHouseGuid(),
|
||||
static_cast<int32>(comp.ID), roomData->RoomWmoDataID,
|
||||
/*exteriorComponentType*/ comp.Type, /*houseSize*/ 2,
|
||||
/*hookID*/ static_cast<int32>(comp.ID));
|
||||
|
||||
PhasingHandler::InitDbPhaseShift(mesh->GetPhaseShift(),
|
||||
PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
|
||||
|
||||
if (AddToMap(mesh))
|
||||
{
|
||||
_roomMeshObjects[room->Guid].push_back(mesh->GetGUID());
|
||||
++totalMeshes;
|
||||
}
|
||||
else
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnRoomMeshObjects: "
|
||||
"AddToMap failed for child mesh (fileDataID={}, roomEntry={}, slot={})",
|
||||
comp.ModelFileDataID, room->RoomEntryId, room->SlotIndex);
|
||||
delete mesh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!mesh)
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnRoomMeshObjects: "
|
||||
"CreateMeshObject failed (fileDataID={}, comp={}, type={}, roomEntry={})",
|
||||
comp.ModelFileDataID, comp.ID, comp.Type, room->RoomEntryId);
|
||||
"AddToMap failed for component (roomEntry={})",
|
||||
room->RoomEntryId);
|
||||
delete componentMesh;
|
||||
}
|
||||
}
|
||||
|
||||
TC_LOG_DEBUG("housing", "HouseInteriorMap::SpawnRoomMeshObjects: Room '{}' (entry={}, slot={}, wmoData={}) "
|
||||
"spawned {} components",
|
||||
roomData->Name, room->RoomEntryId, room->SlotIndex, roomData->RoomWmoDataID,
|
||||
_roomMeshObjects.count(room->Guid) ? uint32(_roomMeshObjects[room->Guid].size()) : 0u);
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnRoomMeshObjects: Room '{}' (entry={}, slot={}) "
|
||||
"spawned {} component MeshObjects (themeID={}) at ({:.1f},{:.1f},{:.1f})",
|
||||
roomData->Name, room->RoomEntryId, room->SlotIndex,
|
||||
uint32(componentMeshes.size()), factionThemeID,
|
||||
roomX, roomY, roomZ);
|
||||
}
|
||||
|
||||
TC_LOG_INFO("housing", "HouseInteriorMap::SpawnRoomMeshObjects: Spawned {} total MeshObjects for {} rooms "
|
||||
"(owner={}, map={}, instanceId={})",
|
||||
totalMeshes, uint32(rooms.size()), _owner.ToString(), GetId(), GetInstanceId());
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnRoomMeshObjects: Spawned {} total MeshObjects for {} rooms "
|
||||
"(owner={}, map={}, instanceId={}, faction={})",
|
||||
totalMeshes, uint32(rooms.size()), _owner.ToString(), GetId(), GetInstanceId(),
|
||||
factionRestriction == NEIGHBORHOOD_FACTION_ALLIANCE ? "Alliance" : "Horde");
|
||||
}
|
||||
|
||||
void HouseInteriorMap::DespawnAllRoomMeshObjects()
|
||||
@@ -261,8 +342,266 @@ void HouseInteriorMap::DespawnAllRoomMeshObjects()
|
||||
despawnCount, _owner.ToString());
|
||||
}
|
||||
|
||||
void HouseInteriorMap::SpawnInteriorDecor(Housing* housing)
|
||||
{
|
||||
if (!housing)
|
||||
return;
|
||||
|
||||
ObjectGuid houseGuid = housing->GetHouseGuid();
|
||||
uint32 spawnCount = 0;
|
||||
uint32 exteriorSkipped = 0;
|
||||
uint32 totalDecor = uint32(housing->GetPlacedDecorMap().size());
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnInteriorDecor: Starting ? totalDecor={} "
|
||||
"_roomMeshObjects entries={} owner={}",
|
||||
totalDecor, uint32(_roomMeshObjects.size()), _owner.ToString());
|
||||
|
||||
// Log all room mesh object entries for cross-reference
|
||||
for (auto const& [roomGuid, meshGuids] : _roomMeshObjects)
|
||||
{
|
||||
TC_LOG_ERROR("housing", " _roomMeshObjects[{}] = {} entries (first={})",
|
||||
roomGuid.ToString(), uint32(meshGuids.size()),
|
||||
meshGuids.empty() ? "EMPTY" : meshGuids[0].ToString());
|
||||
}
|
||||
|
||||
for (auto const& [decorGuid, decor] : housing->GetPlacedDecorMap())
|
||||
{
|
||||
HouseDecorData const* decorData = sHousingMgr.GetHouseDecorData(decor.DecorEntryId);
|
||||
if (!decorData)
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnInteriorDecor: No HouseDecorData for entry {} (decorGuid={})",
|
||||
decor.DecorEntryId, decor.Guid.ToString());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only spawn decor placed inside a room (interior). Exterior decor has empty RoomGuid.
|
||||
if (decor.RoomGuid.IsEmpty())
|
||||
{
|
||||
++exteriorSkipped;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip decor already spawned (e.g., placed during this session via SpawnSingleInteriorDecor)
|
||||
if (_decorGuidToObjGuid.count(decor.Guid))
|
||||
continue;
|
||||
|
||||
TC_LOG_ERROR("housing", " SpawnInteriorDecor: decor entry={} roomGuid={} pos=({:.1f},{:.1f},{:.1f})",
|
||||
decor.DecorEntryId, decor.RoomGuid.ToString(), decor.PosX, decor.PosY, decor.PosZ);
|
||||
|
||||
// Sniff-verified: ALL retail decor is MeshObject (never GO).
|
||||
// Determine FileDataID: prefer ModelFileDataID, fall back to GO template displayInfo.
|
||||
int32 fileDataID = decorData->ModelFileDataID;
|
||||
if (fileDataID <= 0 && decorData->GameObjectID > 0)
|
||||
{
|
||||
GameObjectTemplate const* goTemplate = sObjectMgr->GetGameObjectTemplate(
|
||||
static_cast<uint32>(decorData->GameObjectID));
|
||||
if (goTemplate)
|
||||
{
|
||||
GameObjectDisplayInfoEntry const* displayInfo =
|
||||
sGameObjectDisplayInfoStore.LookupEntry(goTemplate->displayId);
|
||||
if (displayInfo && displayInfo->FileDataID > 0)
|
||||
fileDataID = displayInfo->FileDataID;
|
||||
}
|
||||
}
|
||||
|
||||
if (fileDataID <= 0)
|
||||
{
|
||||
TC_LOG_DEBUG("housing", "HouseInteriorMap::SpawnInteriorDecor: Cannot derive FileDataID for decor entry {} "
|
||||
"(GameObjectID={}, ModelFileDataID={}), skipping",
|
||||
decor.DecorEntryId, decorData->GameObjectID, decorData->ModelFileDataID);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the room entity that this decor is attached to (by RoomGuid).
|
||||
// Sniff-verified: decor attaches to room entity with attachFlags=3.
|
||||
ObjectGuid roomEntityGuid = ObjectGuid::Empty;
|
||||
Position roomWorldPos;
|
||||
auto roomItr = _roomMeshObjects.find(decor.RoomGuid);
|
||||
if (roomItr != _roomMeshObjects.end() && !roomItr->second.empty())
|
||||
{
|
||||
// First entry in the room's mesh list is the room entity itself
|
||||
roomEntityGuid = roomItr->second[0];
|
||||
if (MeshObject* roomEntity = GetMeshObject(roomEntityGuid))
|
||||
roomWorldPos = roomEntity->GetPosition();
|
||||
}
|
||||
|
||||
float worldX = decor.PosX;
|
||||
float worldY = decor.PosY;
|
||||
float worldZ = decor.PosZ;
|
||||
LoadGrid(worldX, worldY);
|
||||
|
||||
QuaternionData rot(decor.RotationX, decor.RotationY, decor.RotationZ, decor.RotationW);
|
||||
|
||||
// Convert world position to room-local position
|
||||
float localX = worldX;
|
||||
float localY = worldY;
|
||||
float localZ = worldZ;
|
||||
if (!roomEntityGuid.IsEmpty())
|
||||
{
|
||||
localX = worldX - roomWorldPos.GetPositionX();
|
||||
localY = worldY - roomWorldPos.GetPositionY();
|
||||
localZ = worldZ - roomWorldPos.GetPositionZ();
|
||||
}
|
||||
|
||||
Position localPos(localX, localY, localZ);
|
||||
Position worldPos(worldX, worldY, worldZ);
|
||||
|
||||
MeshObject* mesh = MeshObject::CreateMeshObject(this, localPos, rot, 1.0f,
|
||||
fileDataID, /*isWMO*/ false,
|
||||
roomEntityGuid, /*attachFlags*/ roomEntityGuid.IsEmpty() ? uint8(0) : uint8(3),
|
||||
&worldPos);
|
||||
|
||||
if (!mesh)
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnInteriorDecor: Failed to create MeshObject for decor {} (fileDataID={})",
|
||||
decor.Guid.ToString(), fileDataID);
|
||||
continue;
|
||||
}
|
||||
|
||||
PhasingHandler::InitDbPhaseShift(mesh->GetPhaseShift(), PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
|
||||
mesh->InitHousingDecorData(decor.Guid, houseGuid, decor.Locked ? 1 : 0, roomEntityGuid);
|
||||
|
||||
if (AddToMap(mesh))
|
||||
{
|
||||
_decorGuidToObjGuid[decor.Guid] = mesh->GetGUID();
|
||||
++spawnCount;
|
||||
TC_LOG_INFO("housing", "HouseInteriorMap::SpawnInteriorDecor: Spawned decor MeshObject fileDataID={} "
|
||||
"at world({:.1f},{:.1f},{:.1f}) local({:.1f},{:.1f},{:.1f}) room={}",
|
||||
fileDataID, worldX, worldY, worldZ, localX, localY, localZ, roomEntityGuid.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
delete mesh;
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnInteriorDecor: AddToMap failed for MeshObject decor {}", decor.Guid.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::SpawnInteriorDecor: Spawned {} decor entities for owner {} "
|
||||
"(total={}, exteriorSkipped={})",
|
||||
spawnCount, _owner.ToString(), totalDecor, exteriorSkipped);
|
||||
}
|
||||
|
||||
void HouseInteriorMap::SpawnSingleInteriorDecor(Housing::PlacedDecor const& decor, ObjectGuid houseGuid)
|
||||
{
|
||||
if (decor.RoomGuid.IsEmpty())
|
||||
return; // exterior decor, not for interior map
|
||||
|
||||
// Already spawned?
|
||||
if (_decorGuidToObjGuid.count(decor.Guid))
|
||||
return;
|
||||
|
||||
HouseDecorData const* decorData = sHousingMgr.GetHouseDecorData(decor.DecorEntryId);
|
||||
if (!decorData)
|
||||
return;
|
||||
|
||||
int32 fileDataID = decorData->ModelFileDataID;
|
||||
if (fileDataID <= 0 && decorData->GameObjectID > 0)
|
||||
{
|
||||
GameObjectTemplate const* goTemplate = sObjectMgr->GetGameObjectTemplate(
|
||||
static_cast<uint32>(decorData->GameObjectID));
|
||||
if (goTemplate)
|
||||
{
|
||||
GameObjectDisplayInfoEntry const* displayInfo =
|
||||
sGameObjectDisplayInfoStore.LookupEntry(goTemplate->displayId);
|
||||
if (displayInfo && displayInfo->FileDataID > 0)
|
||||
fileDataID = displayInfo->FileDataID;
|
||||
}
|
||||
}
|
||||
|
||||
if (fileDataID <= 0)
|
||||
return;
|
||||
|
||||
// Find the room entity for this decor
|
||||
ObjectGuid roomEntityGuid = ObjectGuid::Empty;
|
||||
Position roomWorldPos;
|
||||
auto roomItr = _roomMeshObjects.find(decor.RoomGuid);
|
||||
if (roomItr != _roomMeshObjects.end() && !roomItr->second.empty())
|
||||
{
|
||||
roomEntityGuid = roomItr->second[0];
|
||||
if (MeshObject* roomEntity = GetMeshObject(roomEntityGuid))
|
||||
roomWorldPos = roomEntity->GetPosition();
|
||||
}
|
||||
|
||||
float worldX = decor.PosX, worldY = decor.PosY, worldZ = decor.PosZ;
|
||||
LoadGrid(worldX, worldY);
|
||||
|
||||
QuaternionData rot(decor.RotationX, decor.RotationY, decor.RotationZ, decor.RotationW);
|
||||
|
||||
float localX = worldX, localY = worldY, localZ = worldZ;
|
||||
if (!roomEntityGuid.IsEmpty())
|
||||
{
|
||||
localX = worldX - roomWorldPos.GetPositionX();
|
||||
localY = worldY - roomWorldPos.GetPositionY();
|
||||
localZ = worldZ - roomWorldPos.GetPositionZ();
|
||||
}
|
||||
|
||||
Position localPos(localX, localY, localZ);
|
||||
Position worldPos(worldX, worldY, worldZ);
|
||||
|
||||
MeshObject* mesh = MeshObject::CreateMeshObject(this, localPos, rot, 1.0f,
|
||||
fileDataID, /*isWMO*/ false,
|
||||
roomEntityGuid, /*attachFlags*/ roomEntityGuid.IsEmpty() ? uint8(0) : uint8(3),
|
||||
&worldPos);
|
||||
|
||||
if (!mesh)
|
||||
return;
|
||||
|
||||
PhasingHandler::InitDbPhaseShift(mesh->GetPhaseShift(), PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
|
||||
mesh->InitHousingDecorData(decor.Guid, houseGuid, decor.Locked ? 1 : 0, roomEntityGuid);
|
||||
|
||||
if (AddToMap(mesh))
|
||||
{
|
||||
_decorGuidToObjGuid[decor.Guid] = mesh->GetGUID();
|
||||
TC_LOG_INFO("housing", "HouseInteriorMap::SpawnSingleInteriorDecor: Spawned decor fileDataID={} "
|
||||
"at world({:.1f},{:.1f},{:.1f}) room={}",
|
||||
fileDataID, worldX, worldY, worldZ, roomEntityGuid.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
delete mesh;
|
||||
}
|
||||
}
|
||||
|
||||
void HouseInteriorMap::UpdateDecorPosition(ObjectGuid decorGuid, Position const& pos, QuaternionData const& /*rot*/)
|
||||
{
|
||||
auto itr = _decorGuidToObjGuid.find(decorGuid);
|
||||
if (itr == _decorGuidToObjGuid.end())
|
||||
return;
|
||||
|
||||
if (MeshObject* mesh = GetMeshObject(itr->second))
|
||||
{
|
||||
mesh->Relocate(pos);
|
||||
TC_LOG_DEBUG("housing", "HouseInteriorMap::UpdateDecorPosition: Moved decor {} to ({:.1f},{:.1f},{:.1f})",
|
||||
decorGuid.ToString(), pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ());
|
||||
}
|
||||
}
|
||||
|
||||
void HouseInteriorMap::DespawnDecorItem(ObjectGuid decorGuid)
|
||||
{
|
||||
auto itr = _decorGuidToObjGuid.find(decorGuid);
|
||||
if (itr == _decorGuidToObjGuid.end())
|
||||
{
|
||||
TC_LOG_DEBUG("housing", "HouseInteriorMap::DespawnDecorItem: No tracked visual for decorGuid={}", decorGuid.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
ObjectGuid objGuid = itr->second;
|
||||
if (MeshObject* mesh = GetMeshObject(objGuid))
|
||||
mesh->AddObjectToRemoveList();
|
||||
|
||||
_decorGuidToObjGuid.erase(itr);
|
||||
|
||||
TC_LOG_DEBUG("housing", "HouseInteriorMap::DespawnDecorItem: Despawned visual for decorGuid={}", decorGuid.ToString());
|
||||
}
|
||||
|
||||
bool HouseInteriorMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/)
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::AddPlayerToMap: ENTER player={} owner={} isOwner={} "
|
||||
"_roomsSpawned={} map={} instanceId={} this={}",
|
||||
player->GetGUID().ToString(), _owner.ToString(),
|
||||
player->GetGUID() == _owner, _roomsSpawned,
|
||||
GetId(), GetInstanceId(), (void*)this);
|
||||
|
||||
if (player->GetGUID() == _owner)
|
||||
_loadingPlayer = player;
|
||||
|
||||
@@ -271,19 +610,76 @@ bool HouseInteriorMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/
|
||||
if (player->GetGUID() == _owner)
|
||||
_loadingPlayer = nullptr;
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::AddPlayerToMap: Map::AddPlayerToMap returned {} for player={}",
|
||||
result, player->GetGUID().ToString());
|
||||
|
||||
if (result)
|
||||
{
|
||||
Housing* housing = player->GetHousing();
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::AddPlayerToMap: housing={} for player={}",
|
||||
housing ? "VALID" : "NULL", player->GetGUID().ToString());
|
||||
|
||||
if (housing)
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::AddPlayerToMap: houseGuid={} rooms={} decor={} "
|
||||
"houseGuid.IsEmpty={}",
|
||||
housing->GetHouseGuid().ToString(),
|
||||
uint32(housing->GetRooms().size()),
|
||||
uint32(housing->GetPlacedDecorMap().size()),
|
||||
housing->GetHouseGuid().IsEmpty());
|
||||
|
||||
housing->SetInInterior(true);
|
||||
|
||||
// Spawn room meshes on first entry
|
||||
if (!_roomsSpawned && player->GetGUID() == _owner)
|
||||
{
|
||||
SpawnRoomMeshObjects(housing);
|
||||
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={}",
|
||||
room->Guid.ToString(), room->RoomEntryId, room->SlotIndex,
|
||||
room->Orientation, room->Mirrored);
|
||||
}
|
||||
|
||||
int32 faction = (player->GetTeamId() == TEAM_ALLIANCE)
|
||||
? NEIGHBORHOOD_FACTION_ALLIANCE : NEIGHBORHOOD_FACTION_HORDE;
|
||||
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.
|
||||
SpawnInteriorDecor(housing);
|
||||
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::AddPlayerToMap: === SPAWN COMPLETE === "
|
||||
"roomMeshObjects entries={} decorGuidToObj entries={}",
|
||||
uint32(_roomMeshObjects.size()), uint32(_decorGuidToObjGuid.size()));
|
||||
|
||||
// Teleport player to the center of the first visual room.
|
||||
// Player spawns at INTERIOR_ORIGIN (slot 0) but the visual room may be at a different slot.
|
||||
for (Housing::Room const* room : housing->GetRooms())
|
||||
{
|
||||
HouseRoomData const* roomData2 = sHousingMgr.GetHouseRoomData(room->RoomEntryId);
|
||||
if (roomData2 && !roomData2->IsBaseRoom())
|
||||
{
|
||||
float targetX = INTERIOR_ORIGIN_X + static_cast<float>(room->SlotIndex) * HOUSING_ROOM_GRID_SPACING;
|
||||
float targetY = INTERIOR_ORIGIN_Y;
|
||||
float targetZ = INTERIOR_ORIGIN_Z;
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::AddPlayerToMap: Teleporting player to visual room "
|
||||
"entry={} slot={} at ({:.1f},{:.1f},{:.1f})",
|
||||
room->RoomEntryId, room->SlotIndex, targetX, targetY, targetZ);
|
||||
player->NearTeleportTo(targetX, targetY, targetZ, player->GetOrientation());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Send SMSG_HOUSE_INTERIOR_ENTER_HOUSE
|
||||
WorldPackets::Housing::HouseInteriorEnterHouse enterHouse;
|
||||
@@ -298,10 +694,20 @@ bool HouseInteriorMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/
|
||||
statusResponse.Status = 1; // Interior
|
||||
player->SendDirectMessage(statusResponse.Write());
|
||||
}
|
||||
else
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::AddPlayerToMap: NO HOUSING for player {} ? "
|
||||
"cannot spawn rooms/decor", player->GetGUID().ToString());
|
||||
}
|
||||
|
||||
TC_LOG_INFO("housing", "HouseInteriorMap: Player {} entered house interior (owner={}, map={}, instanceId={})",
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap: Player {} entered house interior (owner={}, map={}, instanceId={})",
|
||||
player->GetGUID().ToString(), _owner.ToString(), GetId(), GetInstanceId());
|
||||
}
|
||||
else
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::AddPlayerToMap: FAILED for player={}",
|
||||
player->GetGUID().ToString());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -312,8 +718,10 @@ void HouseInteriorMap::RemovePlayerFromMap(Player* player, bool remove)
|
||||
if (housing)
|
||||
housing->SetInInterior(false);
|
||||
|
||||
TC_LOG_DEBUG("housing", "HouseInteriorMap: Player {} leaving house interior (owner={}, map={}, instanceId={})",
|
||||
player->GetGUID().ToString(), _owner.ToString(), GetId(), GetInstanceId());
|
||||
TC_LOG_ERROR("housing", "HouseInteriorMap::RemovePlayerFromMap: Player {} leaving interior "
|
||||
"(owner={}, map={}, instanceId={}, _roomsSpawned={}, roomMeshEntries={}, decorEntries={}, this={})",
|
||||
player->GetGUID().ToString(), _owner.ToString(), GetId(), GetInstanceId(),
|
||||
_roomsSpawned, uint32(_roomMeshObjects.size()), uint32(_decorGuidToObjGuid.size()), (void*)this);
|
||||
|
||||
Map::RemovePlayerFromMap(player, remove);
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
#ifndef HouseInteriorMap_h__
|
||||
#define HouseInteriorMap_h__
|
||||
|
||||
#include "Housing.h"
|
||||
#include "Map.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include <vector>
|
||||
|
||||
class Housing;
|
||||
class Player;
|
||||
|
||||
/// Map instance for a player's house interior (MAP_HOUSE_INTERIOR = 7, MapID 2783).
|
||||
@@ -54,11 +54,24 @@ public:
|
||||
|
||||
/// Spawn all room meshes for the owner's house layout.
|
||||
/// Called once when the interior map is first populated.
|
||||
void SpawnRoomMeshObjects(Housing* housing);
|
||||
/// @param factionRestriction NEIGHBORHOOD_FACTION_ALLIANCE or NEIGHBORHOOD_FACTION_HORDE
|
||||
void SpawnRoomMeshObjects(Housing* housing, int32 factionRestriction);
|
||||
|
||||
/// Despawn all room meshes (e.g., when the interior is rebuilt).
|
||||
void DespawnAllRoomMeshObjects();
|
||||
|
||||
/// Spawn all placed decor for the owner's house on the interior map.
|
||||
void SpawnInteriorDecor(Housing* housing);
|
||||
|
||||
/// Spawn a single placed decor item immediately (called from PLACE handler).
|
||||
void SpawnSingleInteriorDecor(Housing::PlacedDecor const& decor, ObjectGuid houseGuid);
|
||||
|
||||
/// Update position/rotation of a single interior decor item.
|
||||
void UpdateDecorPosition(ObjectGuid decorGuid, Position const& pos, QuaternionData const& rot);
|
||||
|
||||
/// Despawn a single decor item by its Housing decor GUID.
|
||||
void DespawnDecorItem(ObjectGuid decorGuid);
|
||||
|
||||
private:
|
||||
ObjectGuid _owner;
|
||||
Player* _loadingPlayer; ///< @workaround Player not in ObjectAccessor during login
|
||||
@@ -68,6 +81,9 @@ 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)
|
||||
std::unordered_map<ObjectGuid, ObjectGuid> _decorGuidToObjGuid;
|
||||
};
|
||||
|
||||
#endif // HouseInteriorMap_h__
|
||||
|
||||
@@ -107,7 +107,7 @@ bool Housing::LoadFromDB(PreparedQueryResult housing, PreparedQueryResult decor,
|
||||
placed.DyeSlots[2] = fields[11].GetUInt32();
|
||||
uint64 roomDbId = fields[12].GetUInt64();
|
||||
if (roomDbId)
|
||||
placed.RoomGuid = ObjectGuid::Create<HighGuid::Housing>(0, 0, 0, roomDbId);
|
||||
placed.RoomGuid = ObjectGuid::Create<HighGuid::Housing>(/*subType*/ 2, 0, 0, roomDbId);
|
||||
placed.Locked = fields[13].GetUInt8() != 0;
|
||||
|
||||
if (decorDbId >= _decorDbIdGenerator)
|
||||
@@ -127,11 +127,18 @@ bool Housing::LoadFromDB(PreparedQueryResult housing, PreparedQueryResult decor,
|
||||
fields = rooms->Fetch();
|
||||
|
||||
uint64 roomDbId = fields[0].GetUInt64();
|
||||
ObjectGuid roomGuid = ObjectGuid::Create<HighGuid::Housing>(0, 0, 0, roomDbId);
|
||||
uint32 roomEntryId = fields[1].GetUInt32();
|
||||
|
||||
// Fix up roomDbId=0 from old saves that used ObjectGuid::Empty (subType=0 produced Empty GUID).
|
||||
// Without this, all rooms get the same GUID key and overwrite each other in _rooms.
|
||||
if (roomDbId == 0)
|
||||
roomDbId = _roomDbIdGenerator++;
|
||||
|
||||
ObjectGuid roomGuid = ObjectGuid::Create<HighGuid::Housing>(/*subType*/ 2, 0, 0, roomDbId);
|
||||
|
||||
Room& room = _rooms[roomGuid];
|
||||
room.Guid = roomGuid;
|
||||
room.RoomEntryId = fields[1].GetUInt32();
|
||||
room.RoomEntryId = roomEntryId;
|
||||
room.SlotIndex = fields[2].GetUInt32();
|
||||
room.Orientation = fields[3].GetUInt32();
|
||||
room.Mirrored = fields[4].GetBool();
|
||||
@@ -149,6 +156,83 @@ bool Housing::LoadFromDB(PreparedQueryResult housing, PreparedQueryResult decor,
|
||||
} while (rooms->NextRow());
|
||||
}
|
||||
|
||||
// Runtime fixup: ensure we have the correct default visual room.
|
||||
// Replace any visual room that has no RoomComponentOption entries (e.g., Octagon room 9
|
||||
// was picked by a previous non-deterministic GetDefaultVisualRoomEntry).
|
||||
{
|
||||
uint32 correctVisualRoom = sHousingMgr.GetDefaultVisualRoomEntry();
|
||||
bool hasVisualRoom = false;
|
||||
ObjectGuid wrongRoomGuid;
|
||||
|
||||
for (auto const& [guid, room] : _rooms)
|
||||
{
|
||||
if (sHousingMgr.IsBaseRoom(room.RoomEntryId))
|
||||
continue;
|
||||
|
||||
if (room.RoomEntryId == correctVisualRoom)
|
||||
{
|
||||
hasVisualRoom = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if this room's default visual room entry ? if not the correct one,
|
||||
// and it's the only non-base room, replace it
|
||||
if (wrongRoomGuid.IsEmpty())
|
||||
wrongRoomGuid = guid;
|
||||
else
|
||||
hasVisualRoom = true; // Multiple visual rooms ? don't mess with them
|
||||
}
|
||||
|
||||
// Replace wrong room with correct one
|
||||
if (!hasVisualRoom && !wrongRoomGuid.IsEmpty() && correctVisualRoom)
|
||||
{
|
||||
auto wrongItr = _rooms.find(wrongRoomGuid);
|
||||
if (wrongItr != _rooms.end())
|
||||
{
|
||||
uint32 oldEntry = wrongItr->second.RoomEntryId;
|
||||
uint32 oldSlot = wrongItr->second.SlotIndex;
|
||||
|
||||
// Erase from map ? SaveToDB will persist the change on next save
|
||||
_rooms.erase(wrongItr);
|
||||
|
||||
// Place new room in same slot
|
||||
HousingResult placeResult = PlaceRoom(correctVisualRoom, oldSlot, 0, false);
|
||||
TC_LOG_ERROR("housing", "Housing::LoadFromDB: Replaced visual room {} with {} in slot {} "
|
||||
"for house {} (migration fixup, result={})",
|
||||
oldEntry, correctVisualRoom, oldSlot, _houseGuid.ToString(), placeResult);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasVisualRoom && wrongRoomGuid.IsEmpty() && !_rooms.empty())
|
||||
{
|
||||
uint32 visualRoom = sHousingMgr.GetDefaultVisualRoomEntry();
|
||||
if (visualRoom)
|
||||
{
|
||||
// Find the next free slot (slot 0 is base room)
|
||||
uint32 nextSlot = 1;
|
||||
for (auto const& [guid, room] : _rooms)
|
||||
{
|
||||
if (room.SlotIndex >= nextSlot)
|
||||
nextSlot = room.SlotIndex + 1;
|
||||
}
|
||||
|
||||
HousingResult placeResult = PlaceRoom(visualRoom, nextSlot, /*orientation*/ 0, /*mirrored*/ false);
|
||||
if (placeResult == HOUSING_RESULT_SUCCESS)
|
||||
{
|
||||
TC_LOG_ERROR("housing", "Housing::LoadFromDB: Auto-placed visual room entry {} in slot {} "
|
||||
"for existing house {} (migration fixup)",
|
||||
visualRoom, nextSlot, _houseGuid.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
TC_LOG_ERROR("housing", "Housing::LoadFromDB: PlaceRoom FAILED for visual room entry {} "
|
||||
"slot {} ? result={} ? interior will be empty (house {})",
|
||||
visualRoom, nextSlot, placeResult, _houseGuid.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load fixtures
|
||||
// 0 1
|
||||
// SELECT fixturePointId, optionId
|
||||
@@ -443,6 +527,35 @@ HousingResult Housing::Create(ObjectGuid neighborhoodGuid, uint8 plotIndex)
|
||||
_owner->GetName(), bnetAccountId, plotIndex, _neighborhoodGuid.ToString(), _houseGuid.ToString());
|
||||
|
||||
SyncUpdateFields();
|
||||
|
||||
// Every new house starts with a base room (HouseRoom.db2 entry 18, BASE_ROOM flag).
|
||||
// Without this, entering the interior spawns nothing and decor placement has no Geobox.
|
||||
PlaceRoom(/*roomEntryId*/ 18, /*slotIndex*/ 0, /*orientation*/ 0, /*mirrored*/ false);
|
||||
|
||||
// Also place a default visual room so the interior renders walls/floor/ceiling.
|
||||
// Base room (18) only provides the geobox boundary ? visual geometry needs a separate room.
|
||||
uint32 visualRoom = sHousingMgr.GetDefaultVisualRoomEntry();
|
||||
if (visualRoom)
|
||||
{
|
||||
HousingResult visualResult = PlaceRoom(visualRoom, /*slotIndex*/ 1, /*orientation*/ 0, /*mirrored*/ false);
|
||||
if (visualResult == HOUSING_RESULT_SUCCESS)
|
||||
{
|
||||
TC_LOG_ERROR("housing", "Housing::Create: Auto-placed visual room entry {} in slot 1 for player {}",
|
||||
visualRoom, _owner->GetName());
|
||||
}
|
||||
else
|
||||
{
|
||||
TC_LOG_ERROR("housing", "Housing::Create: PlaceRoom FAILED for visual room entry {} ? result={} ? "
|
||||
"interior will be empty for player {}",
|
||||
visualRoom, visualResult, _owner->GetName());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TC_LOG_ERROR("housing", "Housing::Create: No visual room entry found ? interior will be empty for player {}",
|
||||
_owner->GetName());
|
||||
}
|
||||
|
||||
return HOUSING_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -800,8 +913,9 @@ HousingResult Housing::MoveDecor(ObjectGuid decorGuid, float x, float y, float z
|
||||
if (itr == _placedDecor.end())
|
||||
return HOUSING_RESULT_DECOR_NOT_FOUND;
|
||||
|
||||
if (itr->second.Locked)
|
||||
return HOUSING_RESULT_LOCKED_BY_OTHER_PLAYER;
|
||||
// Sniff-verified: Lock?Move is valid (the locker is the one moving).
|
||||
// Lock only prevents OTHER editors from modifying ? not the owner.
|
||||
// TODO: When multi-editor support is added, track LockedByGuid and check here.
|
||||
|
||||
PlacedDecor& decor = itr->second;
|
||||
decor.PosX = x;
|
||||
@@ -841,8 +955,9 @@ HousingResult Housing::RemoveDecor(ObjectGuid decorGuid)
|
||||
if (itr == _placedDecor.end())
|
||||
return HOUSING_RESULT_DECOR_NOT_FOUND;
|
||||
|
||||
if (itr->second.Locked)
|
||||
return HOUSING_RESULT_LOCKED_BY_OTHER_PLAYER;
|
||||
// Sniff-verified: Lock?Remove is a valid retail flow (packet #27117 LOCK then
|
||||
// #27139 REMOVE with Result=0). The house owner can always remove their own decor.
|
||||
// Lock only prevents OTHER editors from modifying ? not the owner.
|
||||
|
||||
// Refund WeightCost budget (route to correct budget based on room)
|
||||
uint32 decorEntryId = itr->second.DecorEntryId;
|
||||
@@ -1015,21 +1130,14 @@ HousingResult Housing::PlaceRoom(uint32 roomEntryId, uint32 slotIndex, uint32 or
|
||||
return HOUSING_RESULT_PLOT_NOT_FOUND;
|
||||
}
|
||||
|
||||
// Room must have at least one doorway to connect to the house layout
|
||||
if (!roomData->IsBaseRoom())
|
||||
{
|
||||
uint32 doorCount = sHousingMgr.GetRoomDoorCount(roomEntryId);
|
||||
if (doorCount == 0)
|
||||
{
|
||||
TC_LOG_ERROR("housing", "Housing::PlaceRoom: Room entry {} has no doorways, cannot connect to house layout",
|
||||
roomEntryId);
|
||||
return HOUSING_RESULT_ROOM_UPDATE_FAILED;
|
||||
}
|
||||
}
|
||||
// NOTE: Doorway components (Type 7) are OPTIONAL in the DB2.
|
||||
// Standard rooms (1-15) have 0 doorway components ? they use wall segments (Type 1) instead.
|
||||
// Only prefab/custom rooms (113+) have explicit doorway components.
|
||||
// Retail places rooms without doorways, so we don't enforce this check.
|
||||
|
||||
// Generate a new room guid
|
||||
uint64 newDbId = GenerateRoomDbId();
|
||||
ObjectGuid roomGuid = ObjectGuid::Create<HighGuid::Housing>(0, 0, 0, newDbId);
|
||||
ObjectGuid roomGuid = ObjectGuid::Create<HighGuid::Housing>(/*subType*/ 2, 0, 0, newDbId);
|
||||
|
||||
Room& room = _rooms[roomGuid];
|
||||
room.Guid = roomGuid;
|
||||
|
||||
@@ -650,15 +650,19 @@ static constexpr uint32 SPELL_HOUSING_PLOT_PRESENCE = 469226;
|
||||
static constexpr uint32 SPELL_HOUSING_PLOT_ENTER_2 = 1266699;
|
||||
|
||||
// WorldState IDs ? continuous counters sent throughout the entire housing session.
|
||||
// Sniff-verified: increment by ~1333 every ~300ms, starting at map login with INIT_WORLD_STATES.
|
||||
// Initial values from sniff: 13436=417396536, 13437=166923928, 13438=473155274
|
||||
// Sniff-verified: 5 counters total, sent as individual SMSG_UPDATE_WORLD_STATE packets.
|
||||
// Counters 1-3 increment by ~1333 every ~300ms.
|
||||
// Counters 4-5 increment by ~7233 every ~300ms.
|
||||
static constexpr uint32 WORLDSTATE_HOUSING_COUNTER_1 = 13436;
|
||||
static constexpr uint32 WORLDSTATE_HOUSING_COUNTER_2 = 13437;
|
||||
static constexpr uint32 WORLDSTATE_HOUSING_COUNTER_3 = 13438;
|
||||
static constexpr uint32 WORLDSTATE_HOUSING_COUNTER_4 = 16035;
|
||||
static constexpr uint32 WORLDSTATE_HOUSING_COUNTER_5 = 16711;
|
||||
|
||||
// Interval and increment for housing WorldState counter updates
|
||||
static constexpr uint32 HOUSING_WORLDSTATE_INTERVAL_MS = 300;
|
||||
static constexpr uint32 HOUSING_WORLDSTATE_INCREMENT = 1333;
|
||||
static constexpr uint32 HOUSING_WORLDSTATE_INCREMENT_2 = 7233;
|
||||
|
||||
// Cosmetic phases removed when a player enters their own housing plot and
|
||||
// restored when they leave. Sniff-verified: 16 phases with ~10s delay.
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include "ObjectAccessor.h"
|
||||
#include "ObjectGridLoader.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "PhasingHandler.h"
|
||||
#include "Player.h"
|
||||
#include "RealmList.h"
|
||||
@@ -70,14 +71,19 @@ namespace
|
||||
return result;
|
||||
}
|
||||
|
||||
// Recurring event that sends housing WorldState counters (13436/13437/13438) every ~300ms.
|
||||
// Sniff-verified: these are continuous counters incrementing by ~1333 each tick throughout
|
||||
// the entire housing map session, NOT edit-mode-specific.
|
||||
// Recurring event that sends housing WorldState counters every ~300ms.
|
||||
// Sniff-verified: 5 continuous counters throughout the entire housing map session.
|
||||
// Counters 1-3 (13436/13437/13438) increment by ~1333 each tick.
|
||||
// Counters 4-5 (16035/16711) increment by ~7233 each tick.
|
||||
class HousingWorldStateCounterEvent : public BasicEvent
|
||||
{
|
||||
public:
|
||||
HousingWorldStateCounterEvent(ObjectGuid playerGuid, uint32 counter1, uint32 counter2, uint32 counter3)
|
||||
: _playerGuid(playerGuid), _counter1(counter1), _counter2(counter2), _counter3(counter3) {
|
||||
HousingWorldStateCounterEvent(ObjectGuid playerGuid,
|
||||
uint32 counter1, uint32 counter2, uint32 counter3,
|
||||
uint32 counter4, uint32 counter5)
|
||||
: _playerGuid(playerGuid)
|
||||
, _counter1(counter1), _counter2(counter2), _counter3(counter3)
|
||||
, _counter4(counter4), _counter5(counter5) {
|
||||
}
|
||||
|
||||
bool Execute(uint64 /*e_time*/, uint32 /*p_time*/) override
|
||||
@@ -86,19 +92,24 @@ namespace
|
||||
if (!player || !player->IsInWorld())
|
||||
return true; // delete event ? player gone
|
||||
|
||||
// Send the three counter WorldState updates
|
||||
// Send all five counter WorldState updates
|
||||
player->SendUpdateWorldState(WORLDSTATE_HOUSING_COUNTER_1, _counter1);
|
||||
player->SendUpdateWorldState(WORLDSTATE_HOUSING_COUNTER_2, _counter2);
|
||||
player->SendUpdateWorldState(WORLDSTATE_HOUSING_COUNTER_3, _counter3);
|
||||
player->SendUpdateWorldState(WORLDSTATE_HOUSING_COUNTER_4, _counter4);
|
||||
player->SendUpdateWorldState(WORLDSTATE_HOUSING_COUNTER_5, _counter5);
|
||||
|
||||
// Increment for next tick
|
||||
// Increment for next tick (different rates per sniff)
|
||||
_counter1 += HOUSING_WORLDSTATE_INCREMENT;
|
||||
_counter2 += HOUSING_WORLDSTATE_INCREMENT;
|
||||
_counter3 += HOUSING_WORLDSTATE_INCREMENT;
|
||||
_counter4 += HOUSING_WORLDSTATE_INCREMENT_2;
|
||||
_counter5 += HOUSING_WORLDSTATE_INCREMENT_2;
|
||||
|
||||
// Re-schedule self for next tick
|
||||
player->m_Events.AddEventAtOffset(
|
||||
new HousingWorldStateCounterEvent(_playerGuid, _counter1, _counter2, _counter3),
|
||||
new HousingWorldStateCounterEvent(_playerGuid,
|
||||
_counter1, _counter2, _counter3, _counter4, _counter5),
|
||||
Milliseconds(HOUSING_WORLDSTATE_INTERVAL_MS));
|
||||
|
||||
return true; // delete this instance (new one scheduled)
|
||||
@@ -109,6 +120,8 @@ namespace
|
||||
uint32 _counter1;
|
||||
uint32 _counter2;
|
||||
uint32 _counter3;
|
||||
uint32 _counter4;
|
||||
uint32 _counter5;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -300,34 +313,19 @@ void HousingMap::SpawnPlotGameObjects()
|
||||
}
|
||||
}
|
||||
|
||||
// Set per-plot WorldState values from DB2 so the client can render plot status on the map
|
||||
// WorldState values use HousingPlotOwnerType: 0=None, 1=Stranger, 2=Friend, 3=Self
|
||||
// The map-global value is set to STRANGER (1) for occupied plots as the default.
|
||||
// When a specific player enters the map, SendPerPlayerPlotWorldStates() sends
|
||||
// personalized corrections (SELF/FRIEND) based on the player's relationship to each plot owner.
|
||||
// IMPORTANT: Must use Map::SetWorldStateValue() (not sWorldStateMgr->SetValue()) because:
|
||||
// 1. The same WorldState IDs are shared across maps (2735/2736) - each instance needs its own values
|
||||
// 2. These IDs have no template in world_state SQL table, so sWorldStateMgr stores them
|
||||
// realm-wide which is wrong for instanced housing maps
|
||||
// 3. Map-scoped values are included in SMSG_INIT_WORLD_STATES via FillInitialWorldStates()
|
||||
uint32 wsSetCount = 0;
|
||||
// Sniff-verified: SMSG_INIT_WORLD_STATES for housing maps (2735/2736) has Field Count = 0.
|
||||
// Per-plot ownership worldstates are sent as individual SMSG_UPDATE_WORLD_STATE packets
|
||||
// in SendPerPlayerPlotWorldStates() after the player joins ? NOT in the init packet.
|
||||
// Do NOT use Map::SetWorldStateValue() here as it pollutes INIT_WORLD_STATES.
|
||||
uint32 wsCount = 0;
|
||||
for (NeighborhoodPlotData const* plot : plots)
|
||||
{
|
||||
if (plot->WorldState != 0)
|
||||
{
|
||||
Neighborhood::PlotInfo const* plotInfo = _neighborhood->GetPlotInfo(static_cast<uint8>(plot->PlotIndex));
|
||||
bool isOccupied = plotInfo && !plotInfo->OwnerGuid.IsEmpty();
|
||||
|
||||
// Default: NONE (0) for unoccupied, STRANGER (1) for occupied
|
||||
// Per-player corrections (SELF/FRIEND) are sent in SendPerPlayerPlotWorldStates()
|
||||
int32 wsValue = isOccupied ? HOUSING_PLOT_OWNER_STRANGER : HOUSING_PLOT_OWNER_NONE;
|
||||
SetWorldStateValue(plot->WorldState, wsValue, false);
|
||||
++wsSetCount;
|
||||
}
|
||||
++wsCount;
|
||||
}
|
||||
|
||||
TC_LOG_INFO("housing", "HousingMap::SpawnPlotGameObjects: Spawned {} GOs, set {} WorldStates for {} plots in neighborhood '{}' (noEntry={})",
|
||||
goCount, wsSetCount, uint32(plots.size()), _neighborhood->GetName(), noEntryCount);
|
||||
TC_LOG_INFO("housing", "HousingMap::SpawnPlotGameObjects: Spawned {} GOs, {} plots have WorldState IDs for {} plots in neighborhood '{}' (noEntry={})",
|
||||
goCount, wsCount, uint32(plots.size()), _neighborhood->GetName(), noEntryCount);
|
||||
|
||||
// Spawn house structure GOs for owned plots
|
||||
uint32 houseCount = 0;
|
||||
@@ -470,10 +468,9 @@ void HousingMap::SetPlotOwnershipState(uint8 plotIndex, bool owned)
|
||||
plotAt->UpdateHousingPlotOwnerData(ObjectGuid::Empty, ObjectGuid::Empty, ObjectGuid::Empty);
|
||||
}
|
||||
|
||||
// Update the per-plot WorldState using HousingPlotOwnerType values
|
||||
// Default map-global: STRANGER (1) for occupied, NONE (0) for unoccupied
|
||||
// Then send per-player corrections to all online players on this map
|
||||
// Must use Map::SetWorldStateValue (see SpawnPlotGameObjects comment for rationale)
|
||||
// Send per-plot WorldState update to all players on the map.
|
||||
// Sniff-verified: housing maps use individual SMSG_UPDATE_WORLD_STATE packets
|
||||
// (NOT map-scoped SetWorldStateValue, which pollutes INIT_WORLD_STATES).
|
||||
uint32 neighborhoodMapId = _neighborhood->GetNeighborhoodMapID();
|
||||
std::vector<NeighborhoodPlotData const*> plots = sHousingMgr.GetPlotsForMap(neighborhoodMapId);
|
||||
for (NeighborhoodPlotData const* plotData : plots)
|
||||
@@ -482,11 +479,7 @@ void HousingMap::SetPlotOwnershipState(uint8 plotIndex, bool owned)
|
||||
{
|
||||
if (plotData->WorldState != 0)
|
||||
{
|
||||
int32 wsValue = owned ? HOUSING_PLOT_OWNER_STRANGER : HOUSING_PLOT_OWNER_NONE;
|
||||
SetWorldStateValue(plotData->WorldState, wsValue, false);
|
||||
|
||||
// Send personalized corrections to each player on the map
|
||||
// (the map-global value only shows STRANGER; each player needs SELF/FRIEND override)
|
||||
// Send personalized value to each player on the map
|
||||
for (MapReference const& ref : GetPlayers())
|
||||
{
|
||||
if (Player* mapPlayer = ref.GetSource())
|
||||
@@ -645,8 +638,10 @@ bool HousingMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/)
|
||||
|
||||
Position pos = houseGo->GetPosition();
|
||||
QuaternionData rot = houseGo->GetLocalRotation();
|
||||
int32 faction = _neighborhood ? _neighborhood->GetFactionRestriction()
|
||||
: NEIGHBORHOOD_FACTION_ALLIANCE;
|
||||
SpawnFullHouseMeshObjects(plotIdx, pos, rot,
|
||||
housing->GetHouseGuid(), 141, 9);
|
||||
housing->GetHouseGuid(), 141, 9, faction);
|
||||
|
||||
// Also spawn room entity + Geobox if not already present
|
||||
if (_roomEntities.find(plotIdx) == _roomEntities.end())
|
||||
@@ -780,17 +775,18 @@ bool HousingMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/)
|
||||
}
|
||||
|
||||
// Start the periodic housing WorldState counter timer.
|
||||
// Sniff-verified: counters 13436/13437/13438 increment by ~1333 every ~300ms
|
||||
// throughout the entire housing map session. Seed with getMSTime()-based values
|
||||
// (retail uses opaque server-tick values; the exact seed doesn't matter as long
|
||||
// as the increment pattern is correct).
|
||||
// Sniff-verified: 5 counters sent as individual SMSG_UPDATE_WORLD_STATE packets.
|
||||
// Counters 1-3 increment by ~1333, counters 4-5 by ~7233, every ~300ms.
|
||||
// Seed with getMSTime()-based values (retail uses opaque server-tick values;
|
||||
// the exact seed doesn't matter as long as the increment pattern is correct).
|
||||
{
|
||||
uint32 baseSeed = getMSTime();
|
||||
player->m_Events.AddEventAtOffset(
|
||||
new HousingWorldStateCounterEvent(player->GetGUID(),
|
||||
baseSeed, baseSeed / 3, baseSeed + 55758738),
|
||||
baseSeed, baseSeed / 3, baseSeed + 55758738,
|
||||
baseSeed * 2, baseSeed + 123456789),
|
||||
Milliseconds(HOUSING_WORLDSTATE_INTERVAL_MS));
|
||||
TC_LOG_DEBUG("housing", "HousingMap::AddPlayerToMap: Started WorldState counter timer for player {}",
|
||||
TC_LOG_DEBUG("housing", "HousingMap::AddPlayerToMap: Started WorldState counter timer (5 counters) for player {}",
|
||||
player->GetGUID().ToString());
|
||||
}
|
||||
|
||||
@@ -1075,25 +1071,32 @@ void HousingMap::SendPerPlayerPlotWorldStates(Player* player)
|
||||
uint32 neighborhoodMapId = _neighborhood->GetNeighborhoodMapID();
|
||||
std::vector<NeighborhoodPlotData const*> plots = sHousingMgr.GetPlotsForMap(neighborhoodMapId);
|
||||
|
||||
uint32 correctionCount = 0;
|
||||
uint32 sentCount = 0;
|
||||
uint32 noWsCount = 0;
|
||||
uint32 selfCount = 0;
|
||||
uint32 friendCount = 0;
|
||||
for (NeighborhoodPlotData const* plot : plots)
|
||||
{
|
||||
if (plot->WorldState == 0)
|
||||
{
|
||||
++noWsCount;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8 plotIdx = static_cast<uint8>(plot->PlotIndex);
|
||||
HousingPlotOwnerType ownerType = GetPlotOwnerTypeForPlayer(player, plotIdx);
|
||||
player->SendUpdateWorldState(plot->WorldState, static_cast<uint32>(ownerType), false);
|
||||
++sentCount;
|
||||
|
||||
if (ownerType != HOUSING_PLOT_OWNER_NONE && ownerType != HOUSING_PLOT_OWNER_STRANGER)
|
||||
++correctionCount;
|
||||
if (ownerType == HOUSING_PLOT_OWNER_SELF)
|
||||
++selfCount;
|
||||
else if (ownerType == HOUSING_PLOT_OWNER_FRIEND)
|
||||
++friendCount;
|
||||
}
|
||||
|
||||
if (correctionCount > 0)
|
||||
{
|
||||
TC_LOG_DEBUG("housing", "HousingMap::SendPerPlayerPlotWorldStates: Sent {} personalized WorldState corrections to player {} (SELF/FRIEND)",
|
||||
correctionCount, player->GetGUID().ToString());
|
||||
}
|
||||
TC_LOG_INFO("housing", "HousingMap::SendPerPlayerPlotWorldStates: Player {} ? sent {} WorldState updates "
|
||||
"(self={}, friend={}, {} plots had no WorldState ID in DB2)",
|
||||
player->GetGUID().ToString(), sentCount, selfCount, friendCount, noWsCount);
|
||||
}
|
||||
|
||||
void HousingMap::AddPlayerHousing(ObjectGuid playerGuid, Housing* housing)
|
||||
@@ -1208,13 +1211,15 @@ GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* custo
|
||||
plotIndex, x, y, z, facing, rot.x, rot.y, rot.z, rot.w,
|
||||
plotInfo != nullptr, plotInfo && !plotInfo->HouseGuid.IsEmpty());
|
||||
|
||||
// Spawn all house MeshObjects (sniff-verified: 10 structural pieces for Stucco Small alliance house)
|
||||
// Spawn all house MeshObjects (sniff-verified: 10 structural pieces for alliance, different for horde)
|
||||
// Pieces have a parent-child hierarchy: base piece (0) and door piece (1) are roots,
|
||||
// other pieces attach to them with local-space positions/rotations.
|
||||
if (plotInfo && !plotInfo->HouseGuid.IsEmpty())
|
||||
{
|
||||
int32 faction = _neighborhood ? _neighborhood->GetFactionRestriction()
|
||||
: NEIGHBORHOOD_FACTION_ALLIANCE;
|
||||
SpawnFullHouseMeshObjects(plotIndex, pos, rot, plotInfo->HouseGuid,
|
||||
exteriorComponentID, houseExteriorWmoDataID);
|
||||
exteriorComponentID, houseExteriorWmoDataID, faction);
|
||||
|
||||
// Spawn room entity + component mesh with Geobox for this plot.
|
||||
// The client uses the MeshObject Geobox to validate decor placement bounds.
|
||||
@@ -1325,7 +1330,6 @@ void HousingMap::SpawnRoomForPlot(uint8 plotIndex, Position const& housePos,
|
||||
}
|
||||
|
||||
// 4. RoomComponentOption ? theme-specific cosmetic data (varies by faction/house style)
|
||||
// Iterate DB2 to find any option matching RoomComponentID=196.
|
||||
// Alliance sniff: optionID=874, themeID=6, field24=1, textureID=3
|
||||
// Horde sniff: optionID=420, themeID=2, field24=2, textureID=40
|
||||
// These are cosmetic only (don't affect Geobox/bounds check).
|
||||
@@ -1333,15 +1337,17 @@ void HousingMap::SpawnRoomForPlot(uint8 plotIndex, Position const& housePos,
|
||||
int32 houseThemeID = 0;
|
||||
int32 roomComponentTextureID = 0;
|
||||
int32 field24 = 0;
|
||||
for (RoomComponentOptionEntry const* optEntry : sRoomComponentOptionStore)
|
||||
|
||||
// Use faction-aware theme lookup
|
||||
int32 factionThemeID = _neighborhood
|
||||
? sHousingMgr.GetFactionDefaultThemeID(_neighborhood->GetFactionRestriction())
|
||||
: 6;
|
||||
RoomComponentOptionEntry const* optEntry = sHousingMgr.FindRoomComponentOption(ROOM_COMPONENT_ID, factionThemeID);
|
||||
if (optEntry)
|
||||
{
|
||||
if (optEntry && optEntry->RoomComponentID == ROOM_COMPONENT_ID)
|
||||
{
|
||||
roomComponentOptionID = static_cast<int32>(optEntry->ID);
|
||||
houseThemeID = optEntry->HouseThemeID;
|
||||
field24 = static_cast<int32>(optEntry->SubType); // Sniff pattern: SubType matches Field_24
|
||||
break;
|
||||
}
|
||||
roomComponentOptionID = static_cast<int32>(optEntry->ID);
|
||||
houseThemeID = optEntry->HouseThemeID;
|
||||
field24 = static_cast<int32>(optEntry->SubType);
|
||||
}
|
||||
// If no DB2 entry found, use sniff-verified alliance defaults
|
||||
if (roomComponentOptionID == 0)
|
||||
@@ -1503,9 +1509,19 @@ MeshObject* HousingMap::SpawnHouseMeshObject(uint8 plotIndex, int32 fileDataID,
|
||||
|
||||
void HousingMap::SpawnFullHouseMeshObjects(uint8 plotIndex, Position const& housePos,
|
||||
QuaternionData const& houseRot, ObjectGuid houseGuid,
|
||||
int32 exteriorComponentID, int32 houseExteriorWmoDataID)
|
||||
int32 exteriorComponentID, int32 houseExteriorWmoDataID,
|
||||
int32 factionRestriction /*= NEIGHBORHOOD_FACTION_ALLIANCE*/)
|
||||
{
|
||||
// Sniff-verified: Alliance starter house (Stucco Small) consists of 10 structural MeshObjects.
|
||||
// Branch on faction ? alliance and horde have different exterior mesh hierarchies
|
||||
if (factionRestriction == NEIGHBORHOOD_FACTION_HORDE)
|
||||
{
|
||||
SpawnHordeHouseMeshObjects(plotIndex, housePos, houseRot, houseGuid,
|
||||
exteriorComponentID, houseExteriorWmoDataID);
|
||||
return;
|
||||
}
|
||||
|
||||
// === ALLIANCE EXTERIOR (Stucco Small) ===
|
||||
// Sniff-verified: Alliance starter house consists of 10 structural MeshObjects.
|
||||
// Two root pieces (base + door) positioned at the house location, and 8 child pieces
|
||||
// attached to roots with local-space offsets.
|
||||
//
|
||||
@@ -1616,12 +1632,111 @@ void HousingMap::SpawnFullHouseMeshObjects(uint8 plotIndex, Position const& hous
|
||||
if (meshItr != _meshObjects.end())
|
||||
meshCount = static_cast<uint32>(meshItr->second.size());
|
||||
|
||||
TC_LOG_DEBUG("housing", "HousingMap::SpawnFullHouseMeshObjects: Spawned {} MeshObjects for plot {} in neighborhood '{}' "
|
||||
TC_LOG_DEBUG("housing", "HousingMap::SpawnFullHouseMeshObjects: Spawned {} alliance MeshObjects for plot {} in neighborhood '{}' "
|
||||
"(base={} door={})",
|
||||
meshCount, plotIndex, _neighborhood ? _neighborhood->GetName() : "?",
|
||||
basePiece ? "OK" : "FAIL", doorPiece ? "OK" : "FAIL");
|
||||
}
|
||||
|
||||
void HousingMap::SpawnHordeHouseMeshObjects(uint8 plotIndex, Position const& housePos,
|
||||
QuaternionData const& houseRot, ObjectGuid houseGuid,
|
||||
int32 /*exteriorComponentID*/, int32 /*houseExteriorWmoDataID*/)
|
||||
{
|
||||
// === HORDE EXTERIOR ===
|
||||
// Sniff-verified: Horde starter house with HouseExteriorWmoDataID=87.
|
||||
// Two root pieces at the house position, children with local-space offsets.
|
||||
//
|
||||
// Parent-child hierarchy:
|
||||
// Root 0 (main structure, 7118906) - ExteriorComponentID 3811, type 10
|
||||
// ??? Child: Door/entrance (7118912), hookID 17245, extCompID 976
|
||||
// ??? Child: Wall element (7460531), hookID -1, extCompID 2476
|
||||
// ??? Child: Wall variant (7118901), hookID -1, extCompID 1011
|
||||
// ??? Child: Roof piece A (7462686), hookID 17294, extCompID 2445
|
||||
// ??? Child: Structure detail (7118918), hookID 17286, extCompID 980
|
||||
// ??? Child: Roof piece B (7462686), hookID 17285, extCompID 2445
|
||||
// Root 1 (base, 6648685) - ExteriorComponentID 1003, type 9
|
||||
|
||||
int32 hordeWmoDataID = HORDE_HOUSE_EXTERIOR_WMO_DATA_ID; // 87
|
||||
|
||||
// Root piece 0: Main structure
|
||||
MeshObject* rootPiece = SpawnHouseMeshObject(plotIndex, 7118906, /*isWMO*/ true,
|
||||
housePos, houseRot, 1.0f,
|
||||
houseGuid, 3811, hordeWmoDataID,
|
||||
/*exteriorComponentType*/ 10, /*houseSize*/ 2, /*hookID*/ -1,
|
||||
ObjectGuid::Empty, /*attachFlags*/ 0);
|
||||
|
||||
// Root piece 1: Base structure
|
||||
MeshObject* basePiece = SpawnHouseMeshObject(plotIndex, 6648685, /*isWMO*/ true,
|
||||
housePos, houseRot, 1.0f,
|
||||
houseGuid, 1003, hordeWmoDataID,
|
||||
/*exteriorComponentType*/ 9, /*houseSize*/ 2, /*hookID*/ -1,
|
||||
ObjectGuid::Empty, /*attachFlags*/ 0);
|
||||
|
||||
// Children of root piece 0
|
||||
if (rootPiece)
|
||||
{
|
||||
ObjectGuid rootGuid = rootPiece->GetGUID();
|
||||
|
||||
// Door/entrance
|
||||
SpawnHouseMeshObject(plotIndex, 7118912, /*isWMO*/ true,
|
||||
Position(14.2722f, -8.6194f, 0.0f, 0.0f),
|
||||
QuaternionData(0.0f, 0.0f, -0.2873478f, 0.9578263f), 1.0f,
|
||||
houseGuid, 976, hordeWmoDataID,
|
||||
/*exteriorComponentType*/ 11, /*houseSize*/ 2, /*hookID*/ 17245,
|
||||
rootGuid, /*attachFlags*/ 3, &housePos);
|
||||
|
||||
// Wall element
|
||||
SpawnHouseMeshObject(plotIndex, 7460531, /*isWMO*/ true,
|
||||
Position(0.0f, 0.0f, 0.0f, 0.0f),
|
||||
QuaternionData(0.0f, 0.0f, 0.0f, 1.0f), 1.0f,
|
||||
houseGuid, 2476, hordeWmoDataID,
|
||||
/*exteriorComponentType*/ 12, /*houseSize*/ 2, /*hookID*/ -1,
|
||||
rootGuid, /*attachFlags*/ 3, &housePos);
|
||||
|
||||
// Wall variant
|
||||
SpawnHouseMeshObject(plotIndex, 7118901, /*isWMO*/ true,
|
||||
Position(0.0f, 0.0f, 0.0f, 0.0f),
|
||||
QuaternionData(0.0f, 0.0f, 0.0f, 1.0f), 1.0f,
|
||||
houseGuid, 1011, hordeWmoDataID,
|
||||
/*exteriorComponentType*/ 12, /*houseSize*/ 2, /*hookID*/ -1,
|
||||
rootGuid, /*attachFlags*/ 3, &housePos);
|
||||
|
||||
// Roof piece A (right side)
|
||||
SpawnHouseMeshObject(plotIndex, 7462686, /*isWMO*/ true,
|
||||
Position(6.2889f, -4.4556f, 0.0833f, 0.0f),
|
||||
QuaternionData(0.0f, 0.0f, 0.95782566f, 0.28735f), 1.0f,
|
||||
houseGuid, 2445, hordeWmoDataID,
|
||||
/*exteriorComponentType*/ 13, /*houseSize*/ 2, /*hookID*/ 17294,
|
||||
rootGuid, /*attachFlags*/ 3, &housePos);
|
||||
|
||||
// Structure detail
|
||||
SpawnHouseMeshObject(plotIndex, 7118918, /*isWMO*/ true,
|
||||
Position(-0.1389f, 8.6806f, 5.4139f, 0.0f),
|
||||
QuaternionData(0.0f, 0.0f, 0.7071066f, 0.70710695f), 1.0f,
|
||||
houseGuid, 980, hordeWmoDataID,
|
||||
/*exteriorComponentType*/ 12, /*houseSize*/ 2, /*hookID*/ 17286,
|
||||
rootGuid, /*attachFlags*/ 3, &housePos);
|
||||
|
||||
// Roof piece B (left side)
|
||||
SpawnHouseMeshObject(plotIndex, 7462686, /*isWMO*/ true,
|
||||
Position(-7.0611f, -3.7361f, 0.0833f, 0.0f),
|
||||
QuaternionData(0.0f, 0.0f, 0.2873478f, 0.9578263f), 1.0f,
|
||||
houseGuid, 2445, hordeWmoDataID,
|
||||
/*exteriorComponentType*/ 13, /*houseSize*/ 2, /*hookID*/ 17285,
|
||||
rootGuid, /*attachFlags*/ 3, &housePos);
|
||||
}
|
||||
|
||||
uint32 meshCount = 0;
|
||||
auto meshItr = _meshObjects.find(plotIndex);
|
||||
if (meshItr != _meshObjects.end())
|
||||
meshCount = static_cast<uint32>(meshItr->second.size());
|
||||
|
||||
TC_LOG_DEBUG("housing", "HousingMap::SpawnHordeHouseMeshObjects: Spawned {} MeshObjects for plot {} in neighborhood '{}' "
|
||||
"(root={} base={})",
|
||||
meshCount, plotIndex, _neighborhood ? _neighborhood->GetName() : "?",
|
||||
rootPiece ? "OK" : "FAIL", basePiece ? "OK" : "FAIL");
|
||||
}
|
||||
|
||||
void HousingMap::DespawnAllMeshObjectsForPlot(uint8 plotIndex)
|
||||
{
|
||||
auto itr = _meshObjects.find(plotIndex);
|
||||
@@ -1676,10 +1791,10 @@ int8 HousingMap::GetPlotIndexForHouseGO(ObjectGuid goGuid) const
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Decor GO Management
|
||||
// Decor Management (all decor is MeshObject ? sniff-verified)
|
||||
// ============================================================
|
||||
|
||||
GameObject* HousingMap::SpawnDecorItem(uint8 plotIndex, Housing::PlacedDecor const& decor, ObjectGuid houseGuid)
|
||||
MeshObject* HousingMap::SpawnDecorItem(uint8 plotIndex, Housing::PlacedDecor const& decor, ObjectGuid houseGuid)
|
||||
{
|
||||
HouseDecorData const* decorData = sHousingMgr.GetHouseDecorData(decor.DecorEntryId);
|
||||
if (!decorData)
|
||||
@@ -1689,75 +1804,125 @@ GameObject* HousingMap::SpawnDecorItem(uint8 plotIndex, Housing::PlacedDecor con
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32 goEntry = decorData->GameObjectID > 0 ? static_cast<uint32>(decorData->GameObjectID) : 0;
|
||||
if (!goEntry)
|
||||
// Sniff-verified: ALL retail placed decor is MeshObject (never GO).
|
||||
// FHousingDecor_C on a GameObject crashes the client (same issue as FHousingFixture_C on GOs).
|
||||
// Determine FileDataID: prefer ModelFileDataID, fall back to GO template displayInfo.
|
||||
int32 fileDataID = decorData->ModelFileDataID;
|
||||
if (fileDataID <= 0 && decorData->GameObjectID > 0)
|
||||
{
|
||||
TC_LOG_DEBUG("housing", "HousingMap::SpawnDecorItem: Decor entry {} has GameObjectID=0 (CLIENT_MODEL type), skipping GO spawn",
|
||||
GameObjectTemplate const* goTemplate = sObjectMgr->GetGameObjectTemplate(
|
||||
static_cast<uint32>(decorData->GameObjectID));
|
||||
if (goTemplate)
|
||||
{
|
||||
GameObjectDisplayInfoEntry const* displayInfo =
|
||||
sGameObjectDisplayInfoStore.LookupEntry(goTemplate->displayId);
|
||||
if (displayInfo && displayInfo->FileDataID > 0)
|
||||
fileDataID = displayInfo->FileDataID;
|
||||
}
|
||||
|
||||
if (fileDataID <= 0)
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HousingMap::SpawnDecorItem: Cannot derive FileDataID for decor entry {} "
|
||||
"(GameObjectID={}, ModelFileDataID={}), skipping",
|
||||
decor.DecorEntryId, decorData->GameObjectID, decorData->ModelFileDataID);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TC_LOG_DEBUG("housing", "HousingMap::SpawnDecorItem: Derived FileDataID={} from GameObjectID={} displayId for entry {}",
|
||||
fileDataID, decorData->GameObjectID, decor.DecorEntryId);
|
||||
}
|
||||
else if (fileDataID <= 0)
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HousingMap::SpawnDecorItem: Decor entry {} has no ModelFileDataID and no GameObjectID, skipping",
|
||||
decor.DecorEntryId);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
float x = decor.PosX;
|
||||
float y = decor.PosY;
|
||||
float z = decor.PosZ;
|
||||
// Sniff-verified: Decor MeshObjects are attached to the plot's base room entity
|
||||
// (Housing/18) with attachFlags=3. Position is room-local space.
|
||||
// Get the room entity for this plot (spawned by SpawnRoomForPlot).
|
||||
ObjectGuid roomEntityGuid = ObjectGuid::Empty;
|
||||
Position roomWorldPos;
|
||||
auto roomItr = _roomEntities.find(plotIndex);
|
||||
if (roomItr != _roomEntities.end())
|
||||
{
|
||||
roomEntityGuid = roomItr->second;
|
||||
if (MeshObject* roomEntity = GetMeshObject(roomEntityGuid))
|
||||
roomWorldPos = roomEntity->GetPosition();
|
||||
}
|
||||
|
||||
LoadGrid(x, y);
|
||||
float worldX = decor.PosX;
|
||||
float worldY = decor.PosY;
|
||||
float worldZ = decor.PosZ;
|
||||
LoadGrid(worldX, worldY);
|
||||
|
||||
// Decor rotation is stored as quaternion
|
||||
QuaternionData rot(decor.RotationX, decor.RotationY, decor.RotationZ, decor.RotationW);
|
||||
|
||||
Position pos(x, y, z);
|
||||
GameObject* go = GameObject::CreateGameObject(goEntry, this, pos, rot, 255, GO_STATE_ACTIVE);
|
||||
if (!go)
|
||||
// Convert world position to room-local position if we have a room entity
|
||||
float localX = worldX;
|
||||
float localY = worldY;
|
||||
float localZ = worldZ;
|
||||
if (!roomEntityGuid.IsEmpty())
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HousingMap::SpawnDecorItem: Failed to create decor GO entry {} at ({}, {}, {}) for decor {}",
|
||||
goEntry, x, y, z, decor.Guid.ToString());
|
||||
localX = worldX - roomWorldPos.GetPositionX();
|
||||
localY = worldY - roomWorldPos.GetPositionY();
|
||||
localZ = worldZ - roomWorldPos.GetPositionZ();
|
||||
}
|
||||
|
||||
Position localPos(localX, localY, localZ);
|
||||
Position worldPos(worldX, worldY, worldZ);
|
||||
|
||||
MeshObject* mesh = MeshObject::CreateMeshObject(this, localPos, rot, 1.0f,
|
||||
fileDataID, /*isWMO*/ false,
|
||||
roomEntityGuid, /*attachFlags*/ roomEntityGuid.IsEmpty() ? uint8(0) : uint8(3),
|
||||
&worldPos);
|
||||
|
||||
if (!mesh)
|
||||
{
|
||||
TC_LOG_ERROR("housing", "HousingMap::SpawnDecorItem: Failed to create decor MeshObject fileDataID={} for decor {}",
|
||||
fileDataID, decor.Guid.ToString());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
go->SetFlag(GO_FLAG_NODESPAWN);
|
||||
PhasingHandler::InitDbPhaseShift(mesh->GetPhaseShift(), PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
|
||||
mesh->InitHousingDecorData(decor.Guid, houseGuid, decor.Locked ? 1 : 0, roomEntityGuid);
|
||||
|
||||
// Universally visible (same rationale as cornerstones ? no DB spawn, no phase_area)
|
||||
PhasingHandler::InitDbPhaseShift(go->GetPhaseShift(), PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
|
||||
|
||||
// Populate the FHousingDecor_C entity fragment
|
||||
go->InitHousingDecorData(decor.Guid, houseGuid, decor.Locked ? 1 : 0);
|
||||
|
||||
if (!AddToMap(go))
|
||||
if (!AddToMap(mesh))
|
||||
{
|
||||
delete go;
|
||||
TC_LOG_ERROR("housing", "HousingMap::SpawnDecorItem: Failed to add decor GO to map for decor {}", decor.Guid.ToString());
|
||||
TC_LOG_ERROR("housing", "HousingMap::SpawnDecorItem: Failed to add decor MeshObject to map for decor {}", decor.Guid.ToString());
|
||||
delete mesh;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Track the decor GO
|
||||
_decorGameObjects[plotIndex].push_back(go->GetGUID());
|
||||
_decorGuidToGoGuid[decor.Guid] = go->GetGUID();
|
||||
_decorGameObjects[plotIndex].push_back(mesh->GetGUID());
|
||||
_decorGuidToGoGuid[decor.Guid] = mesh->GetGUID();
|
||||
_decorGuidToPlotIndex[decor.Guid] = plotIndex;
|
||||
|
||||
TC_LOG_DEBUG("housing", "HousingMap::SpawnDecorItem: Spawned decor GO entry={} goGuid={} decorGuid={} at ({:.1f}, {:.1f}, {:.1f}) for plot {}",
|
||||
goEntry, go->GetGUID().ToString(), decor.Guid.ToString(), x, y, z, plotIndex);
|
||||
|
||||
return go;
|
||||
TC_LOG_INFO("housing", "HousingMap::SpawnDecorItem: Spawned decor MeshObject fileDataID={} meshGuid={} decorGuid={} "
|
||||
"at world({:.1f},{:.1f},{:.1f}) local({:.1f},{:.1f},{:.1f}) room={} plot={}",
|
||||
fileDataID, mesh->GetGUID().ToString(), decor.Guid.ToString(),
|
||||
worldX, worldY, worldZ, localX, localY, localZ,
|
||||
roomEntityGuid.ToString(), plotIndex);
|
||||
return mesh;
|
||||
}
|
||||
|
||||
void HousingMap::DespawnDecorItem(uint8 plotIndex, ObjectGuid decorGuid)
|
||||
{
|
||||
auto goItr = _decorGuidToGoGuid.find(decorGuid);
|
||||
if (goItr == _decorGuidToGoGuid.end())
|
||||
auto itr = _decorGuidToGoGuid.find(decorGuid);
|
||||
if (itr == _decorGuidToGoGuid.end())
|
||||
return;
|
||||
|
||||
ObjectGuid goGuid = goItr->second;
|
||||
if (GameObject* go = GetGameObject(goGuid))
|
||||
go->AddObjectToRemoveList();
|
||||
ObjectGuid objGuid = itr->second;
|
||||
if (MeshObject* mesh = GetMeshObject(objGuid))
|
||||
mesh->AddObjectToRemoveList();
|
||||
|
||||
// Remove from tracking
|
||||
auto& plotDecor = _decorGameObjects[plotIndex];
|
||||
plotDecor.erase(std::remove(plotDecor.begin(), plotDecor.end(), goGuid), plotDecor.end());
|
||||
_decorGuidToGoGuid.erase(goItr);
|
||||
plotDecor.erase(std::remove(plotDecor.begin(), plotDecor.end(), objGuid), plotDecor.end());
|
||||
_decorGuidToGoGuid.erase(itr);
|
||||
_decorGuidToPlotIndex.erase(decorGuid);
|
||||
|
||||
TC_LOG_DEBUG("housing", "HousingMap::DespawnDecorItem: Despawned decor GO for decorGuid={} plot={}", decorGuid.ToString(), plotIndex);
|
||||
TC_LOG_DEBUG("housing", "HousingMap::DespawnDecorItem: Despawned decor MeshObject for decorGuid={} plot={}", decorGuid.ToString(), plotIndex);
|
||||
}
|
||||
|
||||
void HousingMap::DespawnAllDecorForPlot(uint8 plotIndex)
|
||||
@@ -1766,10 +1931,10 @@ void HousingMap::DespawnAllDecorForPlot(uint8 plotIndex)
|
||||
if (itr == _decorGameObjects.end())
|
||||
return;
|
||||
|
||||
for (ObjectGuid const& goGuid : itr->second)
|
||||
for (ObjectGuid const& objGuid : itr->second)
|
||||
{
|
||||
if (GameObject* go = GetGameObject(goGuid))
|
||||
go->AddObjectToRemoveList();
|
||||
if (MeshObject* mesh = GetMeshObject(objGuid))
|
||||
mesh->AddObjectToRemoveList();
|
||||
}
|
||||
|
||||
// Clean up all tracking for this plot's decor
|
||||
@@ -1788,7 +1953,7 @@ void HousingMap::DespawnAllDecorForPlot(uint8 plotIndex)
|
||||
itr->second.clear();
|
||||
_decorSpawnedPlots.erase(plotIndex);
|
||||
|
||||
TC_LOG_DEBUG("housing", "HousingMap::DespawnAllDecorForPlot: Despawned all decor GOs for plot {}", plotIndex);
|
||||
TC_LOG_DEBUG("housing", "HousingMap::DespawnAllDecorForPlot: Despawned all decor MeshObjects for plot {}", plotIndex);
|
||||
}
|
||||
|
||||
void HousingMap::SpawnAllDecorForPlot(uint8 plotIndex, Housing const* housing)
|
||||
@@ -1801,30 +1966,41 @@ void HousingMap::SpawnAllDecorForPlot(uint8 plotIndex, Housing const* housing)
|
||||
|
||||
ObjectGuid houseGuid = housing->GetHouseGuid();
|
||||
uint32 spawnCount = 0;
|
||||
uint32 exteriorCount = 0;
|
||||
uint32 failCount = 0;
|
||||
for (auto const& [decorGuid, decor] : housing->GetPlacedDecorMap())
|
||||
{
|
||||
if (SpawnDecorItem(plotIndex, decor, houseGuid))
|
||||
// Skip interior decor ? those are spawned by HouseInteriorMap::SpawnInteriorDecor
|
||||
if (!decor.RoomGuid.IsEmpty())
|
||||
continue;
|
||||
|
||||
++exteriorCount;
|
||||
MeshObject* mesh = SpawnDecorItem(plotIndex, decor, houseGuid);
|
||||
if (mesh)
|
||||
++spawnCount;
|
||||
else
|
||||
++failCount;
|
||||
}
|
||||
|
||||
_decorSpawnedPlots.insert(plotIndex);
|
||||
|
||||
TC_LOG_INFO("housing", "HousingMap::SpawnAllDecorForPlot: Spawned {} decor GOs for plot {} in neighborhood '{}'",
|
||||
spawnCount, plotIndex, _neighborhood ? _neighborhood->GetName() : "?");
|
||||
TC_LOG_ERROR("housing", "HousingMap::SpawnAllDecorForPlot: Spawned {}/{} exterior decor for plot {} "
|
||||
"(failed={}, neighborhood='{}')",
|
||||
spawnCount, exteriorCount, plotIndex, failCount,
|
||||
_neighborhood ? _neighborhood->GetName() : "?");
|
||||
}
|
||||
|
||||
void HousingMap::UpdateDecorPosition(uint8 plotIndex, ObjectGuid decorGuid, Position const& pos, QuaternionData const& rot)
|
||||
void HousingMap::UpdateDecorPosition(uint8 plotIndex, ObjectGuid decorGuid, Position const& pos, QuaternionData const& /*rot*/)
|
||||
{
|
||||
auto goItr = _decorGuidToGoGuid.find(decorGuid);
|
||||
if (goItr == _decorGuidToGoGuid.end())
|
||||
auto itr = _decorGuidToGoGuid.find(decorGuid);
|
||||
if (itr == _decorGuidToGoGuid.end())
|
||||
return;
|
||||
|
||||
if (GameObject* go = GetGameObject(goItr->second))
|
||||
// All decor is MeshObject now
|
||||
if (MeshObject* mesh = GetMeshObject(itr->second))
|
||||
{
|
||||
go->Relocate(pos);
|
||||
go->SetLocalRotation(rot.x, rot.y, rot.z, rot.w);
|
||||
|
||||
TC_LOG_DEBUG("housing", "HousingMap::UpdateDecorPosition: Moved decor GO {} to ({:.1f}, {:.1f}, {:.1f}) for plot {}",
|
||||
mesh->Relocate(pos);
|
||||
TC_LOG_DEBUG("housing", "HousingMap::UpdateDecorPosition: Moved decor MeshObject {} to ({:.1f}, {:.1f}, {:.1f}) for plot {}",
|
||||
decorGuid.ToString(), pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), plotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,10 @@ public:
|
||||
ObjectGuid attachParent = ObjectGuid::Empty, uint8 attachFlags = 0,
|
||||
Position const* worldPos = nullptr);
|
||||
void SpawnFullHouseMeshObjects(uint8 plotIndex, Position const& housePos,
|
||||
QuaternionData const& houseRot, ObjectGuid houseGuid,
|
||||
int32 exteriorComponentID, int32 houseExteriorWmoDataID,
|
||||
int32 factionRestriction = NEIGHBORHOOD_FACTION_ALLIANCE);
|
||||
void SpawnHordeHouseMeshObjects(uint8 plotIndex, Position const& housePos,
|
||||
QuaternionData const& houseRot, ObjectGuid houseGuid,
|
||||
int32 exteriorComponentID, int32 houseExteriorWmoDataID);
|
||||
void DespawnAllMeshObjectsForPlot(uint8 plotIndex);
|
||||
@@ -83,8 +87,8 @@ public:
|
||||
QuaternionData const& houseRot, ObjectGuid houseGuid);
|
||||
void DespawnRoomForPlot(uint8 plotIndex);
|
||||
|
||||
// Decor GO management
|
||||
GameObject* SpawnDecorItem(uint8 plotIndex, Housing::PlacedDecor const& decor, ObjectGuid houseGuid);
|
||||
// Decor management (all decor is MeshObject ? sniff-verified, never GO)
|
||||
MeshObject* SpawnDecorItem(uint8 plotIndex, Housing::PlacedDecor const& decor, ObjectGuid houseGuid);
|
||||
void DespawnDecorItem(uint8 plotIndex, ObjectGuid decorGuid);
|
||||
void DespawnAllDecorForPlot(uint8 plotIndex);
|
||||
void SpawnAllDecorForPlot(uint8 plotIndex, Housing const* housing);
|
||||
|
||||
@@ -855,6 +855,37 @@ void HousingMgr::LoadRoomComponentData()
|
||||
"across {} room types from {} DB2 entries",
|
||||
totalCount, doorwayCount, uint32(_roomComponentsByWmoData.size()),
|
||||
uint32(sRoomComponentStore.GetNumRows()));
|
||||
|
||||
// Diagnostic: log HouseRoom entries with their component counts
|
||||
for (auto const& [roomId, roomData] : _houseRoomStore)
|
||||
{
|
||||
auto const* comps = GetRoomComponents(roomData.RoomWmoDataID);
|
||||
uint32 compCount = comps ? uint32(comps->size()) : 0;
|
||||
|
||||
// Count component types
|
||||
uint32 wallCount = 0, floorCount = 0, ceilCount = 0, doorwayCount2 = 0, otherCount = 0;
|
||||
if (comps)
|
||||
{
|
||||
for (auto const& c : *comps)
|
||||
{
|
||||
switch (c.Type)
|
||||
{
|
||||
case HOUSING_ROOM_COMPONENT_WALL: ++wallCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_FLOOR: ++floorCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_CEILING: ++ceilCount; break;
|
||||
case HOUSING_ROOM_COMPONENT_DOORWAY:
|
||||
case HOUSING_ROOM_COMPONENT_DOORWAY_WALL: ++doorwayCount2; break;
|
||||
default: ++otherCount; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TC_LOG_INFO("housing", " HouseRoom [ID={} '{}' RoomWmoDataID={} Flags=0x{:X}{}] -> {} components "
|
||||
"({} wall, {} floor, {} ceiling, {} doorway, {} other)",
|
||||
roomId, roomData.Name, roomData.RoomWmoDataID, roomData.Flags,
|
||||
roomData.IsBaseRoom() ? " BASE_ROOM" : "",
|
||||
compCount, wallCount, floorCount, ceilCount, doorwayCount2, otherCount);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<RoomComponentData> const* HousingMgr::GetRoomComponents(uint32 roomWmoDataId) const
|
||||
@@ -1050,3 +1081,60 @@ std::vector<DecorDyeSlotData const*> HousingMgr::GetDyeSlotsForDecor(uint32 hous
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
int32 HousingMgr::GetFactionDefaultThemeID(int32 factionRestriction) const
|
||||
{
|
||||
// Sniff-verified: Alliance theme=6, Horde theme=2
|
||||
if (factionRestriction == NEIGHBORHOOD_FACTION_ALLIANCE)
|
||||
return 6;
|
||||
if (factionRestriction == NEIGHBORHOOD_FACTION_HORDE)
|
||||
return 2;
|
||||
return 6; // fallback to alliance
|
||||
}
|
||||
|
||||
RoomComponentOptionEntry const* HousingMgr::FindRoomComponentOption(uint32 roomComponentID, int32 houseThemeID) const
|
||||
{
|
||||
for (RoomComponentOptionEntry const* optEntry : sRoomComponentOptionStore)
|
||||
{
|
||||
if (optEntry && optEntry->RoomComponentID == static_cast<int32>(roomComponentID)
|
||||
&& optEntry->HouseThemeID == houseThemeID)
|
||||
return optEntry;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32 HousingMgr::GetDefaultVisualRoomEntry() const
|
||||
{
|
||||
// Sniff-verified: both alliance and horde use HouseRoomID=1 ("Square Room Small")
|
||||
// as the primary interior room. The faction theme (themeID 6=Alliance, 2=Horde)
|
||||
// controls wall/floor textures via RoomComponentOption, not the room shape.
|
||||
// Pick the lowest-ID non-base room with UNLOCKED_BY_DEFAULT + visual components.
|
||||
uint32 bestId = 0;
|
||||
uint32 fallbackId = 0;
|
||||
|
||||
for (auto const& [id, roomData] : _houseRoomStore)
|
||||
{
|
||||
if (roomData.IsBaseRoom())
|
||||
continue;
|
||||
|
||||
auto const* comps = GetRoomComponents(roomData.RoomWmoDataID);
|
||||
if (!comps || comps->size() <= 1)
|
||||
continue;
|
||||
|
||||
if (roomData.Flags & HOUSING_ROOM_FLAG_UNLOCKED_BY_DEFAULT)
|
||||
{
|
||||
// Pick lowest ID for determinism (room 1 = Square Room Small, the sniff default)
|
||||
if (!bestId || id < bestId)
|
||||
bestId = id;
|
||||
}
|
||||
else if (!fallbackId || id < fallbackId)
|
||||
{
|
||||
fallbackId = id;
|
||||
}
|
||||
}
|
||||
|
||||
uint32 result = bestId ? bestId : fallbackId;
|
||||
TC_LOG_ERROR("housing", "HousingMgr::GetDefaultVisualRoomEntry: bestId={} fallbackId={} -> returning {}",
|
||||
bestId, fallbackId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -296,6 +296,16 @@ public:
|
||||
// Room component data (all types: wall, floor, ceiling, stairs, doorway, etc.)
|
||||
std::vector<RoomComponentData> const* GetRoomComponents(uint32 roomWmoDataId) const;
|
||||
|
||||
// Faction-to-theme mapping (sniff-verified: Alliance=6, Horde=2)
|
||||
int32 GetFactionDefaultThemeID(int32 factionRestriction) const;
|
||||
|
||||
// Find a RoomComponentOption matching a specific component + theme
|
||||
// Returns nullptr if no match found
|
||||
RoomComponentOptionEntry const* FindRoomComponentOption(uint32 roomComponentID, int32 houseThemeID) const;
|
||||
|
||||
// Find the first HouseRoom entry with visual components (not the base room 18)
|
||||
uint32 GetDefaultVisualRoomEntry() const;
|
||||
|
||||
// Starter decor (items granted on first house purchase)
|
||||
// Returns starter decor IDs filtered by faction (teamId: ALLIANCE=469, HORDE=67)
|
||||
// Sniff-verified: Alliance and Horde receive different starter decor sets
|
||||
|
||||
@@ -277,8 +277,19 @@ Map* MapManager::CreateMap(uint32 mapId, Player* player, Optional<uint32> lfgDun
|
||||
// Instance ID = player's GUID counter so each player gets their own interior.
|
||||
newInstanceId = player->GetGUID().GetCounter();
|
||||
map = FindMap_i(mapId, newInstanceId);
|
||||
if (!map)
|
||||
if (map)
|
||||
{
|
||||
TC_LOG_ERROR("housing", "MapManager::CreateMap: REUSING existing HouseInteriorMap mapId={} instanceId={} "
|
||||
"for player {} (map ptr={})",
|
||||
mapId, newInstanceId, player->GetGUID().ToString(), (void*)map);
|
||||
}
|
||||
else
|
||||
{
|
||||
map = CreateHouseInterior(mapId, newInstanceId, player);
|
||||
TC_LOG_ERROR("housing", "MapManager::CreateMap: CREATED NEW HouseInteriorMap mapId={} instanceId={} "
|
||||
"for player {} (map ptr={})",
|
||||
mapId, newInstanceId, player->GetGUID().ToString(), (void*)map);
|
||||
}
|
||||
}
|
||||
else if (entry->IsNeighborhood())
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user