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:
@@ -196,15 +196,26 @@ void MeshObject::InitHousingDecorData(ObjectGuid decorGuid, ObjectGuid houseGuid
|
|||||||
GetGUID().ToString(), decorGuid.ToString(), houseGuid.ToString(), flags, roomEntityGuid.ToString());
|
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*/,
|
int32 houseExteriorWmoDataID, uint8 exteriorComponentType /*= 9*/,
|
||||||
uint8 houseSize /*= 2*/, int32 exteriorComponentHookID /*= -1*/, bool isRoot /*= false*/)
|
uint8 houseSize /*= 2*/, int32 exteriorComponentHookID /*= -1*/, bool isRoot /*= false*/)
|
||||||
{
|
{
|
||||||
if (m_housingFixtureData.has_value())
|
if (m_housingFixtureData.has_value())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Uses the same m_housingFixtureData declared on Object (Object.h:167)
|
// FHousingFixture_C fragment (ID 34, 96 bytes, 11 fields with HasChangesMask<11>).
|
||||||
// Sniff-verified field values (11.2 retail MeshObject with FHousingFixture_C)
|
// 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)
|
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
|
||||||
.ModifyValue(&UF::HousingFixtureData::ExteriorComponentID), exteriorComponentID);
|
.ModifyValue(&UF::HousingFixtureData::ExteriorComponentID), exteriorComponentID);
|
||||||
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
|
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
|
||||||
@@ -213,9 +224,15 @@ void MeshObject::InitHousingFixtureData(ObjectGuid houseGuid, int32 exteriorComp
|
|||||||
.ModifyValue(&UF::HousingFixtureData::ExteriorComponentHookID), exteriorComponentHookID);
|
.ModifyValue(&UF::HousingFixtureData::ExteriorComponentHookID), exteriorComponentHookID);
|
||||||
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
|
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
|
||||||
.ModifyValue(&UF::HousingFixtureData::HouseGUID), houseGuid);
|
.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)
|
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
|
// GameObjectGUID: sniff confirms empty for all fixture pieces
|
||||||
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
|
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
|
||||||
.ModifyValue(&UF::HousingFixtureData::ExteriorComponentType), exteriorComponentType);
|
.ModifyValue(&UF::HousingFixtureData::ExteriorComponentType), exteriorComponentType);
|
||||||
@@ -227,9 +244,10 @@ void MeshObject::InitHousingFixtureData(ObjectGuid houseGuid, int32 exteriorComp
|
|||||||
m_entityFragments.Add(WowCS::EntityFragment::FHousingFixture_C, IsInWorld(),
|
m_entityFragments.Add(WowCS::EntityFragment::FHousingFixture_C, IsInWorld(),
|
||||||
WowCS::GetRawFragmentData(m_housingFixtureData));
|
WowCS::GetRawFragmentData(m_housingFixtureData));
|
||||||
|
|
||||||
// Cache for targeted fixture lookup
|
// Cache for targeted fixture lookup and hierarchy traversal
|
||||||
_exteriorComponentHookID = exteriorComponentHookID;
|
_exteriorComponentHookID = exteriorComponentHookID;
|
||||||
_exteriorComponentID = exteriorComponentID;
|
_exteriorComponentID = exteriorComponentID;
|
||||||
|
_fixtureGuid = fixtureGuid;
|
||||||
|
|
||||||
// Root pieces get Tag_HouseExteriorRoot (225), child pieces get Tag_HouseExteriorPiece (224).
|
// 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.
|
// 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
|
else
|
||||||
m_entityFragments.Add(WowCS::EntityFragment::Tag_HouseExteriorPiece, IsInWorld());
|
m_entityFragments.Add(WowCS::EntityFragment::Tag_HouseExteriorPiece, IsInWorld());
|
||||||
|
|
||||||
TC_LOG_ERROR("housing", "MeshObject::InitHousingFixtureData: guid={} houseGuid={} "
|
TC_LOG_DEBUG("housing", "MeshObject::InitHousingFixtureData: meshGuid={} fixtureGuid={} "
|
||||||
"exteriorComponentID={} wmoDataID={} hookID={} componentType={} size={} isRoot={}",
|
"parentFixtureGuid={} houseGuid={} extCompID={} wmoDataID={} hookID={} type={} size={} isRoot={}",
|
||||||
GetGUID().ToString(), houseGuid.ToString(),
|
GetGUID().ToString(), fixtureGuid.ToString(), parentFixtureGuid.ToString(),
|
||||||
exteriorComponentID, houseExteriorWmoDataID, exteriorComponentHookID,
|
houseGuid.ToString(), exteriorComponentID, houseExteriorWmoDataID,
|
||||||
exteriorComponentType, houseSize, isRoot);
|
exteriorComponentHookID, exteriorComponentType, houseSize, isRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MeshObject::UpdateExteriorComponentID(int32 id)
|
void MeshObject::UpdateExteriorComponentID(int32 id)
|
||||||
|
|||||||
@@ -66,9 +66,11 @@ public:
|
|||||||
// Housing fixture
|
// Housing fixture
|
||||||
// isRoot: true for root pieces (no parent attachment), false for child pieces.
|
// isRoot: true for root pieces (no parent attachment), false for child pieces.
|
||||||
// Root pieces get Tag_HouseExteriorRoot (225), children get Tag_HouseExteriorPiece (224).
|
// 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,
|
int32 houseExteriorWmoDataID, uint8 exteriorComponentType = 9,
|
||||||
uint8 houseSize = 2, int32 exteriorComponentHookID = -1, bool isRoot = false);
|
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)
|
// Housing decor (adds FHousingDecor_C entity fragment for placed decor items)
|
||||||
// Sniff-verified: retail decor is ALWAYS MeshObject (never GO). TargetGameObjectGUID=empty.
|
// Sniff-verified: retail decor is ALWAYS MeshObject (never GO). TargetGameObjectGUID=empty.
|
||||||
@@ -123,6 +125,7 @@ private:
|
|||||||
bool _isExteriorRoot = false;
|
bool _isExteriorRoot = false;
|
||||||
int32 _exteriorComponentHookID = -1;
|
int32 _exteriorComponentHookID = -1;
|
||||||
int32 _exteriorComponentID = 0;
|
int32 _exteriorComponentID = 0;
|
||||||
|
ObjectGuid _fixtureGuid;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // TRINITYCORE_MESHOBJECT_H
|
#endif // TRINITYCORE_MESHOBJECT_H
|
||||||
|
|||||||
@@ -753,6 +753,13 @@ void WorldSession::HandleHousingDecorSetEditMode(WorldPackets::Housing::HousingD
|
|||||||
player->ClearUpdateMask(false);
|
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={}",
|
TC_LOG_DEBUG("housing", " EditMode EXIT: BNetAccountGuid={}",
|
||||||
response.BNetAccountGuid.ToString());
|
response.BNetAccountGuid.ToString());
|
||||||
}
|
}
|
||||||
@@ -1466,6 +1473,15 @@ void WorldSession::HandleHousingFixtureSetEditMode(WorldPackets::Housing::Housin
|
|||||||
housing->SyncUpdateFields();
|
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):
|
// Sniff-verified retail packet sequence (build 66337):
|
||||||
// #10161 S->C SMSG_UPDATE_OBJECT (56B) ? editor mode field change
|
// #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
|
// Validate ExteriorComponentID against DB2 store
|
||||||
uint32 componentID = housingFixtureSetCoreFixture.ExteriorComponentID;
|
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);
|
ExteriorComponentEntry const* componentEntry = sExteriorComponentStore.LookupEntry(componentID);
|
||||||
if (!componentEntry)
|
if (!componentEntry)
|
||||||
{
|
{
|
||||||
TC_LOG_DEBUG("housing", "CMSG_HOUSING_FIXTURE_SET_CORE_FIXTURE ExteriorComponentID {} not found in DB2",
|
TC_LOG_INFO("housing", "CMSG_HOUSING_FIXTURE_SET_CORE_FIXTURE ExteriorComponentID {} not found in DB2 (store has {} entries)",
|
||||||
componentID);
|
componentID, sExteriorComponentStore.GetNumRows());
|
||||||
WorldPackets::Housing::HousingFixtureSetCoreFixtureResponse response;
|
WorldPackets::Housing::HousingFixtureSetCoreFixtureResponse response;
|
||||||
response.Result = static_cast<uint8>(HOUSING_RESULT_FIXTURE_NOT_FOUND);
|
response.Result = static_cast<uint8>(HOUSING_RESULT_FIXTURE_NOT_FOUND);
|
||||||
SendPacket(response.Write());
|
SendPacket(response.Write());
|
||||||
return;
|
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] : "",
|
componentID, componentEntry->Name[DEFAULT_LOCALE] ? componentEntry->Name[DEFAULT_LOCALE] : "",
|
||||||
componentEntry->Type, componentEntry->Size, componentEntry->Flags,
|
componentEntry->Type, componentEntry->Size, componentEntry->Flags,
|
||||||
componentEntry->ParentComponentID, componentEntry->HouseExteriorWmoDataID);
|
componentEntry->ParentComponentID, componentEntry->HouseExteriorWmoDataID);
|
||||||
@@ -1773,7 +1793,7 @@ void WorldSession::HandleHousingFixtureDeleteFixture(WorldPackets::Housing::Hous
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Spawn default component back at this hook (DB2 default)
|
// 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)
|
if (defaultComp)
|
||||||
{
|
{
|
||||||
housingMap->SpawnFixtureAtHook(plotIndex, removedHookID, defaultComp->ID,
|
housingMap->SpawnFixtureAtHook(plotIndex, removedHookID, defaultComp->ID,
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ void HousingMap::SpawnPlotGameObjects()
|
|||||||
// fragment, SpellForVisuals, SpellXSpellVisualID) BEFORE the CREATE_OBJECT
|
// fragment, SpellForVisuals, SpellXSpellVisualID) BEFORE the CREATE_OBJECT
|
||||||
// packet is sent. The client needs FHousingPlotAreaTrigger_C and
|
// packet is sent. The client needs FHousingPlotAreaTrigger_C and
|
||||||
// DecalPropertiesId=621 in the initial create to render the plot border decal.
|
// 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)
|
if (plotAt)
|
||||||
{
|
{
|
||||||
PhasingHandler::InitDbPhaseShift(plotAt->GetPhaseShift(), PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
|
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).
|
// All other pieces (roof, door, chimney, windows) get Tag_HouseExteriorPiece (224).
|
||||||
// The client uses Tag_HouseExteriorRoot to identify the fixture GUID for edit mode.
|
// The client uses Tag_HouseExteriorRoot to identify the fixture GUID for edit mode.
|
||||||
bool isRoot = (exteriorComponentType == 9) && attachParent.IsEmpty();
|
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);
|
exteriorComponentType, houseSize, exteriorComponentHookID, isRoot);
|
||||||
|
|
||||||
// Now add to map ? this triggers the create packet with all fragments included
|
// 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);
|
uint32 coreExtCompID = static_cast<uint32>(exteriorComponentID);
|
||||||
int32 groupID = sHousingMgr.GetGroupForComponent(coreExtCompID);
|
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)
|
// A house consists of multiple independent root components (Base type=9, Roof type=10, etc.)
|
||||||
std::vector<uint32> const* groupComps = sHousingMgr.GetComponentsInGroup(groupID);
|
// all sharing the same HouseExteriorWmoDataID. Each root is spawned independently at the
|
||||||
if (groupComps && !groupComps->empty())
|
// 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;
|
// Group roots by type, filtering to match the house's Size
|
||||||
for (uint32 compID : *groupComps)
|
uint8 houseSize = coreComp->Size;
|
||||||
|
std::unordered_map<uint8 /*type*/, std::vector<uint32>> rootsByType;
|
||||||
|
for (uint32 rootID : *rootComps)
|
||||||
{
|
{
|
||||||
ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(compID);
|
ExteriorComponentEntry const* rc = sExteriorComponentStore.LookupEntry(rootID);
|
||||||
if (!comp || comp->ParentComponentID > 0) // skip children ? they'll be spawned recursively
|
if (rc && rc->ModelFileDataID > 0 && rc->Size == houseSize)
|
||||||
continue;
|
rootsByType[rc->Type].push_back(rootID);
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalSpawned > 0)
|
for (auto const& [type, compIDs] : rootsByType)
|
||||||
{
|
{
|
||||||
// Check which components were included in the data-driven group.
|
uint32 selectedCompID = 0;
|
||||||
// If missing, spawn them from hardcoded data.
|
|
||||||
bool roofFound = false;
|
if (type == coreComp->Type)
|
||||||
bool doorFound = false;
|
|
||||||
bool chimneyFound = false;
|
|
||||||
MeshObject* basePieceDd = nullptr;
|
|
||||||
MeshObject* roofPieceDd = nullptr;
|
|
||||||
auto meshItr = _meshObjects.find(plotIndex);
|
|
||||||
if (meshItr != _meshObjects.end())
|
|
||||||
{
|
{
|
||||||
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)
|
selectedCompID = id; // prefer IsDefault
|
||||||
{
|
break;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (factionRestriction == NEIGHBORHOOD_FACTION_ALLIANCE)
|
if (selectedCompID)
|
||||||
{
|
{
|
||||||
if (!roofFound)
|
ExteriorComponentEntry const* selComp = sExteriorComponentStore.LookupEntry(selectedCompID);
|
||||||
{
|
TC_LOG_INFO("housing", "SpawnFullHouseMeshObjects: Spawning root type={} comp={} '{}' "
|
||||||
roofPieceDd = SpawnHouseMeshObject(plotIndex, 7420602, /*isWMO*/ true,
|
"(wmoDataID={}, size={}, ModelFDID={})",
|
||||||
housePos, houseRot, 1.0f,
|
type, selectedCompID,
|
||||||
houseGuid, 1503, houseExteriorWmoDataID,
|
selComp && selComp->Name[DEFAULT_LOCALE] ? selComp->Name[DEFAULT_LOCALE] : "",
|
||||||
/*exteriorComponentType*/ 10, /*houseSize*/ 2, /*hookID*/ -1,
|
wmoDataID, houseSize, selComp ? selComp->ModelFileDataID : 0);
|
||||||
ObjectGuid::Empty, /*attachFlags*/ 0);
|
|
||||||
totalSpawned++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!doorFound && basePieceDd)
|
totalSpawned += SpawnExtCompTree(plotIndex, selectedCompID,
|
||||||
{
|
housePos, houseRot,
|
||||||
SpawnHouseMeshObject(plotIndex, 7450804, /*isWMO*/ true,
|
houseGuid, houseExteriorWmoDataID,
|
||||||
Position(9.2805f, -3.4555f, -0.5611f, 0.0f),
|
ObjectGuid::Empty, nullptr, 0, fixtureOverrides);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 "
|
TC_LOG_DEBUG("housing", "HousingMap::SpawnFullHouseMeshObjects: ExteriorComponent {} not found "
|
||||||
"exteriorComponentID {} ? using hardcoded spawn for plot {}",
|
"? using hardcoded spawn for plot {}", exteriorComponentID, plotIndex);
|
||||||
exteriorComponentID, plotIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === HARDCODED FALLBACK ===
|
// === 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
|
// Determine attach flags: root pieces (no parent) use 0, children use 3
|
||||||
uint8 attachFlags = parentGuid.IsEmpty() ? 0 : 3;
|
uint8 attachFlags = parentGuid.IsEmpty() ? 0 : 3;
|
||||||
|
|
||||||
// For CreateFixture: at depth=0, the hookIDOverride tells the client which hook point
|
// The hookIDOverride tells the client which hook point this component occupies.
|
||||||
// this component occupies. The ExteriorComponent has no hook ID field ? hook relationships
|
// This must propagate at ALL depths (not just depth=0) because during initial house spawn
|
||||||
// come from ExteriorComponentHook and ExteriorComponentGroupXHook tables.
|
// via SpawnFullHouseMeshObjects, hooks are iterated at depth >= 1 with valid hookIDOverride.
|
||||||
int32 effectiveHookID = (depth == 0 && hookIDOverride > 0) ? hookIDOverride : -1;
|
int32 effectiveHookID = (hookIDOverride > 0) ? hookIDOverride : -1;
|
||||||
|
|
||||||
MeshObject* mesh = SpawnHouseMeshObject(plotIndex, comp->ModelFileDataID, /*isWMO*/ true,
|
MeshObject* mesh = SpawnHouseMeshObject(plotIndex, comp->ModelFileDataID, /*isWMO*/ true,
|
||||||
pos, rot, 1.0f,
|
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
|
// Use worldPos for children: root's worldPos is itself, child's is the root's position
|
||||||
Position const* childWorldPos = worldPos ? worldPos : &pos;
|
Position const* childWorldPos = worldPos ? worldPos : &pos;
|
||||||
|
|
||||||
|
std::set<uint32> spawnedChildComps; // track to avoid double-spawning
|
||||||
|
|
||||||
// Recurse into hooks on this component
|
// Recurse into hooks on this component
|
||||||
auto const* hooks = sHousingMgr.GetHooksOnComponent(extCompID);
|
auto const* hooks = sHousingMgr.GetHooksOnComponent(extCompID);
|
||||||
if (hooks)
|
if (hooks)
|
||||||
@@ -2364,10 +2344,17 @@ uint32 HousingMap::SpawnExtCompTree(uint8 plotIndex, uint32 extCompID,
|
|||||||
childComp = sExteriorComponentStore.LookupEntry(overrideItr->second);
|
childComp = sExteriorComponentStore.LookupEntry(overrideItr->second);
|
||||||
}
|
}
|
||||||
if (!childComp)
|
if (!childComp)
|
||||||
childComp = sHousingMgr.GetComponentAtHook(static_cast<int32>(hook->ID));
|
childComp = sHousingMgr.GetComponentAtHook(static_cast<int32>(hook->ID), extCompID);
|
||||||
if (!childComp)
|
if (!childComp)
|
||||||
continue;
|
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
|
// Hook position/rotation are local-space offsets relative to the parent
|
||||||
Position hookPos(hook->Position[0], hook->Position[1], hook->Position[2], 0.0f);
|
Position hookPos(hook->Position[0], hook->Position[1], hook->Position[2], 0.0f);
|
||||||
QuaternionData hookRot;
|
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;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1265,79 +1265,40 @@ void HousingMgr::BuildExteriorComponentIndexes()
|
|||||||
_exitPointByExtComp.clear();
|
_exitPointByExtComp.clear();
|
||||||
_groupByExtComp.clear();
|
_groupByExtComp.clear();
|
||||||
_extCompsByGroup.clear();
|
_extCompsByGroup.clear();
|
||||||
|
_childrenByExtComp.clear();
|
||||||
|
_rootCompsByWmoDataId.clear();
|
||||||
|
|
||||||
// 1. Build hook index: which hooks are parented to each component
|
// 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)
|
if (!hook)
|
||||||
continue;
|
continue;
|
||||||
_hooksByExtComp[hook->ExteriorComponentID].push_back(hook);
|
_hooksByExtComp[hook->ExteriorComponentID].push_back(hook);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Build reverse lookup: hookID ? default component that should be placed there.
|
// 1a. Build child index and root-by-WMO index from ExteriorComponent.
|
||||||
// Chain: ExteriorComponentGroupXHook maps groups to hooks.
|
// ParentComponentID > 0 ? child of that component.
|
||||||
// For each hook, find components in groups linked to that hook with matching Type
|
// ParentComponentID == 0 ? root component, indexed by HouseExteriorWmoDataID.
|
||||||
// and IsDefaultFixture flag (Flags & 0x1).
|
// ExteriorComponent uses ParentIndexField (HouseExteriorWmoDataID), so use
|
||||||
// First, build group?hooks mapping from GroupXHook
|
// LookupEntry over GetNumRows() to reach all entries.
|
||||||
std::unordered_map<int32, std::vector<int32>> hooksByGroup; // groupID ? hookIDs
|
for (uint32 i = 0; i < sExteriorComponentStore.GetNumRows(); ++i)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
|
ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(i);
|
||||||
if (!comp)
|
if (!comp)
|
||||||
continue;
|
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
|
// 1b. Build group indexes from ExteriorComponentXGroup (needed by step 2)
|
||||||
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
|
|
||||||
for (ExteriorComponentXGroupEntry const* xg : sExteriorComponentXGroupStore)
|
for (ExteriorComponentXGroupEntry const* xg : sExteriorComponentXGroupStore)
|
||||||
{
|
{
|
||||||
if (!xg)
|
if (!xg)
|
||||||
@@ -1348,11 +1309,93 @@ void HousingMgr::BuildExteriorComponentIndexes()
|
|||||||
_extCompsByGroup[groupID].push_back(compID);
|
_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: "
|
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(_hooksByExtComp.size()), uint32(_extCompByHookId.size()),
|
||||||
uint32(_exitPointByExtComp.size()), uint32(_groupByExtComp.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;
|
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;
|
return itr != _extCompByHookId.end() ? itr->second : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1380,6 +1424,18 @@ int32 HousingMgr::GetGroupForComponent(uint32 extCompID) const
|
|||||||
return itr != _groupByExtComp.end() ? itr->second : 0;
|
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
|
std::vector<uint32> const* HousingMgr::GetComponentsInGroup(int32 groupID) const
|
||||||
{
|
{
|
||||||
auto itr = _extCompsByGroup.find(groupID);
|
auto itr = _extCompsByGroup.find(groupID);
|
||||||
|
|||||||
@@ -330,9 +330,11 @@ public:
|
|||||||
|
|
||||||
// ExteriorComponent indexed lookups
|
// ExteriorComponent indexed lookups
|
||||||
std::vector<ExteriorComponentHookEntry const*> const* GetHooksOnComponent(uint32 extCompID) const;
|
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;
|
ExteriorComponentExitPointEntry const* GetExitPoint(uint32 extCompID) const;
|
||||||
int32 GetGroupForComponent(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;
|
std::vector<uint32> const* GetComponentsInGroup(int32 groupID) const;
|
||||||
|
|
||||||
// Find the first HouseRoom entry with visual components (not the base room 18)
|
// Find the first HouseRoom entry with visual components (not the base room 18)
|
||||||
@@ -432,10 +434,12 @@ private:
|
|||||||
|
|
||||||
// ExteriorComponent indexes
|
// ExteriorComponent indexes
|
||||||
std::unordered_map<uint32 /*extCompID*/, std::vector<ExteriorComponentHookEntry const*>> _hooksByExtComp;
|
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*/, ExteriorComponentExitPointEntry const*> _exitPointByExtComp;
|
||||||
std::unordered_map<uint32 /*extCompID*/, int32 /*groupID*/> _groupByExtComp;
|
std::unordered_map<uint32 /*extCompID*/, int32 /*groupID*/> _groupByExtComp;
|
||||||
std::unordered_map<int32 /*groupID*/, std::vector<uint32 /*extCompID*/>> _extCompsByGroup;
|
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()
|
#define sHousingMgr HousingMgr::Instance()
|
||||||
|
|||||||
@@ -229,10 +229,20 @@ namespace WorldPackets::Housing
|
|||||||
|
|
||||||
void HousingFixtureSetCoreFixture::Read()
|
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 >> FixtureGuid;
|
||||||
_worldPacket >> ExteriorComponentID;
|
_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()
|
void HousingFixtureCreateFixture::Read()
|
||||||
|
|||||||
Reference in New Issue
Block a user