Housing: Fix exterior component tree spawning with data-driven multi-root architecture

- Fix DB2 store iteration bug: ExteriorComponentHook entries with
  ParentIndexField were only partially accessible via range-based
  iteration (1808 of 23881 entries). Use LookupEntry() over
  GetNumRows() to reach all entries, fixing hook resolution
  (compByHook was 0, now resolves all 1461 GroupXHook mappings).

- Spawn all root components per HouseExteriorWmoDataID: houses
  consist of multiple independent roots (Base type=9, Roof type=10)
  sharing the same WMO data ID. Each root spawns independently at
  house position with its own hook children (doors on base,
  chimney/windows on roof). Filter by Size to avoid spawning
  small/medium/large variants simultaneously.

- Build parent-child index from ExteriorComponent.ParentComponentID
  for component variants that reference a parent component.

- Add _rootCompsByWmoDataId index for fast lookup of all root
  components belonging to a house exterior.

- Fix MeshObject fixture data to pass fixtureGuid and
  parentFixtureGuid for proper client-side attachment hierarchy.

- Fix BNetAccount dirty state in fixture edit mode by clearing
  update mask after PopulateCatalogStorageEntries.
This commit is contained in:
luis
2026-03-14 07:45:27 -03:00
parent e9876286b9
commit 788094aebc
7 changed files with 343 additions and 203 deletions
@@ -196,15 +196,26 @@ void MeshObject::InitHousingDecorData(ObjectGuid decorGuid, ObjectGuid houseGuid
GetGUID().ToString(), decorGuid.ToString(), houseGuid.ToString(), flags, roomEntityGuid.ToString());
}
void MeshObject::InitHousingFixtureData(ObjectGuid houseGuid, int32 exteriorComponentID,
void MeshObject::InitHousingFixtureData(ObjectGuid houseGuid, ObjectGuid fixtureGuid,
ObjectGuid parentFixtureGuid, int32 exteriorComponentID,
int32 houseExteriorWmoDataID, uint8 exteriorComponentType /*= 9*/,
uint8 houseSize /*= 2*/, int32 exteriorComponentHookID /*= -1*/, bool isRoot /*= false*/)
{
if (m_housingFixtureData.has_value())
return;
// Uses the same m_housingFixtureData declared on Object (Object.h:167)
// Sniff-verified field values (11.2 retail MeshObject with FHousingFixture_C)
// FHousingFixture_C fragment (ID 34, 96 bytes, 11 fields with HasChangesMask<11>).
// Field order must match the client's CREATE deserializer:
// [0] ExteriorComponentID (CompressedUInt32)
// [1] HouseExteriorWmoDataID (CompressedUInt32)
// [2] ExteriorComponentHookID (CompressedUInt32, defaults -1)
// [3] HouseGUID (PackedGUID128)
// [4] AttachParentGUID (PackedGUID128) ? parent fixture in hierarchy
// [5] Guid (PackedGUID128) ? unique per fixture, for client identification
// [6] GameObjectGUID (PackedGUID128) ? always empty
// [7] ExteriorComponentType (uint8)
// [8] Field_59 (uint8)
// [9] Size (uint8)
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
.ModifyValue(&UF::HousingFixtureData::ExteriorComponentID), exteriorComponentID);
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
@@ -213,9 +224,15 @@ void MeshObject::InitHousingFixtureData(ObjectGuid houseGuid, int32 exteriorComp
.ModifyValue(&UF::HousingFixtureData::ExteriorComponentHookID), exteriorComponentHookID);
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
.ModifyValue(&UF::HousingFixtureData::HouseGUID), houseGuid);
// Guid must be a Housing-type GUID (client crashes with non-Housing GUIDs here)
// AttachParentGUID: the parent fixture's unique GUID in the hierarchy.
// Root pieces have empty parent. Child pieces point to their parent root's fixture GUID.
// The client uses this to build the fixture tree and resolve hook point ownership.
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
.ModifyValue(&UF::HousingFixtureData::Guid), houseGuid);
.ModifyValue(&UF::HousingFixtureData::AttachParentGUID), parentFixtureGuid);
// Guid: unique per fixture ? the client uses this to identify individual fixtures.
// Must be a Housing-type GUID (client crashes with non-Housing GUIDs here).
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
.ModifyValue(&UF::HousingFixtureData::Guid), fixtureGuid);
// GameObjectGUID: sniff confirms empty for all fixture pieces
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
.ModifyValue(&UF::HousingFixtureData::ExteriorComponentType), exteriorComponentType);
@@ -227,9 +244,10 @@ void MeshObject::InitHousingFixtureData(ObjectGuid houseGuid, int32 exteriorComp
m_entityFragments.Add(WowCS::EntityFragment::FHousingFixture_C, IsInWorld(),
WowCS::GetRawFragmentData(m_housingFixtureData));
// Cache for targeted fixture lookup
// Cache for targeted fixture lookup and hierarchy traversal
_exteriorComponentHookID = exteriorComponentHookID;
_exteriorComponentID = exteriorComponentID;
_fixtureGuid = fixtureGuid;
// Root pieces get Tag_HouseExteriorRoot (225), child pieces get Tag_HouseExteriorPiece (224).
// The client uses Tag_HouseExteriorRoot to identify the fixture GUID for edit mode enter/exit.
@@ -239,11 +257,11 @@ void MeshObject::InitHousingFixtureData(ObjectGuid houseGuid, int32 exteriorComp
else
m_entityFragments.Add(WowCS::EntityFragment::Tag_HouseExteriorPiece, IsInWorld());
TC_LOG_ERROR("housing", "MeshObject::InitHousingFixtureData: guid={} houseGuid={} "
"exteriorComponentID={} wmoDataID={} hookID={} componentType={} size={} isRoot={}",
GetGUID().ToString(), houseGuid.ToString(),
exteriorComponentID, houseExteriorWmoDataID, exteriorComponentHookID,
exteriorComponentType, houseSize, isRoot);
TC_LOG_DEBUG("housing", "MeshObject::InitHousingFixtureData: meshGuid={} fixtureGuid={} "
"parentFixtureGuid={} houseGuid={} extCompID={} wmoDataID={} hookID={} type={} size={} isRoot={}",
GetGUID().ToString(), fixtureGuid.ToString(), parentFixtureGuid.ToString(),
houseGuid.ToString(), exteriorComponentID, houseExteriorWmoDataID,
exteriorComponentHookID, exteriorComponentType, houseSize, isRoot);
}
void MeshObject::UpdateExteriorComponentID(int32 id)
@@ -66,9 +66,11 @@ public:
// Housing fixture
// isRoot: true for root pieces (no parent attachment), false for child pieces.
// Root pieces get Tag_HouseExteriorRoot (225), children get Tag_HouseExteriorPiece (224).
void InitHousingFixtureData(ObjectGuid houseGuid, int32 exteriorComponentID,
void InitHousingFixtureData(ObjectGuid houseGuid, ObjectGuid fixtureGuid,
ObjectGuid parentFixtureGuid, int32 exteriorComponentID,
int32 houseExteriorWmoDataID, uint8 exteriorComponentType = 9,
uint8 houseSize = 2, int32 exteriorComponentHookID = -1, bool isRoot = false);
ObjectGuid const& GetFixtureGuid() const { return _fixtureGuid; }
// Housing decor (adds FHousingDecor_C entity fragment for placed decor items)
// Sniff-verified: retail decor is ALWAYS MeshObject (never GO). TargetGameObjectGUID=empty.
@@ -123,6 +125,7 @@ private:
bool _isExteriorRoot = false;
int32 _exteriorComponentHookID = -1;
int32 _exteriorComponentID = 0;
ObjectGuid _fixtureGuid;
};
#endif // TRINITYCORE_MESHOBJECT_H
+24 -4
View File
@@ -753,6 +753,13 @@ void WorldSession::HandleHousingDecorSetEditMode(WorldPackets::Housing::HousingD
player->ClearUpdateMask(false);
}
// Clear Account entity dirty state on EXIT. During edit mode, decor operations
// (place/move/remove) modify FHousingStorage_C which marks the Account dirty.
// Without this, Map::SendObjectUpdates() sends a stale VALUES_UPDATE on the
// next tick, which the client rejects ("Object update failed for BNetAccount").
GetBattlenetAccount().ClearUpdateMask(true);
GetHousingPlayerHouseEntity().ClearUpdateMask(true);
TC_LOG_DEBUG("housing", " EditMode EXIT: BNetAccountGuid={}",
response.BNetAccountGuid.ToString());
}
@@ -1466,6 +1473,15 @@ void WorldSession::HandleHousingFixtureSetEditMode(WorldPackets::Housing::Housin
housing->SyncUpdateFields();
}
// CRITICAL: Clear Account entity dirty state BEFORE sending any packets.
// PopulateCatalogStorageEntries() modifies FHousingStorage_C which marks the
// Account entity dirty. Unlike decor edit mode, fixture edit mode doesn't send
// the Account entity as CREATE here. If we leave it dirty, Map::SendObjectUpdates()
// will send a VALUES_UPDATE on the next tick, which the client rejects because
// MapUpdateField entries can't be added via VALUES_UPDATE when initially empty.
// Also clear on EXIT to prevent any lingering dirty state from decor operations.
GetBattlenetAccount().ClearUpdateMask(true);
// ======================================================================
// Sniff-verified retail packet sequence (build 66337):
// #10161 S->C SMSG_UPDATE_OBJECT (56B) ? editor mode field change
@@ -1581,18 +1597,22 @@ void WorldSession::HandleHousingFixtureSetCoreFixture(WorldPackets::Housing::Hou
// Validate ExteriorComponentID against DB2 store
uint32 componentID = housingFixtureSetCoreFixture.ExteriorComponentID;
TC_LOG_INFO("housing", "CMSG_HOUSING_FIXTURE_SET_CORE_FIXTURE: FixtureGuid={} ExteriorComponentID={} (store has {} entries)",
housingFixtureSetCoreFixture.FixtureGuid.ToString(), componentID, sExteriorComponentStore.GetNumRows());
ExteriorComponentEntry const* componentEntry = sExteriorComponentStore.LookupEntry(componentID);
if (!componentEntry)
{
TC_LOG_DEBUG("housing", "CMSG_HOUSING_FIXTURE_SET_CORE_FIXTURE ExteriorComponentID {} not found in DB2",
componentID);
TC_LOG_INFO("housing", "CMSG_HOUSING_FIXTURE_SET_CORE_FIXTURE ExteriorComponentID {} not found in DB2 (store has {} entries)",
componentID, sExteriorComponentStore.GetNumRows());
WorldPackets::Housing::HousingFixtureSetCoreFixtureResponse response;
response.Result = static_cast<uint8>(HOUSING_RESULT_FIXTURE_NOT_FOUND);
SendPacket(response.Write());
return;
}
TC_LOG_DEBUG("housing", "CMSG_HOUSING_FIXTURE_SET_CORE_FIXTURE DB2 lookup: ExteriorComponentID={}, Name='{}', Type={}, Size={}, Flags={}, ParentCompID={}, WmoDataID={}",
TC_LOG_INFO("housing", "CMSG_HOUSING_FIXTURE_SET_CORE_FIXTURE DB2 lookup OK: ExteriorComponentID={}, Name='{}', Type={}, Size={}, Flags={}, ParentCompID={}, WmoDataID={}",
componentID, componentEntry->Name[DEFAULT_LOCALE] ? componentEntry->Name[DEFAULT_LOCALE] : "",
componentEntry->Type, componentEntry->Size, componentEntry->Flags,
componentEntry->ParentComponentID, componentEntry->HouseExteriorWmoDataID);
@@ -1773,7 +1793,7 @@ void WorldSession::HandleHousingFixtureDeleteFixture(WorldPackets::Housing::Hous
}
// Spawn default component back at this hook (DB2 default)
ExteriorComponentEntry const* defaultComp = sHousingMgr.GetComponentAtHook(static_cast<int32>(removedHookID));
ExteriorComponentEntry const* defaultComp = sHousingMgr.GetComponentAtHook(static_cast<int32>(removedHookID), housing->GetCoreExteriorComponentID());
if (defaultComp)
{
housingMap->SpawnFixtureAtHook(plotIndex, removedHookID, defaultComp->ID,
+149 -120
View File
@@ -299,7 +299,7 @@ void HousingMap::SpawnPlotGameObjects()
// fragment, SpellForVisuals, SpellXSpellVisualID) BEFORE the CREATE_OBJECT
// packet is sent. The client needs FHousingPlotAreaTrigger_C and
// DecalPropertiesId=621 in the initial create to render the plot border decal.
AreaTrigger* plotAt = AreaTrigger::CreateStaticAreaTrigger({ .Id = 37358, .IsCustom = false }, this, atPos, -1);
AreaTrigger* plotAt = AreaTrigger::CreateStaticAreaTrigger({ .Id = 37358, .IsCustom = false }, this, atPos, -1, false);
if (plotAt)
{
PhasingHandler::InitDbPhaseShift(plotAt->GetPhaseShift(), PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
@@ -1938,7 +1938,30 @@ MeshObject* HousingMap::SpawnHouseMeshObject(uint8 plotIndex, int32 fileDataID,
// All other pieces (roof, door, chimney, windows) get Tag_HouseExteriorPiece (224).
// The client uses Tag_HouseExteriorRoot to identify the fixture GUID for edit mode.
bool isRoot = (exteriorComponentType == 9) && attachParent.IsEmpty();
mesh->InitHousingFixtureData(houseGuid, exteriorComponentID, houseExteriorWmoDataID,
// Generate a unique fixture GUID per fixture. The client uses FHousingFixture_C::Guid
// to identify individual fixtures ? if all fixtures share the same GUID (houseGuid),
// the client can't distinguish them and reports "Fixture not found".
// Use subType=5 (fixture), realm, hookID-or-componentID, houseGuid counter.
uint32 fixtureArg2 = (exteriorComponentHookID > 0)
? static_cast<uint32>(exteriorComponentHookID)
: static_cast<uint32>(exteriorComponentID);
ObjectGuid fixtureGuid = ObjectGuid::Create<HighGuid::Housing>(
/*subType*/ 5, /*arg1*/ sRealmList->GetCurrentRealmId().Realm,
/*arg2*/ fixtureArg2, houseGuid.GetCounter());
// Look up the parent fixture's unique GUID for AttachParentGUID field.
// The client uses this to build the fixture hierarchy tree ? without it,
// the client can't determine which hook points have fixtures installed.
ObjectGuid parentFixtureGuid;
if (!attachParent.IsEmpty())
{
if (MeshObject* parentMesh = GetMeshObject(attachParent))
parentFixtureGuid = parentMesh->GetFixtureGuid();
}
mesh->InitHousingFixtureData(houseGuid, fixtureGuid, parentFixtureGuid,
exteriorComponentID, houseExteriorWmoDataID,
exteriorComponentType, houseSize, exteriorComponentHookID, isRoot);
// Now add to map ? this triggers the create packet with all fragments included
@@ -1975,139 +1998,94 @@ void HousingMap::SpawnFullHouseMeshObjects(uint8 plotIndex, Position const& hous
uint32 coreExtCompID = static_cast<uint32>(exteriorComponentID);
int32 groupID = sHousingMgr.GetGroupForComponent(coreExtCompID);
if (groupID != 0)
ExteriorComponentEntry const* coreComp = sExteriorComponentStore.LookupEntry(coreExtCompID);
if (coreComp && coreComp->ModelFileDataID > 0 && coreComp->HouseExteriorWmoDataID > 0)
{
// Find all root components in this group (those with HookID <= 0 are roots)
std::vector<uint32> const* groupComps = sHousingMgr.GetComponentsInGroup(groupID);
if (groupComps && !groupComps->empty())
// A house consists of multiple independent root components (Base type=9, Roof type=10, etc.)
// all sharing the same HouseExteriorWmoDataID. Each root is spawned independently at the
// house position, and each has its own hook children (doors on base, chimney/windows on roof).
//
// Among roots of the same type, there may be variants (e.g. multiple roof styles). The
// coreExtCompID tells us which base variant is selected; for other types we pick the
// default (Flags & 0x1 IsDefault) or first available.
uint32 wmoDataID = coreComp->HouseExteriorWmoDataID;
auto const* rootComps = sHousingMgr.GetRootComponentsForWmoData(wmoDataID);
uint32 totalSpawned = 0;
if (rootComps)
{
uint32 totalSpawned = 0;
for (uint32 compID : *groupComps)
// Group roots by type, filtering to match the house's Size
uint8 houseSize = coreComp->Size;
std::unordered_map<uint8 /*type*/, std::vector<uint32>> rootsByType;
for (uint32 rootID : *rootComps)
{
ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(compID);
if (!comp || comp->ParentComponentID > 0) // skip children ? they'll be spawned recursively
continue;
// Check if this root component has been overridden (e.g., roof 1503 ? 1497)
uint32 spawnCompID = compID;
if (fixtureOverrides)
{
auto overrideItr = fixtureOverrides->find(compID);
if (overrideItr != fixtureOverrides->end())
{
TC_LOG_DEBUG("housing", "HousingMap::SpawnFullHouseMeshObjects: Root override for plot {} ? "
"default {} ? override {}", plotIndex, compID, overrideItr->second);
spawnCompID = overrideItr->second;
}
}
totalSpawned += SpawnExtCompTree(plotIndex, spawnCompID,
housePos, houseRot,
houseGuid, houseExteriorWmoDataID,
ObjectGuid::Empty, nullptr, 0, fixtureOverrides);
ExteriorComponentEntry const* rc = sExteriorComponentStore.LookupEntry(rootID);
if (rc && rc->ModelFileDataID > 0 && rc->Size == houseSize)
rootsByType[rc->Type].push_back(rootID);
}
if (totalSpawned > 0)
for (auto const& [type, compIDs] : rootsByType)
{
// Check which components were included in the data-driven group.
// If missing, spawn them from hardcoded data.
bool roofFound = false;
bool doorFound = false;
bool chimneyFound = false;
MeshObject* basePieceDd = nullptr;
MeshObject* roofPieceDd = nullptr;
auto meshItr = _meshObjects.find(plotIndex);
if (meshItr != _meshObjects.end())
uint32 selectedCompID = 0;
if (type == coreComp->Type)
{
for (ObjectGuid const& guid : meshItr->second)
// For the base type, use the player's selected coreExtCompID
selectedCompID = coreExtCompID;
}
else
{
// For other types (roof, etc.), pick IsDefault or first available
// TODO: check fixture overrides for player-selected roof variant
for (uint32 id : compIDs)
{
if (MeshObject* mesh = GetMeshObject(guid))
ExteriorComponentEntry const* rc = sExteriorComponentStore.LookupEntry(id);
if (!rc) continue;
if (!selectedCompID)
selectedCompID = id; // fallback: first available
if (rc->Flags & 0x1)
{
if (mesh->GetFileDataID() == 7420602)
{
roofFound = true; roofPieceDd = mesh;
}
else if (mesh->GetFileDataID() == 7450804)
doorFound = true;
else if (mesh->GetFileDataID() == 6648736)
basePieceDd = mesh;
else if (mesh->GetFileDataID() == 7118952)
chimneyFound = true;
selectedCompID = id; // prefer IsDefault
break;
}
}
}
if (factionRestriction == NEIGHBORHOOD_FACTION_ALLIANCE)
if (selectedCompID)
{
if (!roofFound)
{
roofPieceDd = SpawnHouseMeshObject(plotIndex, 7420602, /*isWMO*/ true,
housePos, houseRot, 1.0f,
houseGuid, 1503, houseExteriorWmoDataID,
/*exteriorComponentType*/ 10, /*houseSize*/ 2, /*hookID*/ -1,
ObjectGuid::Empty, /*attachFlags*/ 0);
totalSpawned++;
}
ExteriorComponentEntry const* selComp = sExteriorComponentStore.LookupEntry(selectedCompID);
TC_LOG_INFO("housing", "SpawnFullHouseMeshObjects: Spawning root type={} comp={} '{}' "
"(wmoDataID={}, size={}, ModelFDID={})",
type, selectedCompID,
selComp && selComp->Name[DEFAULT_LOCALE] ? selComp->Name[DEFAULT_LOCALE] : "",
wmoDataID, houseSize, selComp ? selComp->ModelFileDataID : 0);
if (!doorFound && basePieceDd)
{
SpawnHouseMeshObject(plotIndex, 7450804, /*isWMO*/ true,
Position(9.2805f, -3.4555f, -0.5611f, 0.0f),
QuaternionData(0.0f, 0.0f, 0.0f, 1.0f), 1.0f,
houseGuid, 1380, houseExteriorWmoDataID,
/*exteriorComponentType*/ 11, /*houseSize*/ 1, /*hookID*/ 2505,
basePieceDd->GetGUID(), /*attachFlags*/ 3, &housePos);
totalSpawned++;
}
// Chimney + windows as children of roof
if (!chimneyFound && roofPieceDd)
{
ObjectGuid roofGuid = roofPieceDd->GetGUID();
SpawnHouseMeshObject(plotIndex, 7118952, /*isWMO*/ true,
Position(-3.6472f, -5.6444f, 12.3556f, 0.0f),
QuaternionData(0.0f, 0.0f, -0.7071066f, 0.70710695f), 1.0f,
houseGuid, 1452, houseExteriorWmoDataID,
/*exteriorComponentType*/ 16, /*houseSize*/ 2, /*hookID*/ 14931,
roofGuid, /*attachFlags*/ 3, &housePos);
SpawnHouseMeshObject(plotIndex, 7450830, /*isWMO*/ true,
Position(-3.025f, -0.0222f, 11.35f, 0.0f),
QuaternionData(0.0f, 0.0f, -1.0f, 0.0f), 1.0f,
houseGuid, 1448, houseExteriorWmoDataID,
/*exteriorComponentType*/ 14, /*houseSize*/ 2, /*hookID*/ 17202,
roofGuid, /*attachFlags*/ 3, &housePos);
SpawnHouseMeshObject(plotIndex, 7450830, /*isWMO*/ true,
Position(3.0305f, -0.0222f, 11.35f, 0.0f),
QuaternionData(0.0f, 0.0f, 0.0f, 1.0f), 1.0f,
houseGuid, 1448, houseExteriorWmoDataID,
/*exteriorComponentType*/ 14, /*houseSize*/ 2, /*hookID*/ 14929,
roofGuid, /*attachFlags*/ 3, &housePos);
totalSpawned += 3;
}
totalSpawned += SpawnExtCompTree(plotIndex, selectedCompID,
housePos, houseRot,
houseGuid, houseExteriorWmoDataID,
ObjectGuid::Empty, nullptr, 0, fixtureOverrides);
}
TC_LOG_INFO("housing", "HousingMap::SpawnFullHouseMeshObjects: Data-driven spawn "
"for plot {} group {} ? {} total MeshObjects (faction={}, door={})",
plotIndex, groupID, totalSpawned,
factionRestriction == NEIGHBORHOOD_FACTION_ALLIANCE ? "Alliance" : "Horde",
doorFound ? "data-driven" : (factionRestriction == NEIGHBORHOOD_FACTION_ALLIANCE ? "hardcoded" : "N/A"));
return;
}
TC_LOG_WARN("housing", "HousingMap::SpawnFullHouseMeshObjects: Data-driven spawn "
"yielded 0 meshes for plot {} group {} ? falling back to hardcoded",
plotIndex, groupID);
}
if (totalSpawned > 0)
{
TC_LOG_INFO("housing", "HousingMap::SpawnFullHouseMeshObjects: Data-driven spawn "
"for plot {} wmoDataID {} coreComp {} ? {} total MeshObjects (faction={})",
plotIndex, wmoDataID, coreExtCompID, totalSpawned,
factionRestriction == NEIGHBORHOOD_FACTION_ALLIANCE ? "Alliance" : "Horde");
return;
}
TC_LOG_WARN("housing", "HousingMap::SpawnFullHouseMeshObjects: Data-driven spawn "
"yielded 0 meshes for plot {} wmoDataID {} ? falling back to hardcoded",
plotIndex, wmoDataID);
}
else
else if (!coreComp)
{
TC_LOG_DEBUG("housing", "HousingMap::SpawnFullHouseMeshObjects: No group found for "
"exteriorComponentID {} ? using hardcoded spawn for plot {}",
exteriorComponentID, plotIndex);
TC_LOG_DEBUG("housing", "HousingMap::SpawnFullHouseMeshObjects: ExteriorComponent {} not found "
"? using hardcoded spawn for plot {}", exteriorComponentID, plotIndex);
}
// === HARDCODED FALLBACK ===
@@ -2321,10 +2299,10 @@ uint32 HousingMap::SpawnExtCompTree(uint8 plotIndex, uint32 extCompID,
// Determine attach flags: root pieces (no parent) use 0, children use 3
uint8 attachFlags = parentGuid.IsEmpty() ? 0 : 3;
// For CreateFixture: at depth=0, the hookIDOverride tells the client which hook point
// this component occupies. The ExteriorComponent has no hook ID field ? hook relationships
// come from ExteriorComponentHook and ExteriorComponentGroupXHook tables.
int32 effectiveHookID = (depth == 0 && hookIDOverride > 0) ? hookIDOverride : -1;
// The hookIDOverride tells the client which hook point this component occupies.
// This must propagate at ALL depths (not just depth=0) because during initial house spawn
// via SpawnFullHouseMeshObjects, hooks are iterated at depth >= 1 with valid hookIDOverride.
int32 effectiveHookID = (hookIDOverride > 0) ? hookIDOverride : -1;
MeshObject* mesh = SpawnHouseMeshObject(plotIndex, comp->ModelFileDataID, /*isWMO*/ true,
pos, rot, 1.0f,
@@ -2346,6 +2324,8 @@ uint32 HousingMap::SpawnExtCompTree(uint8 plotIndex, uint32 extCompID,
// Use worldPos for children: root's worldPos is itself, child's is the root's position
Position const* childWorldPos = worldPos ? worldPos : &pos;
std::set<uint32> spawnedChildComps; // track to avoid double-spawning
// Recurse into hooks on this component
auto const* hooks = sHousingMgr.GetHooksOnComponent(extCompID);
if (hooks)
@@ -2364,10 +2344,17 @@ uint32 HousingMap::SpawnExtCompTree(uint8 plotIndex, uint32 extCompID,
childComp = sExteriorComponentStore.LookupEntry(overrideItr->second);
}
if (!childComp)
childComp = sHousingMgr.GetComponentAtHook(static_cast<int32>(hook->ID));
childComp = sHousingMgr.GetComponentAtHook(static_cast<int32>(hook->ID), extCompID);
if (!childComp)
continue;
spawnedChildComps.insert(childComp->ID);
TC_LOG_DEBUG("housing", "SpawnExtCompTree: parent={} hook={} (type={}) ? child comp {} '{}' (ParentComp={}, ModelFDID={})",
extCompID, hook->ID, hook->ExteriorComponentTypeID, childComp->ID,
childComp->Name[DEFAULT_LOCALE] ? childComp->Name[DEFAULT_LOCALE] : "",
childComp->ParentComponentID, childComp->ModelFileDataID);
// Hook position/rotation are local-space offsets relative to the parent
Position hookPos(hook->Position[0], hook->Position[1], hook->Position[2], 0.0f);
QuaternionData hookRot;
@@ -2392,6 +2379,48 @@ uint32 HousingMap::SpawnExtCompTree(uint8 plotIndex, uint32 extCompID,
}
}
// Also walk children linked via ExteriorComponent.ParentComponentID.
// These are components (e.g. roofs) where ParentComponentID references this component.
// They may not have hooks defining their position ? use the child's Position field as offset.
auto const* children = sHousingMgr.GetChildComponents(extCompID);
if (children)
{
for (uint32 childCompID : *children)
{
if (spawnedChildComps.count(childCompID))
continue; // already spawned via hook
ExteriorComponentEntry const* childComp = sExteriorComponentStore.LookupEntry(childCompID);
if (!childComp || childComp->ModelFileDataID <= 0)
continue;
// Only spawn if this is a default component (or if there's no default, take first)
// Skip non-default variants ? fixture overrides handle those
if (!(childComp->Flags & 0x1))
continue;
TC_LOG_INFO("housing", "SpawnExtCompTree: parent={} ? ParentComponentID child comp {} '{}' "
"(type={}, ModelFDID={}, Pos=({:.1f},{:.1f},{:.1f}))",
extCompID, childComp->ID,
childComp->Name[DEFAULT_LOCALE] ? childComp->Name[DEFAULT_LOCALE] : "",
childComp->Type, childComp->ModelFileDataID,
childComp->Position[0], childComp->Position[1], childComp->Position[2]);
// Child's Position field is local-space offset from parent
Position childPos(childComp->Position[0], childComp->Position[1], childComp->Position[2], 0.0f);
QuaternionData childRot; // identity rotation
childRot.x = 0.0f;
childRot.y = 0.0f;
childRot.z = 0.0f;
childRot.w = 1.0f;
count += SpawnExtCompTree(plotIndex, childCompID,
childPos, childRot,
houseGuid, houseExteriorWmoDataID,
meshGuid, childWorldPos, depth + 1, fixtureOverrides);
}
}
return count;
}
+120 -64
View File
@@ -1265,79 +1265,40 @@ void HousingMgr::BuildExteriorComponentIndexes()
_exitPointByExtComp.clear();
_groupByExtComp.clear();
_extCompsByGroup.clear();
_childrenByExtComp.clear();
_rootCompsByWmoDataId.clear();
// 1. Build hook index: which hooks are parented to each component
for (ExteriorComponentHookEntry const* hook : sExteriorComponentHookStore)
// Note: store iteration only yields a subset of entries due to DB2 ParentIndexField
// sparse indexing. Use LookupEntry(i) over GetNumRows() to reach all entries.
for (uint32 i = 0; i < sExteriorComponentHookStore.GetNumRows(); ++i)
{
ExteriorComponentHookEntry const* hook = sExteriorComponentHookStore.LookupEntry(i);
if (!hook)
continue;
_hooksByExtComp[hook->ExteriorComponentID].push_back(hook);
}
// 2. Build reverse lookup: hookID ? default component that should be placed there.
// Chain: ExteriorComponentGroupXHook maps groups to hooks.
// For each hook, find components in groups linked to that hook with matching Type
// and IsDefaultFixture flag (Flags & 0x1).
// First, build group?hooks mapping from GroupXHook
std::unordered_map<int32, std::vector<int32>> hooksByGroup; // groupID ? hookIDs
for (ExteriorComponentGroupXHookEntry const* gxh : sExteriorComponentGroupXHookStore)
{
if (!gxh)
continue;
hooksByGroup[gxh->ExteriorComponentGroupID].push_back(gxh->ExteriorComponentHookID);
}
// For each hook, find the default component: same Type as hook's ComponentType, IsDefaultFixture
// We do this after step 4 (group indexes) so we can look up components in groups.
// For now, build a type?default-components index to use below.
std::unordered_map<int32, std::vector<ExteriorComponentEntry const*>> compsByType;
for (ExteriorComponentEntry const* comp : sExteriorComponentStore)
// 1a. Build child index and root-by-WMO index from ExteriorComponent.
// ParentComponentID > 0 ? child of that component.
// ParentComponentID == 0 ? root component, indexed by HouseExteriorWmoDataID.
// ExteriorComponent uses ParentIndexField (HouseExteriorWmoDataID), so use
// LookupEntry over GetNumRows() to reach all entries.
for (uint32 i = 0; i < sExteriorComponentStore.GetNumRows(); ++i)
{
ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(i);
if (!comp)
continue;
compsByType[comp->Type].push_back(comp);
if (comp->ParentComponentID > 0)
_childrenByExtComp[static_cast<uint32>(comp->ParentComponentID)].push_back(comp->ID);
// Root components (ParentComponentID=0) with a model, indexed by WMO data ID
if (comp->ParentComponentID == 0 && comp->ModelFileDataID > 0 && comp->HouseExteriorWmoDataID > 0)
_rootCompsByWmoDataId[comp->HouseExteriorWmoDataID].push_back(comp->ID);
}
// Now for each hook, find the default component
for (ExteriorComponentHookEntry const* hook : sExteriorComponentHookStore)
{
if (!hook)
continue;
int32 hookType = hook->ExteriorComponentTypeID;
auto typeItr = compsByType.find(hookType);
if (typeItr == compsByType.end())
continue;
// Find default component (Flags & 0x1 = IsDefaultFixture) that belongs to the
// same parent component's group
ExteriorComponentEntry const* bestComp = nullptr;
for (ExteriorComponentEntry const* comp : typeItr->second)
{
if (comp->Flags & 0x1) // IsDefaultFixture
{
// Check if this component shares a group with the hook's parent component
// via ExteriorComponentGroupXHook
bestComp = comp;
break;
}
}
if (!bestComp && !typeItr->second.empty())
bestComp = typeItr->second.front(); // fallback: first component of this type
if (bestComp)
_extCompByHookId[static_cast<int32>(hook->ID)] = bestComp;
}
// 3. Build exit point index
for (ExteriorComponentExitPointEntry const* exitPt : sExteriorComponentExitPointStore)
{
if (!exitPt)
continue;
_exitPointByExtComp[exitPt->ExteriorComponentID] = exitPt;
}
// 4. Build group indexes from ExteriorComponentXGroup
// 1b. Build group indexes from ExteriorComponentXGroup (needed by step 2)
for (ExteriorComponentXGroupEntry const* xg : sExteriorComponentXGroupStore)
{
if (!xg)
@@ -1348,11 +1309,93 @@ void HousingMgr::BuildExteriorComponentIndexes()
_extCompsByGroup[groupID].push_back(compID);
}
// 2. Build reverse lookup: hookID ? default component that should be placed there.
// Chain: ExteriorComponentGroupXHook maps (GroupID ? HookID), meaning
// "this group of components can be installed at this hook."
// For each hook, we find which groups link to it, then pick the IsDefault
// component from those groups with matching ExteriorComponentTypeID.
//
// NOTE: Hook IDs are NOT globally unique ? the ExteriorComponentHook DB2
// uses ExteriorComponentID as a ParentIndexField, so multiple parent
// components can share the same hook ID. The _extCompByHookId map uses
// a composite key (hookID, parentCompID) to disambiguate.
// Step 2a: Build reverse index from GroupXHook: hookID ? list of groupIDs
std::unordered_map<int32, std::vector<int32>> groupsByHookId; // hookID ? [groupID, ...]
for (ExteriorComponentGroupXHookEntry const* gxh : sExteriorComponentGroupXHookStore)
{
if (!gxh)
continue;
groupsByHookId[gxh->ExteriorComponentHookID].push_back(gxh->ExteriorComponentGroupID);
}
// Step 2b: For each hookID in GroupXHook, look up the hook entry via LookupEntry
// (store iteration only yields a subset; LookupEntry reaches all 23k+ entries)
for (auto const& [hookId, groupList] : groupsByHookId)
{
ExteriorComponentHookEntry const* hook = sExteriorComponentHookStore.LookupEntry(hookId);
if (!hook)
continue;
int32 hookType = hook->ExteriorComponentTypeID;
int32 parentCompID = hook->ExteriorComponentID;
int64 compositeKey = (int64(hookId) << 32) | uint32(parentCompID);
ExteriorComponentEntry const* bestComp = nullptr;
ExteriorComponentEntry const* fallbackComp = nullptr;
// Find default component from linked groups
for (int32 groupID : groupList)
{
auto compItr = _extCompsByGroup.find(groupID);
if (compItr == _extCompsByGroup.end())
continue;
for (uint32 compID : compItr->second)
{
ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(compID);
if (!comp || comp->Type != hookType)
continue;
if (!fallbackComp)
fallbackComp = comp;
if (comp->Flags & 0x1) // IsDefaultFixture
{
bestComp = comp;
break;
}
}
if (bestComp)
break;
}
if (!bestComp)
bestComp = fallbackComp;
if (bestComp)
{
_extCompByHookId[compositeKey] = bestComp;
TC_LOG_DEBUG("housing", "HookID {} (parent={}, type={}) ? comp {} '{}' (Flags=0x{:X}, group-resolved)",
hook->ID, parentCompID, hookType, bestComp->ID,
bestComp->Name[DEFAULT_LOCALE] ? bestComp->Name[DEFAULT_LOCALE] : "",
bestComp->ParentComponentID, bestComp->Flags);
}
}
// 3. Build exit point index
for (ExteriorComponentExitPointEntry const* exitPt : sExteriorComponentExitPointStore)
{
if (!exitPt)
continue;
_exitPointByExtComp[exitPt->ExteriorComponentID] = exitPt;
}
TC_LOG_INFO("housing", "HousingMgr::BuildExteriorComponentIndexes: "
"hooks={} compByHook={} exitPoints={} groups={} compsInGroups={}",
"hooks={} compByHook={} exitPoints={} groups={} compsInGroups={} parentChildren={} wmoRoots={}",
uint32(_hooksByExtComp.size()), uint32(_extCompByHookId.size()),
uint32(_exitPointByExtComp.size()), uint32(_groupByExtComp.size()),
uint32(_extCompsByGroup.size()));
uint32(_extCompsByGroup.size()), uint32(_childrenByExtComp.size()),
uint32(_rootCompsByWmoDataId.size()));
}
@@ -1362,9 +1405,10 @@ std::vector<ExteriorComponentHookEntry const*> const* HousingMgr::GetHooksOnComp
return itr != _hooksByExtComp.end() ? &itr->second : nullptr;
}
ExteriorComponentEntry const* HousingMgr::GetComponentAtHook(int32 hookID) const
ExteriorComponentEntry const* HousingMgr::GetComponentAtHook(int32 hookID, uint32 parentCompID) const
{
auto itr = _extCompByHookId.find(hookID);
int64 compositeKey = (int64(hookID) << 32) | uint32(parentCompID);
auto itr = _extCompByHookId.find(compositeKey);
return itr != _extCompByHookId.end() ? itr->second : nullptr;
}
@@ -1380,6 +1424,18 @@ int32 HousingMgr::GetGroupForComponent(uint32 extCompID) const
return itr != _groupByExtComp.end() ? itr->second : 0;
}
std::vector<uint32> const* HousingMgr::GetChildComponents(uint32 parentCompID) const
{
auto itr = _childrenByExtComp.find(parentCompID);
return itr != _childrenByExtComp.end() ? &itr->second : nullptr;
}
std::vector<uint32> const* HousingMgr::GetRootComponentsForWmoData(uint32 wmoDataID) const
{
auto itr = _rootCompsByWmoDataId.find(wmoDataID);
return itr != _rootCompsByWmoDataId.end() ? &itr->second : nullptr;
}
std::vector<uint32> const* HousingMgr::GetComponentsInGroup(int32 groupID) const
{
auto itr = _extCompsByGroup.find(groupID);
+6 -2
View File
@@ -330,9 +330,11 @@ public:
// ExteriorComponent indexed lookups
std::vector<ExteriorComponentHookEntry const*> const* GetHooksOnComponent(uint32 extCompID) const;
ExteriorComponentEntry const* GetComponentAtHook(int32 hookID) const;
ExteriorComponentEntry const* GetComponentAtHook(int32 hookID, uint32 parentCompID) const;
ExteriorComponentExitPointEntry const* GetExitPoint(uint32 extCompID) const;
int32 GetGroupForComponent(uint32 extCompID) const;
std::vector<uint32> const* GetChildComponents(uint32 parentCompID) const;
std::vector<uint32> const* GetRootComponentsForWmoData(uint32 wmoDataID) const;
std::vector<uint32> const* GetComponentsInGroup(int32 groupID) const;
// Find the first HouseRoom entry with visual components (not the base room 18)
@@ -432,10 +434,12 @@ private:
// ExteriorComponent indexes
std::unordered_map<uint32 /*extCompID*/, std::vector<ExteriorComponentHookEntry const*>> _hooksByExtComp;
std::unordered_map<int32 /*hookID*/, ExteriorComponentEntry const*> _extCompByHookId;
std::unordered_map<int64 /*(hookID<<32)|parentCompID*/, ExteriorComponentEntry const*> _extCompByHookId;
std::unordered_map<uint32 /*extCompID*/, ExteriorComponentExitPointEntry const*> _exitPointByExtComp;
std::unordered_map<uint32 /*extCompID*/, int32 /*groupID*/> _groupByExtComp;
std::unordered_map<int32 /*groupID*/, std::vector<uint32 /*extCompID*/>> _extCompsByGroup;
std::unordered_map<uint32 /*parentCompID*/, std::vector<uint32 /*childCompID*/>> _childrenByExtComp;
std::unordered_map<uint32 /*wmoDataID*/, std::vector<uint32 /*compID*/>> _rootCompsByWmoDataId;
};
#define sHousingMgr HousingMgr::Instance()
@@ -229,10 +229,20 @@ namespace WorldPackets::Housing
void HousingFixtureSetCoreFixture::Read()
{
// Diagnostic: log raw packet bytes before parsing
TC_LOG_INFO("housing", "CMSG_HOUSING_FIXTURE_SET_CORE_FIXTURE raw packet: size={} rpos={}", _worldPacket.size(), _worldPacket.rpos());
{
std::string hexDump;
for (std::size_t i = _worldPacket.rpos(); i < _worldPacket.size() && i < _worldPacket.rpos() + 40; ++i)
hexDump += fmt::format("{:02X} ", _worldPacket[i]);
TC_LOG_INFO("housing", " raw bytes: {}", hexDump);
}
_worldPacket >> FixtureGuid;
_worldPacket >> ExteriorComponentID;
TC_LOG_DEBUG("network.opcode", "CMSG_HOUSING_FIXTURE_SET_CORE_FIXTURE FixtureGuid: {} ExteriorComponentID: {}", FixtureGuid.ToString(), ExteriorComponentID);
TC_LOG_INFO("housing", "CMSG_HOUSING_FIXTURE_SET_CORE_FIXTURE parsed: FixtureGuid={} ExteriorComponentID={} (rpos after={})",
FixtureGuid.ToString(), ExteriorComponentID, _worldPacket.rpos());
}
void HousingFixtureCreateFixture::Read()