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:
luis
2026-02-28 18:50:58 -03:00
parent c6e7d589d4
commit 224b62a1f8
12 changed files with 1127 additions and 259 deletions
@@ -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);
+12 -5
View File
@@ -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
+512 -104
View File
@@ -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 -2
View File
@@ -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__
+127 -19
View File
@@ -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;
+6 -2
View File
@@ -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.
+300 -124
View File
@@ -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);
}
}
+6 -2
View File
@@ -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);
+88
View File
@@ -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;
}
+10
View File
@@ -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
+12 -1
View File
@@ -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())
{